Explicit masterfeats/feats removal
This commit is contained in:
@@ -42,12 +42,16 @@ One concrete example already observed:
|
|||||||
- validation in `internal/topdata/topdata.go` still hard-fails specifically for
|
- validation in `internal/topdata/topdata.go` still hard-fails specifically for
|
||||||
`topdata/data/masterfeats/base.json` if `output` is missing
|
`topdata/data/masterfeats/base.json` if `output` is missing
|
||||||
|
|
||||||
That mismatch proves the current methodology is stale:
|
That mismatch proved the current methodology was stale:
|
||||||
|
|
||||||
- the runtime/build side treats `masterfeats` like a generic dataset
|
- the runtime/build side treats `masterfeats` like a generic dataset
|
||||||
- the validator still treats it like a special namespace with explicit required
|
- the validator still treats it like a special namespace with explicit required
|
||||||
metadata
|
metadata
|
||||||
|
|
||||||
|
Current status: the output-name mismatch has been fixed for `masterfeats` and
|
||||||
|
the equivalent `feat` base dataset rule by sharing the derived output-name rule
|
||||||
|
between discovery and validation.
|
||||||
|
|
||||||
This same category of problem appears in several other places.
|
This same category of problem appears in several other places.
|
||||||
|
|
||||||
## Findings From Current Audit
|
## Findings From Current Audit
|
||||||
@@ -67,17 +71,17 @@ This same category of problem appears in several other places.
|
|||||||
Some of these are legitimate dataset-shape differences. Others are stale
|
Some of these are legitimate dataset-shape differences. Others are stale
|
||||||
special-cases that should be generalized.
|
special-cases that should be generalized.
|
||||||
|
|
||||||
### 2. `masterfeats` still carries outdated explicit validation rules
|
### 2. `masterfeats` carries dataset-specific invariants
|
||||||
|
|
||||||
`validateMasterfeatsBaseFile()` currently requires:
|
`validateMasterfeatsBaseFile()` currently enforces:
|
||||||
|
|
||||||
- `output == masterfeats.2da`
|
- explicit `output == masterfeats.2da` when `output` is authored
|
||||||
- specific required columns
|
- specific required columns
|
||||||
- `masterfeats:` row keys
|
- `masterfeats:` row keys
|
||||||
|
|
||||||
The key-prefix and column requirements may still be valid dataset invariants.
|
The key-prefix and column requirements may still be valid dataset invariants.
|
||||||
The `output` requirement is not clearly justified anymore when generic dataset
|
The stale requirement that `output` must be authored has been removed; validation
|
||||||
discovery already computes a default output name from location.
|
now shares the same derived default output-name rule as native discovery.
|
||||||
|
|
||||||
### 3. Migration still seeds explicit dataset exceptions
|
### 3. Migration still seeds explicit dataset exceptions
|
||||||
|
|
||||||
@@ -128,7 +132,9 @@ keep inheriting brittle assumptions.
|
|||||||
|
|
||||||
### Work Item 1: Remove outdated `masterfeats.output` hard requirement
|
### Work Item 1: Remove outdated `masterfeats.output` hard requirement
|
||||||
|
|
||||||
Investigate and likely change `validateMasterfeatsBaseFile()` so:
|
Status: implemented.
|
||||||
|
|
||||||
|
`validateMasterfeatsBaseFile()` now:
|
||||||
|
|
||||||
- missing `output` is accepted when generic dataset discovery can derive the same
|
- missing `output` is accepted when generic dataset discovery can derive the same
|
||||||
output name deterministically
|
output name deterministically
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ Canonical shape:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- `output` must be exactly `masterfeats.2da`
|
- `output` may be omitted; native dataset discovery deterministically derives
|
||||||
|
`masterfeats.2da` from `topdata/data/masterfeats/base.json`
|
||||||
|
- if `output` is present, it must be exactly `masterfeats.2da`
|
||||||
- `columns` must include `LABEL`, `STRREF`, and `DESCRIPTION`
|
- `columns` must include `LABEL`, `STRREF`, and `DESCRIPTION`
|
||||||
- every canonical row must have a non-empty `key`
|
- every canonical row must have a non-empty `key`
|
||||||
- row keys must start with `masterfeats:`
|
- row keys must start with `masterfeats:`
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) {
|
|||||||
}
|
}
|
||||||
outputName, _ := baseData["output"].(string)
|
outputName, _ := baseData["output"].(string)
|
||||||
if strings.TrimSpace(outputName) == "" {
|
if strings.TrimSpace(outputName) == "" {
|
||||||
outputName = filepath.Base(path) + ".2da"
|
outputName = nativeDatasetDefaultOutputName(path, nativeDatasetBase)
|
||||||
}
|
}
|
||||||
datasets = append(datasets, nativeDataset{
|
datasets = append(datasets, nativeDataset{
|
||||||
Kind: nativeDatasetBase,
|
Kind: nativeDatasetBase,
|
||||||
@@ -561,7 +561,7 @@ func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) {
|
|||||||
}
|
}
|
||||||
outputName, _ := tableData["output"].(string)
|
outputName, _ := tableData["output"].(string)
|
||||||
if strings.TrimSpace(outputName) == "" {
|
if strings.TrimSpace(outputName) == "" {
|
||||||
outputName = strings.TrimSuffix(name, filepath.Ext(name)) + ".2da"
|
outputName = nativeDatasetDefaultOutputName(filePath, nativeDatasetPlain)
|
||||||
}
|
}
|
||||||
datasets = append(datasets, nativeDataset{
|
datasets = append(datasets, nativeDataset{
|
||||||
Kind: nativeDatasetPlain,
|
Kind: nativeDatasetPlain,
|
||||||
@@ -4208,6 +4208,14 @@ func outputStem(outputName string) string {
|
|||||||
return strings.TrimSuffix(outputName, filepath.Ext(outputName))
|
return strings.TrimSuffix(outputName, filepath.Ext(outputName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nativeDatasetDefaultOutputName(path string, kind nativeDatasetKind) string {
|
||||||
|
name := filepath.Base(path)
|
||||||
|
if kind == nativeDatasetPlain {
|
||||||
|
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
}
|
||||||
|
return name + ".2da"
|
||||||
|
}
|
||||||
|
|
||||||
func loadJSONObject(path string) (map[string]any, error) {
|
func loadJSONObject(path string) (map[string]any, error) {
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -456,20 +456,7 @@ func validateDataObject(path string, obj map[string]any, report *ValidationRepor
|
|||||||
|
|
||||||
func validateMasterfeatsBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
func validateMasterfeatsBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
||||||
validateRowsFile(path, obj, report, true)
|
validateRowsFile(path, obj, report, true)
|
||||||
|
validateDerivedBaseOutput(path, obj, "masterfeats", report)
|
||||||
if output, ok := obj["output"]; !ok {
|
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
|
||||||
Severity: SeverityError,
|
|
||||||
Path: path,
|
|
||||||
Message: "masterfeats base.json must contain output set to masterfeats.2da",
|
|
||||||
})
|
|
||||||
} else if outputText, ok := output.(string); !ok || strings.TrimSpace(outputText) != "masterfeats.2da" {
|
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
|
||||||
Severity: SeverityError,
|
|
||||||
Path: path,
|
|
||||||
Message: "masterfeats output must be masterfeats.2da",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
columns := extractValidationColumns(obj)
|
columns := extractValidationColumns(obj)
|
||||||
for _, required := range []string{"LABEL", "STRREF", "DESCRIPTION"} {
|
for _, required := range []string{"LABEL", "STRREF", "DESCRIPTION"} {
|
||||||
@@ -520,18 +507,17 @@ func validateMasterfeatsLockFile(path string, obj map[string]any, report *Valida
|
|||||||
|
|
||||||
func validateFeatBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
func validateFeatBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
||||||
validateRowsFile(path, obj, report, true)
|
validateRowsFile(path, obj, report, true)
|
||||||
|
validateDerivedBaseOutput(path, obj, "feat", report)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDerivedBaseOutput(path string, obj map[string]any, datasetLabel string, report *ValidationReport) {
|
||||||
if output, ok := obj["output"]; !ok {
|
if output, ok := obj["output"]; !ok {
|
||||||
|
return
|
||||||
|
} else if outputText, ok := output.(string); !ok || strings.TrimSpace(outputText) != nativeDatasetDefaultOutputName(filepath.Dir(path), nativeDatasetBase) {
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
Severity: SeverityError,
|
Severity: SeverityError,
|
||||||
Path: path,
|
Path: path,
|
||||||
Message: "feat base.json must contain output set to feat.2da",
|
Message: fmt.Sprintf("%s output must be %s", datasetLabel, nativeDatasetDefaultOutputName(filepath.Dir(path), nativeDatasetBase)),
|
||||||
})
|
|
||||||
} else if outputText, ok := output.(string); !ok || strings.TrimSpace(outputText) != "feat.2da" {
|
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
|
||||||
Severity: SeverityError,
|
|
||||||
Path: path,
|
|
||||||
Message: "feat output must be feat.2da",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,39 @@ func TestValidateProjectAcceptsInlineTLKSchema(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAndBuildDerivesFeatOutputWhenOmitted(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
|
||||||
|
"columns": ["LABEL", "FEAT", "DESCRIPTION"],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"key": "feat:test",
|
||||||
|
"LABEL": "FEAT_TEST",
|
||||||
|
"FEAT": {"tlk": {"key": "feat:test.name", "text": "Test Feat"}},
|
||||||
|
"DESCRIPTION": {"tlk": {"key": "feat:test.description", "text": "Test Description"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{"feat:test":0}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
report := ValidateProject(proj)
|
||||||
|
if report.HasErrors() {
|
||||||
|
t.Fatalf("expected omitted feat output to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected omitted feat output to build: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(result.Output2DADir, "feat.2da")); err != nil {
|
||||||
|
t.Fatalf("expected derived feat.2da output: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateProjectRejectsDuplicateJSONKeys(t *testing.T) {
|
func TestValidateProjectRejectsDuplicateJSONKeys(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
@@ -860,6 +893,42 @@ func TestValidateProjectAcceptsCanonicalMasterfeatsContract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAndBuildDerivesMasterfeatsOutputWhenOmitted(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "base.json"), `{
|
||||||
|
"columns": ["LABEL", "STRREF", "DESCRIPTION", "ICON"],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"key": "masterfeats:weaponfocus",
|
||||||
|
"LABEL": "WeaponFocus",
|
||||||
|
"STRREF": "6490",
|
||||||
|
"DESCRIPTION": "436",
|
||||||
|
"ICON": "ife_wepfoc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{
|
||||||
|
"masterfeats:weaponfocus": 0
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
report := ValidateProject(proj)
|
||||||
|
if report.HasErrors() {
|
||||||
|
t.Fatalf("expected omitted masterfeats output to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected omitted masterfeats output to build: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(result.Output2DADir, "masterfeats.2da")); err != nil {
|
||||||
|
t.Fatalf("expected derived masterfeats.2da output: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateProjectRejectsInvalidMasterfeatsContract(t *testing.T) {
|
func TestValidateProjectRejectsInvalidMasterfeatsContract(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
|
||||||
|
|||||||
Reference in New Issue
Block a user