Revert "chore(assets): regen character manifest — was stale since the wardrobe landed"

This reverts commit 04241989b. The premise was wrong: the manifest is not
stale, it is AHEAD of its generator.

Both versions list the same 24 garments. What the regen dropped is the
`style` and `logo_capable` fields that T-1089 added to the manifest schema
but never taught `generate-character-manifest` to emit. So the generator
faithfully reproduces a manifest that is missing half its schema, and
`test_garment_engine_t1089.gd` catches it — which is how this was found, on
the push gate rather than in review.

`make manifest` is therefore a trap for anyone who runs it: it silently
reverts the T-1089 extension. Filed as T-1287.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 17:55:37 +02:00
co-authored by Claude Opus 5
parent 04241989b0
commit 59d644cc49
4 changed files with 347 additions and 54 deletions
+35
View File
@@ -120,3 +120,38 @@ worktree refusals — none of them performed. Every assertion was mutation-check
Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', NULL, '2026-09-02 14:54:35', '2026-09-02 14:54:35.578', '2026-09-02 14:54:35.578', NULL, 'e0bd8f1d16b2dc7c7d1b8b1511633cc3', 2) ON CONFLICT(hash) DO NOTHING; Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', NULL, '2026-09-02 14:54:35', '2026-09-02 14:54:35.578', '2026-09-02 14:54:35.578', NULL, 'e0bd8f1d16b2dc7c7d1b8b1511633cc3', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G64ASTXBBHMWMBQSVPM9MK1W', 'status', 'backlog', 'done', NULL, '2026-09-02 14:54:37', '2026-09-02 14:54:37.762', '2026-09-02 14:54:37.762', NULL, '5f9240c61aa2bf03924e4edaf4dd6f7c', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G64ASTXBBHMWMBQSVPM9MK1W', 'status', 'backlog', 'done', NULL, '2026-09-02 14:54:37', '2026-09-02 14:54:37.762', '2026-09-02 14:54:37.762', NULL, '5f9240c61aa2bf03924e4edaf4dd6f7c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65N2W393K7A4B8JA8FGP22R', 'description', NULL, '`generate-character-manifest` (now `reach generate character-manifest`) emits
only `{"slot": ...}` per clothing entry. The committed
`client/assets/characters/manifest.json` also carries `style` and
`logo_capable`, added to the schema by T-1089 — the generator was never taught
to produce them.
So the artifact is AHEAD of its generator, and `make manifest` silently reverts
the T-1089 extension. Confirmed on 2026-09-02: a regen produced the same 24
garments with those two fields dropped, and `test_garment_engine_t1089.gd >
test_manifest_has_tshirt_with_style_and_logo_flag` failed on the push gate.
Reverted in 6024045fc.
This is the exact failure mode `.claude/rules/asset-pipeline.md` exists to
prevent, one directory over: an artifact that cannot be reproduced from its
source. The manifest is not covered by that rule (no meta stamp, not
systems.db), which is why it drifted unnoticed.
WHAT TO DECIDE FIRST — where do `style` and `logo_capable` come from? They are
not derivable from a filename. Either:
(a) a sidecar the generator reads (e.g. the clothing `coverage.json` already
beside the assets, or per-garment metadata), or
(b) the manifest is genuinely hand-maintained, in which case the generator
should MERGE rather than overwrite, and say so.
(a) is the asset-pipeline-consistent answer. (b) needs the generator to stop
claiming to be the source of truth.
ALSO: `test_garment_engine_t1089.gd:108` reports `Expecting ''modern'' but was
''modern''` — the strings print identically, so the assertion is comparing
something other than what it displays (String vs StringName, or a trailing
character). Worth a look while in there; a failure message that shows two
identical values costs whoever hits it real time.
Acceptance: `reach generate character-manifest` on a clean tree leaves
manifest.json byte-identical, and the T-1089 test still passes after it runs.', NULL, '2026-09-02 15:54:41', '2026-09-02 15:54:41.911', '2026-09-02 15:54:41.911', NULL, '1a36359b2986139da3157d2429351f74', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -1,3 +1,4 @@
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G63FEA51J4JKE3A7473D3QZ0', 'T-1284', '2026-09-02 10:50:23.145', '2026-09-02 10:50:23.145', NULL, '74113524e3a280bd06875305a547349a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G63FEA51J4JKE3A7473D3QZ0', 'T-1284', '2026-09-02 10:50:23.145', '2026-09-02 10:50:23.145', NULL, '74113524e3a280bd06875305a547349a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G642RKW8PNW7AP9BY9203SPG', 'T-1285', '2026-09-02 12:14:48.290', '2026-09-02 12:14:48.290', NULL, '020f491d1ae8fbde83d2aae0f8325011', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G642RKW8PNW7AP9BY9203SPG', 'T-1285', '2026-09-02 12:14:48.290', '2026-09-02 12:14:48.290', NULL, '020f491d1ae8fbde83d2aae0f8325011', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G64ASTXBBHMWMBQSVPM9MK1W', 'T-1286', '2026-09-02 12:49:55.434', '2026-09-02 12:49:55.434', NULL, '7c65442cc261176c9c472fdbd635cc91', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G64ASTXBBHMWMBQSVPM9MK1W', 'T-1286', '2026-09-02 12:49:55.434', '2026-09-02 12:49:55.434', NULL, '7c65442cc261176c9c472fdbd635cc91', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65N2W393K7A4B8JA8FGP22R', 'T-1287', '2026-09-02 15:54:39.514', '2026-09-02 15:54:39.514', NULL, 'de690eafa611669d722b0ffb850a1d7b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
+36
View File
@@ -229,3 +229,39 @@ worktree refusals — none of them performed. Every assertion was mutation-check
unrecognised verdict and blocked the push. unrecognised verdict and blocked the push.
Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', 'done', 'medium', NULL, NULL, 'D-263', '2026-09-02 12:49:55.434', '2026-09-02 14:54:37.762', NULL, '678c0d0578c2255438d2e51829d87a64', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', 'done', 'medium', NULL, NULL, 'D-263', '2026-09-02 12:49:55.434', '2026-09-02 14:54:37.762', NULL, '678c0d0578c2255438d2e51829d87a64', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65N2W393K7A4B8JA8FGP22R', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'make manifest silently drops the T-1089 garment schema fields', NULL, 'backlog', 'medium', NULL, 'client', NULL, '2026-09-02 15:54:39.514', '2026-09-02 15:54:39.514', NULL, 'ddb9c65aa4c4537924de591c4c017051', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65N2W393K7A4B8JA8FGP22R', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'make manifest silently drops the T-1089 garment schema fields', '`generate-character-manifest` (now `reach generate character-manifest`) emits
only `{"slot": ...}` per clothing entry. The committed
`client/assets/characters/manifest.json` also carries `style` and
`logo_capable`, added to the schema by T-1089 — the generator was never taught
to produce them.
So the artifact is AHEAD of its generator, and `make manifest` silently reverts
the T-1089 extension. Confirmed on 2026-09-02: a regen produced the same 24
garments with those two fields dropped, and `test_garment_engine_t1089.gd >
test_manifest_has_tshirt_with_style_and_logo_flag` failed on the push gate.
Reverted in 6024045fc.
This is the exact failure mode `.claude/rules/asset-pipeline.md` exists to
prevent, one directory over: an artifact that cannot be reproduced from its
source. The manifest is not covered by that rule (no meta stamp, not
systems.db), which is why it drifted unnoticed.
WHAT TO DECIDE FIRST — where do `style` and `logo_capable` come from? They are
not derivable from a filename. Either:
(a) a sidecar the generator reads (e.g. the clothing `coverage.json` already
beside the assets, or per-garment metadata), or
(b) the manifest is genuinely hand-maintained, in which case the generator
should MERGE rather than overwrite, and say so.
(a) is the asset-pipeline-consistent answer. (b) needs the generator to stop
claiming to be the source of truth.
ALSO: `test_garment_engine_t1089.gd:108` reports `Expecting ''modern'' but was
''modern''` — the strings print identically, so the assertion is comparing
something other than what it displays (String vs StringName, or a trailing
character). Worth a look while in there; a failure message that shows two
identical values costs whoever hits it real time.
Acceptance: `reach generate character-manifest` on a clean tree leaves
manifest.json byte-identical, and the T-1089 test still passes after it runs.', 'backlog', 'medium', NULL, 'client', NULL, '2026-09-02 15:54:39.514', '2026-09-02 15:54:41.911', NULL, '2f6393f44525977bfeba1200e8f79561', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
+275 -54
View File
@@ -46,33 +46,6 @@
"thick" "thick"
], ],
"clothing": { "clothing": {
"boots_modern": {
"slot": "feet"
},
"buttondown_modern": {
"slot": "torso"
},
"cargo_pants": {
"slot": "legs"
},
"hoodie_modern": {
"slot": "torso"
},
"jacket_modern": {
"slot": "torso"
},
"jeans_modern": {
"slot": "torso"
},
"joggers_modern": {
"slot": "torso"
},
"pants_formal": {
"slot": "legs"
},
"parka_thrds": {
"slot": "torso"
},
"peasant_pants": { "peasant_pants": {
"slot": "legs" "slot": "legs"
}, },
@@ -82,41 +55,289 @@
"peasant_tunic": { "peasant_tunic": {
"slot": "torso" "slot": "torso"
}, },
"shoes_formal": { "tshirt_modern": {
"slot": "feet" "slot": "torso",
"style": "modern",
"logo_capable": true
},
"hoodie_modern": {
"slot": "torso",
"style": "modern",
"logo_capable": true
},
"buttondown_modern": {
"slot": "torso",
"style": "modern",
"logo_capable": false
}, },
"shorts_modern": { "shorts_modern": {
"slot": "legs" "slot": "legs",
"style": "modern",
"logo_capable": false
}, },
"slides": { "jeans_modern": {
"slot": "torso" "slot": "legs",
"style": "modern",
"logo_capable": false
}, },
"sneakers_modern": { "pants_formal": {
"slot": "torso" "slot": "legs",
"style": "formal",
"logo_capable": false
},
"jacket_modern": {
"slot": "torso",
"style": "modern",
"logo_capable": true
}, },
"suit_jacket_black": { "suit_jacket_black": {
"slot": "torso" "slot": "torso",
}, "style": "formal",
"sweater_modern": { "logo_capable": false,
"slot": "torso" "regions": {
}, "R": "lapels_collar",
"swim_trunks": { "G": "jacket_body",
"slot": "torso" "B": "sleeves",
}, "A": "shirt_triangle"
"swimsuit_onepiece": { },
"slot": "torso" "default_tints": [
}, [
"tank_top": { 0.13,
"slot": "torso" 0.13,
}, 0.16,
"track_jacket": { 1
"slot": "torso" ],
}, [
"tshirt_modern": { 0.09,
"slot": "torso" 0.09,
0.11,
1
],
[
0.09,
0.09,
0.11,
1
],
[
0.93,
0.93,
0.96,
1
]
]
}, },
"uniform_utility": { "uniform_utility": {
"slot": "torso" "slot": "full_body",
"style": "utility",
"logo_capable": true,
"default_tints": [
"#3b474f",
"#6b8280",
"#d18c2e",
"#d9dee0"
]
},
"tank_top": {
"slot": "torso",
"style": "modern",
"logo_capable": true,
"regions": {
"R": "collar_band",
"G": "body",
"B": "armhole_trim"
},
"default_tints": [
"#f5f2eb",
"#e8693b",
"#f5f2eb",
"#ffffff"
]
},
"sweater_modern": {
"slot": "torso",
"style": "modern",
"logo_capable": false,
"regions": {
"R": "collar",
"G": "body",
"B": "cuffs_hem"
},
"default_tints": [
"#946b4f",
"#b8916e",
"#946b4f",
"#ffffff"
]
},
"track_jacket": {
"slot": "torso",
"style": "sport",
"logo_capable": true,
"multi_region": true,
"regions": {
"R": "stand_collar",
"G": "body",
"B": "sleeve_stripes",
"A": "cuffs_hem_band"
},
"default_tints": [
"#ebedf2",
"#294d9e",
"#f0f2f7",
"#1a2652"
]
},
"joggers_modern": {
"slot": "legs",
"style": "modern",
"logo_capable": false
},
"cargo_pants": {
"slot": "legs",
"style": "modern",
"logo_capable": false
},
"swim_trunks": {
"slot": "legs",
"style": "modern",
"logo_capable": false
},
"swimsuit_onepiece": {
"slot": "full_body",
"style": "modern",
"logo_capable": false,
"regions": {
"R": "straps_trim",
"G": "body",
"B": "side_blocks"
},
"default_tints": [
"#f2f0e8",
"#e34a33",
"#1f7a8c",
"#ffffff"
]
},
"sneakers_modern": {
"slot": "feet",
"style": "modern",
"logo_capable": false,
"multi_region": true,
"regions": {
"R": "sole",
"G": "upper",
"B": "laces_trim"
},
"default_tints": [
"#bdc0c4",
"#ebebee",
"#e1e1e6"
]
},
"shoes_formal": {
"slot": "feet",
"style": "formal",
"logo_capable": false,
"regions": {
"R": "sole",
"G": "upper",
"B": "toe_cap"
},
"default_tints": [
[
0.1,
0.1,
0.11,
1
],
[
0.07,
0.07,
0.08,
1
],
[
0.07,
0.07,
0.08,
1
],
[
1,
1,
1,
1
]
]
},
"boots_modern": {
"slot": "feet",
"style": "modern",
"logo_capable": false,
"regions": {
"R": "sole",
"G": "upper_shaft",
"B": "laces_eyelets"
},
"default_tints": [
"#292624",
"#704a2e",
"#382e26",
"#ffffff"
]
},
"slides": {
"slot": "feet",
"style": "modern",
"logo_capable": false,
"regions": {
"R": "sole",
"G": "strap"
},
"default_tints": [
[
0.82,
0.83,
0.85,
1
],
[
0.93,
0.35,
0.2,
1
],
[
1,
1,
1,
1
],
[
1,
1,
1,
1
]
]
},
"parka_thrds": {
"slot": "torso",
"style": "modern",
"logo_capable": true,
"regions": {
"R": "hood_roll_collar",
"G": "body",
"B": "sleeves",
"A": "pocket_quilt_trim"
},
"default_tints": [
"#384257",
"#4d5973",
"#455269",
"#788599"
]
} }
}, },
"accessories": [] "accessories": []