feat(topdata): parts autogen from CDN part.yml channel (Phase 2) (#25)
test / test (push) Successful in 1m26s
build-binaries / build-binaries (push) Successful in 2m12s
build-image / publish (push) Successful in 38s

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>
This commit was merged in pull request #25.
This commit is contained in:
2026-06-26 12:12:06 +00:00
committed by archvillainette
parent 332a24fd3e
commit 06e5893734
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 {