feat(topdata): parts autogen from CDN part.yml channel
build-binaries / build-binaries (pull_request) Successful in 2m15s
test-image / build-image (pull_request) Successful in 37s
test / test (pull_request) Successful in 1m24s

Phase 2 of the parts-row autogen design: Crucible 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).

- generalize resolveCDNChannelManifest: derive manifest basename + provenance
  label from config; drop hardcoded assets/vfxs.yml so part.yml resolves too
- add filterPartsCDNChannelEntries + mode dispatch implementing the inventory
  contract (restype mdl under part/<cat>/, trailing-digit row id, dedup
  l/r/race/gender variants, sort by source, zero accepted rows = hard fail)
- support hand category + parts/hand dataset mapping (12 datasets total)
- native pipeline: single augment -> normalize -> override sequence with the
  configured parts_rows policy; overrides may update but never synthesize a row
- reject multiple parts_rows consumers in project validation
- tests: parts filter contract, category coverage, offline basename, orphan
  override reject, and a parity guard proving a bare CDN-only parts build

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 13:18:20 +02:00
co-authored by Claude Opus 4.8
parent 332a24fd3e
commit 2586b9193e
9 changed files with 363 additions and 21 deletions
+51
View File
@@ -0,0 +1,51 @@
package topdata
import (
"os"
"path/filepath"
"testing"
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)
func partsDatasetWithIDs(name string, ids []int) nativeCollectedDataset {
rows := make([]map[string]any, 0, len(ids))
for _, id := range ids {
rows = append(rows, map[string]any{"id": id, "COSTMODIFIER": "0", "ACBONUS": "0.00"})
}
return nativeCollectedDataset{
Dataset: nativeDataset{Name: name, Kind: nativeDatasetBase},
Columns: []string{"COSTMODIFIER", "ACBONUS"},
Rows: rows,
}
}
func TestApplyPartOverridesRejectsOrphan(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "data", "parts", "overrides"), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(dir, "data", "parts", "overrides", "belt.json"),
`{"overrides": [{"id": 999, "COSTMODIFIER": "5"}]}`)
collected := []nativeCollectedDataset{partsDatasetWithIDs("parts/belt", []int{17})}
if _, err := applyPartOverridesWithConfig(dir, collected, project.PartsRowsConfig{}); err == nil {
t.Fatal("expected orphan override to fail, not synthesize a row")
}
}
func TestApplyPartOverridesUpdatesDiscoveredRow(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "data", "parts", "overrides"), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(dir, "data", "parts", "overrides", "belt.json"),
`{"overrides": [{"id": 17, "COSTMODIFIER": "5"}]}`)
collected := []nativeCollectedDataset{partsDatasetWithIDs("parts/belt", []int{17})}
out, err := applyPartOverridesWithConfig(dir, collected, project.PartsRowsConfig{})
if err != nil {
t.Fatalf("override: %v", err)
}
if got := out[0].Rows[0]["COSTMODIFIER"]; got != "5" {
t.Fatalf("row 17 COSTMODIFIER want 5, got %v", got)
}
}