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
+9
View File
@@ -1581,6 +1581,15 @@ func validateAutogenConfig(cfg AutogenConfig) []error {
func validateAutogenConsumerSources(consumers []AutogenConsumerConfig) []error {
var failures []error
partsRows := 0
for _, c := range consumers {
if strings.TrimSpace(c.Mode) == "parts_rows" {
partsRows++
}
}
if partsRows > 1 {
failures = append(failures, fmt.Errorf("at most one autogen consumer may use mode parts_rows; found %d (override precedence would be ambiguous)", partsRows))
}
for _, c := range consumers {
if strings.TrimSpace(c.Source.Kind) != "cdn_channel" {
continue
+14
View File
@@ -1864,6 +1864,20 @@ func TestValidateAutogenConsumerSourcesCDNChannel(t *testing.T) {
}
}
func TestValidateRejectsMultiplePartsRowsConsumers(t *testing.T) {
consumers := []AutogenConsumerConfig{
{ID: "parts-a", Mode: "parts_rows"},
{ID: "parts-b", Mode: "parts_rows"},
}
if failures := validateAutogenConsumerSources(consumers); len(failures) == 0 {
t.Fatal("expected error for two parts_rows consumers")
}
single := []AutogenConsumerConfig{{ID: "parts", Mode: "parts_rows"}}
if failures := validateAutogenConsumerSources(single); len(failures) != 0 {
t.Fatalf("single parts_rows consumer must validate, got %v", failures)
}
}
func loadAndValidate(root string) error {
proj, err := Load(root)
if err != nil {