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
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@@ -194,6 +195,35 @@ func TestResolveCDNChannelOfflineOverride(t *testing.T) {
}
}
// TestResolveCDNChannelOfflineUsesManifestBasename proves the resolver derives
// the offline checkout filename from manifest_path's basename (part.yml here),
// not a hardcoded assets/vfxs.yml, and routes parts_rows through the parts filter.
func TestResolveCDNChannelOfflineUsesManifestBasename(t *testing.T) {
root := testProjectRoot(t)
dir := filepath.Join(root, "manifest-checkout")
if err := os.MkdirAll(filepath.Join(dir, "assets"), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(dir, "assets", "part.yml"),
"assets:\n - {path: part/belt/m/pfa0_belt017.mdl, restype: mdl}\n")
t.Setenv("SOW_PART_MANIFEST", dir)
c := projectPartsConsumer()
c.Source = project.AutogenSourceConfig{
Kind: "cdn_channel",
ManifestPath: "releases/haks/{tag}/part.yml",
OfflineOverrideEnv: "SOW_PART_MANIFEST",
}
m, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil)
if err != nil {
t.Fatalf("resolve: %v", err)
}
if len(m.Entries) != 1 || m.Entries[0].RowID != 17 || m.Entries[0].Group != "belt" {
t.Fatalf("unexpected entries: %+v", m.Entries)
}
}
func errorIsUnavailable(err error) bool {
return errors.Is(err, errAutogenManifestUnavailable)
}