ID List Support expansion

**Scope Audited**
- Topdata configured value encoders in [native.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/topdata/native.go:3894)
- Config validation in [project.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/project/project.go:799)
- Wiki alignment formatting
- Module topdata config/docs and authored `PreferredAlignments` / `ProficiencyFeats` files

**Changes Made**
- Added generic `id_hex_list` support. It works for any configured dataset/column and emits packed hex row IDs from `ids`.
- Updated alignment encoding to accept explicit `alignments`; legacy `list` still works.
- Kept numeric packed lists on `list` / `all_except`.
- Configured `baseitems.ProficiencyFeats` in [nwn-tool.yaml](/home/vicky/Projects/nwnee-shadowsoverwestgate/module/nwn-tool.yaml:73).
- Migrated module proficiency authoring to `ids` and racial alignment authoring to `alignments`.
- Updated docs in [module/topdata/README.md](/home/vicky/Projects/nwnee-shadowsoverwestgate/module/topdata/README.md:157).

**Configuration / Schema Impact**
- New `topdata.value_encodings[].mode`: `id_hex_list`.
- `id_hex_list` accepts:
  - `"feat:some_key"`
  - `{ "id": "feat:some_key" }`
  - numeric IDs, still range-checked by `min`, `max`, `hex_width`.

**Compatibility Impact**
- Existing numeric packed lists are unchanged.
- Existing alignment `{ "list": [...] }` remains accepted, but docs and module data now prefer `{ "alignments": [...] }`.
- ID-list behavior is config-driven, not hardcoded to proficiencies.

**Tests Added / Updated**
- Added build test for generic `id_hex_list`.
- Added validation test for unknown ID references.
- Updated alignment tests to use `alignments`.
- Updated wiki alignment tests for explicit `alignments` plus legacy `list`.

**Validation Performed**
- `go test ./internal/topdata` passed.
- `go test ./internal/project` passed.
- `go test ./...` passed.
- `SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./scripts/run-nwn-tool.sh config validate` passed in `module/`.

**Remaining Risk**
- `./validate-topdata.sh` in `module/` is currently blocked by existing staged module authored-content changes: generated feat/baseitems validation reports `dataset baseitems: override must specify id or key` from `topdata/data/feat/generated`. I did not try to repair that unrelated staged work.
This commit is contained in:
2026-05-26 13:39:07 +02:00
parent 37db91314b
commit 22e3fabbbb
5 changed files with 192 additions and 9 deletions
+104 -2
View File
@@ -266,7 +266,7 @@ func TestBuildNativeEncodesConfiguredAlignmentHexLists(t *testing.T) {
"overrides": [
{
"key": "racialtypes:human",
"PreferredAlignments": {"list": ["le", "CG", "tn"]}
"PreferredAlignments": {"alignments": ["le", "CG", "tn"]}
}
]
}`+"\n")
@@ -290,6 +290,64 @@ func TestBuildNativeEncodesConfiguredAlignmentHexLists(t *testing.T) {
}
}
func TestBuildNativeEncodesConfiguredIDHexLists(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "baseitems", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"columns": ["LABEL"],
"rows": [
{"id": 1, "key": "feat:weapon_proficiency_martial", "LABEL": "WeaponProfMartial"},
{"id": 7, "key": "feat:weapon_proficiency_elf", "LABEL": "WeaponProfElf"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{
"feat:weapon_proficiency_martial": 1,
"feat:weapon_proficiency_elf": 7
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{
"output": "baseitems.2da",
"columns": ["Label", "ProficiencyFeats"],
"rows": [
{"id": 42, "key": "baseitems:longsword", "Label": "Longsword"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), `{"baseitems:longsword":42}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "modules", "10_longsword.json"), `{
"overrides": [
{
"key": "baseitems:longsword",
"ProficiencyFeats": {
"ids": [
"feat:weapon_proficiency_martial",
{"id": "feat:weapon_proficiency_elf"}
]
}
}
]
}`+"\n")
proj := testProject(root)
proj.Config.TopData.ValueEncodings = []project.TopDataValueEncodingConfig{
{Dataset: "baseitems", Column: "ProficiencyFeats", Mode: "id_hex_list", Min: 0, Max: 65535, HexWidth: 4},
}
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
if err != nil {
t.Fatalf("BuildNativeWithOptions failed: %v", err)
}
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "baseitems.2da"))
if err != nil {
t.Fatalf("read baseitems.2da: %v", err)
}
got := string(raw)
if !strings.Contains(got, "0x00010007") {
t.Fatalf("expected compiled baseitems.2da to contain encoded proficiency feat IDs, got:\n%s", got)
}
}
func TestBuildNativeAppliesConfiguredValueDefaults(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
@@ -382,7 +440,7 @@ func TestValidateProjectRejectsInvalidConfiguredAlignmentHexList(t *testing.T) {
"overrides": [
{
"key": "racialtypes:human",
"PreferredAlignments": {"list": ["lawful evil"]}
"PreferredAlignments": {"alignments": ["lawful evil"]}
}
]
}`+"\n")
@@ -401,6 +459,50 @@ func TestValidateProjectRejectsInvalidConfiguredAlignmentHexList(t *testing.T) {
}
}
func TestValidateProjectRejectsUnknownConfiguredIDHexListReference(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "baseitems", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"columns": ["LABEL"],
"rows": [
{"id": 1, "key": "feat:weapon_proficiency_martial", "LABEL": "WeaponProfMartial"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{"feat:weapon_proficiency_martial":1}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{
"output": "baseitems.2da",
"columns": ["Label", "ProficiencyFeats"],
"rows": [
{"id": 42, "key": "baseitems:longsword", "Label": "Longsword"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), `{"baseitems:longsword":42}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "modules", "10_longsword.json"), `{
"overrides": [
{
"key": "baseitems:longsword",
"ProficiencyFeats": {"ids": ["feat:missing_proficiency"]}
}
]
}`+"\n")
proj := testProject(root)
proj.Config.TopData.ValueEncodings = []project.TopDataValueEncodingConfig{
{Dataset: "baseitems", Column: "ProficiencyFeats", Mode: "id_hex_list", Min: 0, Max: 65535, HexWidth: 4},
}
report := ValidateProject(proj)
if !report.HasErrors() {
t.Fatalf("expected unknown ID list reference to fail validation, got:\n%s", diagnosticsText(report.Diagnostics))
}
if !diagnosticsContain(report.Diagnostics, "unknown key reference: feat:missing_proficiency") {
t.Fatalf("expected unknown key reference diagnostic, got:\n%s", diagnosticsText(report.Diagnostics))
}
}
func TestEncodeConfiguredPackedHexListSupportsAllExcept(t *testing.T) {
encoding := project.TopDataValueEncodingConfig{
Dataset: "racialtypes/core",