diff --git a/internal/topdata/topdata.go b/internal/topdata/topdata.go index 9e2f98f..ce270e4 100644 --- a/internal/topdata/topdata.go +++ b/internal/topdata/topdata.go @@ -1485,9 +1485,71 @@ func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) { } validateGeneratedFeatFamilyCompleteness(path, spec, ctx, report) } + if hasGeneratedFamilySpecs(specs, coreWeaponGeneratedFamilyKeys) { + validateBaseitemsWeaponFeatColumnCompleteness(featDataset.GeneratedDir, ctx, report) + } validateRequiredProductionGeneratedFamilies(featDataset.GeneratedDir, specs, report) } +var coreWeaponGeneratedFamilyKeys = []string{ + "weaponfocus", + "weaponspecialization", + "improvedcritical", + "overwhelmingcritical", + "greaterweaponfocus", + "greaterweaponspecialization", +} + +func hasGeneratedFamilySpecs(specs map[string]familyExpansionSpec, familyKeys []string) bool { + for _, familyKey := range familyKeys { + if _, _, ok := generatedFamilySpecByNormalizedKey(specs, nil, familyKey); !ok { + return false + } + } + return true +} + +func validateBaseitemsWeaponFeatColumnCompleteness(path string, ctx *featGeneratedContext, report *ValidationReport) { + rows, err := ctx.datasetRows("baseitems") + if err != nil { + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: path, + Message: fmt.Sprintf("validate baseitems weapon feat coverage: %v", err), + }) + return + } + coreColumns := []string{ + "WeaponFocusFeat", + "WeaponSpecializationFeat", + "WeaponImprovedCriticalFeat", + "EpicWeaponFocusFeat", + "EpicWeaponSpecializationFeat", + "EpicWeaponOverwhelmingCriticalFeat", + } + for _, sourceKey := range sortedStringMapKeys(rows) { + row := rows[sourceKey] + hasAny := false + missing := make([]string, 0) + for _, column := range coreColumns { + value, ok := lookupField(row, column) + if !ok || isNullishValue(value) { + missing = append(missing, column) + continue + } + hasAny = true + } + if !hasAny || len(missing) == 0 { + continue + } + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: path, + Message: fmt.Sprintf("baseitems row %q declares partial core weapon feat coverage; missing %s", sourceKey, strings.Join(missing, ", ")), + }) + } +} + func generatedFamilySpecByNormalizedKey(specs map[string]familyExpansionSpec, specPaths map[string]string, familyKey string) (familyExpansionSpec, string, bool) { normalized := normalizeKeyIdentity(familyKey) for candidateKey, spec := range specs { @@ -1503,18 +1565,8 @@ func generatedFamilySpecByNormalizedKey(specs map[string]familyExpansionSpec, sp } func validateRequiredProductionGeneratedFamilies(path string, specs map[string]familyExpansionSpec, report *ValidationReport) { - weaponFamilies := []string{ - "weaponfocus", - "weaponspecialization", - "improvedcritical", - "overwhelmingcritical", - "greaterweaponfocus", - "greaterweaponspecialization", - } - for _, familyKey := range weaponFamilies { - if _, _, ok := generatedFamilySpecByNormalizedKey(specs, nil, familyKey); !ok { - return - } + if !hasGeneratedFamilySpecs(specs, coreWeaponGeneratedFamilyKeys) { + return } if _, _, ok := generatedFamilySpecByNormalizedKey(specs, nil, "weaponofchoice"); !ok { report.Diagnostics = append(report.Diagnostics, Diagnostic{ diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index d4add96..74eb9c3 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -2921,6 +2921,42 @@ func TestParseFamilyExpansionRejectsInvalidLegacyFamilyKeys(t *testing.T) { } } +func TestValidateBaseitemsWeaponFeatColumnCompletenessRejectsPartialCoreFamilies(t *testing.T) { + ctx := &featGeneratedContext{ + rowsByDataset: map[string]map[string]map[string]any{ + "baseitems": { + "baseitems:testblade": { + "key": "baseitems:testblade", + "WeaponFocusFeat": 1000, + "WeaponSpecializationFeat": 1001, + "WeaponImprovedCriticalFeat": 1002, + "EpicWeaponFocusFeat": 1003, + "EpicWeaponSpecializationFeat": 1004, + "EpicWeaponOverwhelmingCriticalFeat": "****", + }, + "baseitems:prop": { + "key": "baseitems:prop", + "WeaponType": 2, + }, + }, + }, + } + report := ValidationReport{} + + validateBaseitemsWeaponFeatColumnCompleteness("generated", ctx, &report) + + if !report.HasErrors() { + t.Fatalf("expected partial core weapon feat columns to be rejected") + } + if len(report.Diagnostics) != 1 { + t.Fatalf("expected one diagnostic, got %#v", report.Diagnostics) + } + if !strings.Contains(report.Diagnostics[0].Message, "baseitems:testblade") || + !strings.Contains(report.Diagnostics[0].Message, "EpicWeaponOverwhelmingCriticalFeat") { + t.Fatalf("unexpected diagnostic: %#v", report.Diagnostics[0]) + } +} + func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))