Fold check-ascii-mdl, fix-mdl-model-names, check-duplicate-names, and
clean-duplicate-names out of sow-assets-manifest into `crucible assets`
(seven commands total), via a shared internal/assets/mdl port of
mdl-name-lib.sh + mdl-scan.awk. Consumer keeps the awk libs for import.sh's
hot-path gate; moving import.sh into `crucible depot` is a decomposed
follow-up spec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec for folding the NWN:EE model-compile, texture-convert, and
texture-upscale tools out of the Python toolkit into a self-contained
`crucible assets` builder. Arguments only, no config, in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweep prints `probed N/M` every 1000 blobs, but every caller passed `io.Discard`, so a full ~71k-blob verify ran ~56 minutes with zero output and looked hung in CI (sow-assets-manifest run 245).
`go test ./internal/depot` passes; verified live against the cdn (progress lines now appear on stderr).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Reviewed-on: #35
Co-authored-by: gitea-bot <gitea-bot@noreply.git.westgate.pw>
Co-committed-by: gitea-bot <gitea-bot@noreply.git.westgate.pw>
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>
`classFeatExpansionCanUseFeat` excludes `ALLCLASSESCANUSE=0` feats from class feat table masterfeat expansion. When such a feat also has no `MinLevelClass`, no class can ever obtain it — a data bug that previously failed silently (Spell Focus vanishing from the wizard table, Favored Enemy from ranger, proficiencies from commoner).
New `validateClassFeatMasterfeatAccessibility` warns for exactly that case; exclusions with an explicit `MinLevelClass` (deliberate class restrictions, e.g. fighter weapon specialization) and `ALLCLASSESCANUSE=1` feats stay silent. No keys are hardcoded — everything is derived from the datasets.
Verified: new test + full `go test ./internal/topdata/` pass; against live sow-topdata data it flagged 36 real drops (all fixed by the companion sow-topdata PR, after which it reports 0).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #31
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
## Summary
- skip overwriting existing `.are.json` files when only the root GFF `Version` differs
- keep real extraction changes writing normally
- add regression coverage for version-only area extraction
## Test Plan
- `nix develop --command make check`
- sample extraction in a clean temp `sow-module` clone using the patched `crucible` binary
Reviewed-on: #28
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
Phase 2 of the parts-row autogen design — Crucible (sow-tools) side only.
Resolves parts 2DA rows from the immutable \`part.yml\` published with the asset HAK release, over the anonymous Bunny CDN channel, with the parts consumer **required** (no fail-open).
## Changes
- **Resolver (T3):** generalize \`resolveCDNChannelManifest\` — derive manifest basename + provenance label from config; drop hardcoded \`assets/vfxs.yml\` so \`part.yml\` resolves through the same path. VFX behavior unchanged.
- **Parts filter (T4):** \`filterCDNChannelEntries\` dispatches on consumer mode; new \`filterPartsCDNChannelEntries\` implements the inventory contract — \`restype: mdl\` under \`part/<supported-cat>/\`, row id from trailing digits, dedup l/r/race/gender variants by (category,rowID), sort by source, **zero accepted rows = hard fail**, reject row id 0 / non-numeric / malformed / no-assets.
- **Category (T5):** add \`hand\` + \`parts/hand\` mapping (12 datasets; \`leg→legs\` already present).
- **Pipeline (T6):** native build runs one explicit augment → normalize → override sequence under the configured \`parts_rows\` policy; overrides may update an existing/discovered row but **never synthesize** one (orphan override now errors).
- **Validation (T7):** reject more than one \`parts_rows\` consumer.
## Tests
9 new tests: parts filter contract, 12-dataset coverage, offline manifest-basename, orphan-override reject + discovered-row update, and a parity guard proving a bare CDN-only parts build needs no wrapper/token/NWN_ROOT/checkout.
Gate green: \`go test ./internal/topdata/... ./internal/project/...\` + full \`go build ./... && go test ./...\`.
## Scope / ordering
Phase 2 is inert until **Phase 1** (sow-assets-manifest publishes \`part.yml\`) ships and the operational **Gate** (release publish/promote) runs, then **Phase 3** enables the sow-topdata consumer. Do not enable the consumer against a channel whose current release predates \`part.yml\`.
Plan: \`sow-topdata/docs/superpowers/plans/2026-06-25-topdata-parts-autogen-channel.md\`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #25
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
It vendors crucible.sh/.ps1 for off-Nix local dev and now carries the
drift-check workflow, so sync-wrappers should open update PRs to it too.
Requires: grant the gitea-bot org token write access to
ShadowsOverWestgate/sow-assets-manifest (per the header note in this file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #20
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
Auto-loads the repo's own flake devshell on `cd` via direnv. No-op for non-Nix users (direnv simply isn't installed). `.direnv/` eval cache gitignored.
Part of dropping the redundant root migration/ flake aggregator (its non-git path: input copied the whole tree and broke on the local postgres data dir). Each repo's own git-scoped flake is the source of truth now.
Co-authored-by: archvillainette <zoelynne.victoria@gmail.com>
Reviewed-on: #19
`crucible module build` runs the full pipeline (module + HAK producer +
topdata). A module-only project consumes prebuilt HAKs and has no
`paths.assets`, so the HAK stage crashed on the unset assets dir: the
music-credits walk hit `filepath.WalkDir("")` (`lstat : no such file or
directory`) and asset collection hit `filepath.Rel(root, "")`.
Guard both on an unset `AssetsDir()`: an absent assets tree simply means
no authored credits and no asset files to collect, so the HAK stage
no-ops cleanly while topdata generated-2DA assets still build. Honors
spec Decision 3 (canonical `crucible module build`, no `build-module`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #17
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>
## What
Two correctness fixes surfaced in review of the direct-depot HAK artifact work.
### Reject resref+type collisions in `chunksFromManifest`
A manifest can name two distinct source paths that collapse to the same
`resref+type` (e.g. `creature/foo.tga` and `placeable/foo.tga` — resref is the
lowercase basename minus extension). The ERF writer keys resources on
`Name:Type`, so the second silently shadowed the first: an asset would vanish
from the packed HAK with no error.
`chunksFromManifest` now runs `ensureUniqueChunkResources` per chunk and fails
the build on a duplicate. This guards **both** build paths — the legacy
`--source-manifest` flow and the direct content-addressed flow
(`chunksFromSourceManifest` → `chunksFromManifest`).
### Document erf post-write hash coupling
`writeResourceData` streams the source into the output while hashing, so
size/SHA mismatches are only detected *after* the bytes are written. A non-nil
return therefore means the writer holds partial, unverified output and the
caller must discard it. Added a comment making that contract explicit;
`writeHAKArchive` already honours it (writes to a temp file, removes on any
Write error, never renames a bad archive into place).
## Tests
- `TestChunksFromManifestRejectsResrefCollision`: collision → error, distinct
resrefs → clean. Asserts only error presence/absence — silent asset loss is
the contract, not any specific wording.
- `go vet ./internal/erf/ ./internal/pipeline/` clean.
- `go test ./internal/erf/ ./internal/pipeline/` green.
## Follow-up
A new crucible release must be cut after this merges so the guard ships in the
binary `sow-assets-manifest` pins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #8
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
RC#1 — build wrote 2-space instead of .editorconfig's 4-space
- editorconfig_format.go: the glob translator escaped {/} as literals, so [*.{json,jsonc}]→indent_size=4 never matched any file and formatting fell back to [*]→2. Implemented real editorconfig brace expansion — {a,b,c} alternation and {n..m} numeric ranges. Nothing is hardcoded; saveLockfile already read .editorconfig, it just got the wrong section.
RC#2 — validate wrote a lockfile (it must be read-only)
- The three registry collectors persisted lockfiles as a side effect of collection; ValidateProject calls collection outside its snapshot guard, so the write leaked. Threaded a persistLocks bool through collectGeneratedRegistryDatasets and the damagetypes/itemprops/racialtypes collectors. Only buildNativeUnchecked (the real build allocator) passes true; validate, discovery, packaging queries, and wiki discovery pass false.
Tests added: editorconfig_glob_test.go (brace match + 4-space via brace glob), registry_readonly_test.go (read-only writes nothing; persist writes allocations).
Reviewed-on: #5
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
Adds a manual release workflow and ensures images are only published on v* tags
Reviewed-on: #1
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>