Racial spell-like abilities only reach the in-game radial when the feat
has an OnMenu row in every cls_feat_<class>.2da. Racial feats are granted
by race, not by a class, so nothing puts them there automatically - they
were hand-written as ~23 rows in sow-topdata data/classes/feats/global.json
and prepended into every class table. That list drifts: the build already
carried 24 usable racial feats in the race tables but only 23 hand rows,
so one activatable feat was silently missing from the radial.
Generate those rows at build time from the one source that already marks
them - the UsableFeat column in the race feats tables (race_feat_*.2da).
Every feat flagged UsableFeat=1 gets one cls_feat row (List=3,
GrantedOnLevel=99, OnMenu=1) injected into every class table, deduped and
sorted. List=3 keeps it off every level-up selection list, GrantedOnLevel=99
sits above the level cap so no class ever actually grants it, OnMenu=1
renders the button once the creature holds the feat (possession stays the
job of chargen / the login racial-feat sync, sow-codebase#359).
The rules reuse the existing globalRules injection path (same dedup,
feat-existence check and label lookup) but apply unconditionally, so a
leftover hand row in global.json deduplicates to a no-op and the
sow-topdata cleanup can land separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
The custom palette taxonomy (#46) pins display strings to fixed TLK ids in `tlk/custom.tlk.yml`, and `registerInlineAtID` demanded each pinned id be free. But `.tlk_state.json` is **gitignored and per-machine** — each dev grows their own copy, and before #46 every ref got its id dynamically (first-come-first-served).
On any machine whose state predates #46, a ref could have already parked on a now-pinned id. That fails the build:
```
topdata validation failed with 1 error(s): error: native topdata buildability check failed:
TLK id 2689 is already reserved by "feat:yuanti/alternate_form.feat"
```
It only passes on machines whose state was regenerated after #46 (pinned window already clean). The `allocateID` reserved-skip that #46 added protects a *fresh* build, but does nothing about a *stale cache*.
## Fix
Make the pin authoritative over the per-machine cache. A stale cached owner sitting on a pinned id is evicted and reallocated a fresh id when next made active; only two pins fighting over the same id in `custom.tlk.yml` is now an error. This self-heals on the next build — no manual `.tlk_state.json` deletion needed.
Caveat: the evicted ref's strref shifts on affected machines. That's unavoidable (something must move off the pinned id), and dynamic strrefs were never stable across machines anyway.
## Tests
Added `TestBuildStandaloneTLKPinEvictsStaleStateOwner` (seeds a pre-#46 state with a feat parked on the pinned id, asserts the build succeeds, the pin owns it, and the feat is reallocated). Full `internal/topdata` suite + `go vet` pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #48
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
The legacy normalize migration is done (confirmed). This deletes the dead migration path: NormalizeProject + 26 *_migrate.go files, the importLegacyItempropsRegistry cluster, loadLegacyTLK, and their tests (~7.9k lines, -33 files worth). mergeLegacyTLK moved into tlk_native.go since the live TLK compiler still uses it for base_dialog.json. make check passes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #45
Reviewed-by: gitea-bot <gitea-bot@noreply.git.westgate.pw>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
Docs like AGENTS.md/CLAUDE.md under assets/ broke sow-topdata builds
with "unsupported topdata asset HAK resource extension". Skip .md in
both the validator and HAK package walker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewed-on: #32
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
forces build parity with crucible = local builds and CI/CD builds use different tools.
Reviewed-on: #14
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
The reprovision retired gitea.westgate.pw in favour of git.westgate.pw, but
internal/topdata still defaulted to the old host and made a live API call
during the native buildability check, so once CI checkout was fixed the
parts tests failed (DNS, then HTTP 404).
- defaultGiteaBaseURL -> https://git.westgate.pw; normalize git-ssh. -> git.
- Add stubEmptyPartsManifest (stub_test.go) and call it from the 2 non-optional
parts tests so they exercise the build offline. Per-test (t.Setenv) on
purpose: a global TestMain SOW_ASSETS_SERVER_URL would override the
git-remote-derived server other tests configure. VFX tests already pass
(optional consumer ignores HTTP 404).
make check + make smoke green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implemented on branches `feature/topdata-row-extensions` in both `toolkit` and `module`.
**Scope Audited**
- Toolkit native topdata collection/build path.
- Project config/effective config validation.
- Module `nwn-tool.yaml` topdata config and topdata README.
**Changes Made**
- Added `topdata.row_extensions` config with `repeat_last` support.
- Added glob dataset matching and build-time row extension for plain native datasets in [native.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/topdata/native.go:641).
- Extension copies emitted columns from the highest authored `Level`, increments `Level`, and assigns sequential `id`s through target level.
- Enabled rules for `classes/spellsgained/*` and `classes/spellsknown/*` to level 60 in [nwn-tool.yaml](/home/vicky/Projects/nwnee-shadowsoverwestgate/module/nwn-tool.yaml:90).
- Updated toolkit/module docs.
**Configuration / Compatibility**
- New public config type: `TopDataRowExtensionConfig` in [project.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/project/project.go:233).
- Existing behavior is unchanged unless a repo opts into `topdata.row_extensions`.
- Source JSON and lockfiles are not updated for generated extension rows.
**Tests / Validation**
- Added project config tests for loading and validation.
- Added native topdata tests for extension behavior, glob application, preserving manually authored rows, unmatched datasets, and failure cases.
- Ran:
- `go test ./internal/project ./internal/topdata`
- `go test ./internal/topdata -run TestBuildNativeKeepsGeneratedIDsStableAcrossRepeatedBuilds`
- `/tmp/sow-toolkit-rowext config validate` in `module`
- `/tmp/sow-toolkit-rowext validate-topdata` in `module`
- `/tmp/sow-toolkit-rowext build-topdata --force`, then inspected generated rows: `cls_spgn_acolyte.2da` now reaches row `59` / level `60`.
**Remaining Notes**
- `validate-topdata` still reports the pre-existing portrait lock warnings for `cotbl/cotbl_` and `lantern/lantern_`.
- `build-topdata --force` briefly rewrote three spellbook JSON files as a side effect; I reverted those, leaving only the intended YAML/docs changes in `module`.
Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/12
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
**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.