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
+15 -3
View File
@@ -21,6 +21,7 @@ var supportedPartCategories = []string{
"chest",
"foot",
"forearm",
"hand",
"leg",
"neck",
"pelvis",
@@ -35,6 +36,7 @@ var partDatasetToAssetCategory = map[string]string{
"chest": "chest",
"foot": "foot",
"forearm": "forearm",
"hand": "hand",
"legs": "leg",
"neck": "neck",
"pelvis": "pelvis",
@@ -390,6 +392,18 @@ func applyPartOverrides(sourceDir string, collected []nativeCollectedDataset) ([
return applyPartOverridesWithConfig(sourceDir, collected, project.PartsRowsConfig{})
}
// partsRowsConfigForProject returns the PartsRows policy of the single configured
// parts_rows autogen consumer, or the zero value if none. Project validation
// guarantees at most one parts_rows consumer, so the first match is canonical.
func partsRowsConfigForProject(p *project.Project) project.PartsRowsConfig {
for _, c := range p.Config.Autogen.Consumers {
if strings.TrimSpace(c.Mode) == "parts_rows" {
return c.PartsRows
}
}
return project.PartsRowsConfig{}
}
func applyPartOverridesWithConfig(sourceDir string, collected []nativeCollectedDataset, cfg project.PartsRowsConfig) ([]nativeCollectedDataset, error) {
result := make([]nativeCollectedDataset, len(collected))
copy(result, collected)
@@ -430,9 +444,7 @@ func applyPartOverridesWithConfig(sourceDir string, collected []nativeCollectedD
}
row, ok := rowByID[rowID]
if !ok {
row = createDefaultPartRowForDataset(rowID, dataset.Dataset.Name, dataset.Columns, cfg)
rows = append(rows, row)
rowByID[rowID] = row
return nil, fmt.Errorf("parts/%s override %d targets row id %d which is neither a baseline row nor a discovered model row; author the row in data/parts/%s.json instead", category, index, rowID, category)
}
for field, value := range override {
if field == "id" {