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
+74 -2
View File
@@ -3902,15 +3902,24 @@ func (r *valueResolver) resolveValue(row map[string]any, field string, value any
switch typed := value.(type) {
case map[string]any:
if encoding, ok := r.valueEncodingForField(field); ok {
mode := strings.TrimSpace(encoding.Mode)
_, hasList := typed["list"]
_, hasAllExcept := typed["all_except"]
if hasList || hasAllExcept {
_, hasIDs := typed["ids"]
if mode == "packed_hex_list" && (hasList || hasAllExcept) {
value, err := encodeConfiguredValueList(typed, encoding)
if err != nil {
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
}
return tlkCompiledValue{Value: value}, nil
}
if mode == "id_hex_list" && hasIDs {
value, err := r.encodeConfiguredIDHexList(typed, encoding)
if err != nil {
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
}
return tlkCompiledValue{Value: value}, nil
}
}
if refID, ok := typed["id"]; ok {
if refKey, ok := refID.(string); ok && strings.Contains(refKey, ":") {
@@ -4028,17 +4037,80 @@ func encodeConfiguredAlignmentHexList(raw any, encoding project.TopDataValueEnco
return formatConfiguredPackedHexList(values, encoding), nil
}
func (r *valueResolver) encodeConfiguredIDHexList(obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) {
if strings.TrimSpace(encoding.Mode) != "id_hex_list" {
return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode)
}
for key := range obj {
if key != "ids" {
return "", fmt.Errorf("id list object contains unsupported key %q", key)
}
}
rawList, ok := obj["ids"].([]any)
if !ok {
return "", fmt.Errorf("ids must be an array")
}
values := make([]int, 0, len(rawList))
for _, raw := range rawList {
value, err := r.parseConfiguredIDListValue(raw, encoding)
if err != nil {
return "", err
}
values = append(values, value)
}
return formatConfiguredPackedHexList(values, encoding), nil
}
func (r *valueResolver) parseConfiguredIDListValue(raw any, encoding project.TopDataValueEncodingConfig) (int, error) {
switch typed := raw.(type) {
case map[string]any:
for key := range typed {
if key != "id" {
return 0, fmt.Errorf("id list item contains unsupported key %q", key)
}
}
ref, ok := typed["id"]
if !ok {
return 0, fmt.Errorf("id list item must contain id")
}
return r.parseConfiguredIDListValue(ref, encoding)
case string:
trimmed := strings.TrimSpace(typed)
if trimmed == "" {
return 0, fmt.Errorf("id list value must not be empty")
}
if strings.Contains(trimmed, ":") {
id, ok := r.keyToID[trimmed]
if !ok {
return 0, fmt.Errorf("unknown key reference: %s", trimmed)
}
return parseConfiguredListValue(id, encoding)
}
return parseConfiguredListValue(trimmed, encoding)
default:
return parseConfiguredListValue(raw, encoding)
}
}
func expandConfiguredAlignmentList(raw any) ([]int, error) {
if isNullLike(raw) {
return []int{0}, nil
}
switch typed := raw.(type) {
case map[string]any:
_, hasAlignments := typed["alignments"]
_, hasList := typed["list"]
if hasAlignments && hasList {
return nil, fmt.Errorf("alignment list object cannot contain both alignments and list")
}
for key := range typed {
if key != "list" {
if key != "alignments" && key != "list" {
return nil, fmt.Errorf("alignment list object contains unsupported key %q", key)
}
}
if hasAlignments {
return expandConfiguredAlignmentList(typed["alignments"])
}
return expandConfiguredAlignmentList(typed["list"])
case []any:
values := make([]int, 0, len(typed))