diff --git a/.pql/changelog/ticket_deps/2026-08.sql b/.pql/changelog/ticket_deps/2026-08.sql index e8e0be5df..ff63ad705 100644 --- a/.pql/changelog/ticket_deps/2026-08.sql +++ b/.pql/changelog/ticket_deps/2026-08.sql @@ -19,3 +19,5 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FBZENXG6N9BWR1SK9VNMVC', '06G5FC2VMM5EVM69YBK28MYG1W', '2026-08-31 11:59:45.548', '2026-08-31 11:59:45.548', NULL, 'b91d4331a6c78991483d47b2018636ec', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FC15HDP9JRX3S4J0VA33QG', '06G5FC3YK5NDFABTF6P1FQSR28', '2026-08-31 11:59:46.100', '2026-08-31 11:59:46.100', NULL, 'a28d1af729d30064f1b919ac0aedaaf9', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FC0EGNR11ACT6B72AJS98R', '06G5FC27A7ZV3ZEDYDA1R11PNC', '2026-08-31 11:59:48.345', '2026-08-31 11:59:48.345', NULL, '70cc43ac2b1719a348d0668769ef1399', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FZBGREDJ12W3Q9GNB8C7PC', '06G5FZCVZXH6M00C57EXMZD0SG', '2026-08-31 13:24:08.442', '2026-08-31 13:24:08.442', NULL, 'e9e9cdde0d2dff4f40df155ed2aafea3', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FZBGREDJ12W3Q9GNB8C7PC', '06G5FZDN9YBP0MZ021N3TJCFXM', '2026-08-31 13:24:11.142', '2026-08-31 13:24:11.142', NULL, 'a6d583edb124e48fd3cd118a08084346', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; diff --git a/.pql/changelog/ticket_history/2026-08.sql b/.pql/changelog/ticket_history/2026-08.sql index 66ea5016f..b125d8bfa 100644 --- a/.pql/changelog/ticket_history/2026-08.sql +++ b/.pql/changelog/ticket_history/2026-08.sql @@ -1718,3 +1718,77 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang 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 ('06G5FC27A7ZV3ZEDYDA1R11PNC', 'status', 'in_progress', 'done', NULL, '2026-08-31 13:03:17', '2026-08-31 13:03:17.404', '2026-08-31 13:03:17.404', NULL, '74dbc8733bcd60f59f5f0375f63c66a6', 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 ('06G5FC2VMM5EVM69YBK28MYG1W', 'status', 'in_progress', 'done', NULL, '2026-08-31 13:03:17', '2026-08-31 13:03:17.404', '2026-08-31 13:03:17.404', NULL, 'd8e19c53f5e59e1db8fc12fae8b11aa0', 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 ('06G5FC3YK5NDFABTF6P1FQSR28', 'status', 'in_progress', 'done', NULL, '2026-08-31 13:03:17', '2026-08-31 13:03:17.405', '2026-08-31 13:03:17.405', NULL, 'aa9af65081a7ac5d8f9387d75e8b746b', 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 ('06G1S39T92PW9JNBPWD0F4JFR0', 'status', 'in_progress', 'done', NULL, '2026-08-31 13:03:37', '2026-08-31 13:03:37.758', '2026-08-31 13:03:37.758', NULL, '4add26a63cd2288b917b107933eae2c7', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'status', 'backlog', 'in_progress', NULL, '2026-08-31 13:20:21', '2026-08-31 13:20:21.937', '2026-08-31 13:20:21.937', NULL, 'f3c75c5a57516c94b8d5fd3da1d8727c', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'description', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota.', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move.', NULL, '2026-08-31 13:23:30', '2026-08-31 13:23:30.519', '2026-08-31 13:23:30.519', NULL, 'a73c963b29692ac159c6edf31f2068c3', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'status', 'in_progress', 'in_progress', NULL, '2026-08-31 13:23:30', '2026-08-31 13:23:30.670', '2026-08-31 13:23:30.670', NULL, 'd33a60769660430f09abb549b0d3967f', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'description', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move.', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move. + +REFINED 2026-08-31 — four cross-cutting tasks filed; the per-domain port tickets are DELIBERATELY NOT filed yet. T-1271 the domain map (blocking design — every file assigned before any file moves), T-1272 rename the five hyphenated Python directories, T-1273 the Blender carve-out, T-1274 retire the ruff ignores the package makes unnecessary. T-1272 and T-1273 are blocked on T-1271; T-1274 runs LAST, since re-enabling E402 mid-move means fixing violations in files that are about to move again. The per-domain tickets get filed from the map rather than from the epic''s proposed list, because that list is missing at least one domain (PR/workflow — tea-comment, pr-watchlist-diff) and leaves 28 singleton-prefix files unassigned. Filing nine tickets now and renegotiating their boundaries mid-move is the failure this ordering avoids.', NULL, '2026-08-31 13:24:15', '2026-08-31 13:24:15.758', '2026-08-31 13:24:15.758', NULL, '294ccda19eacc02c793ea17356a01097', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'status', 'backlog', 'in_progress', NULL, '2026-08-31 13:24:23', '2026-08-31 13:24:23.248', '2026-08-31 13:24:23.248', NULL, '96eb1f74d0f67d887eefa0634cdd7199', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'description', 'The blocking design work for this epic, and the reason per-domain port tickets are not filed yet. Produce a single committed artefact — tooling/DOMAINS.md or similar — assigning EVERY one of the ~128 legacy .py files and ~33 extensionless executables to a domain, with its future module path and CLI verb. Until that exists, a per-domain ticket cannot state what it moves. What makes this real work rather than clerical: ~51 top-level files have an obvious home from their prefix (blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2), but 28 SINGLETON prefixes do not, and several are genuinely ambiguous. Worked examples to settle explicitly, since each sets a precedent: canvas_sources.py and generator_sources.py are REGISTRIES consumed by gates, not commands — do they become check-domain helpers, or core, or stay module-level data; schema_version.py is consumed by the DB importer and by a gate, so it is shared between two domains; tea-comment and pr-watchlist-diff are PR workflow, which is NOT in the epic''s proposed domain list at all, so either a workflow domain is added or they go to dev. Also decide: does become a domain, or do the 5 test-prefixed files stay as standalone scripts run by make (they are gate tests, not commands anyone types). Deliverable rule to apply throughout: a domain that has only a router and a service does not get empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. Acceptance: no file unassigned, every ambiguous case carrying a one-line reason, and the verb list readable as the future output of reach --help.', 'The blocking design work for this epic, and the reason per-domain port tickets are not filed yet. Produce a single committed artefact — tooling/DOMAINS.md or similar — assigning EVERY one of the ~128 legacy .py files and ~33 extensionless executables to a domain, with its future module path and CLI verb. Until that exists, a per-domain ticket cannot state what it moves. What makes this real work rather than clerical: ~51 top-level files have an obvious home from their prefix (blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2), but 28 SINGLETON prefixes do not, and several are genuinely ambiguous. Worked examples to settle explicitly, since each sets a precedent: canvas_sources.py and generator_sources.py are REGISTRIES consumed by gates, not commands — do they become check-domain helpers, or core, or stay module-level data; schema_version.py is consumed by the DB importer and by a gate, so it is shared between two domains; tea-comment and pr-watchlist-diff are PR workflow, which is NOT in the epic''s proposed domain list at all, so either a workflow domain is added or they go to dev. Also decide: does become a domain, or do the 5 test-prefixed files stay as standalone scripts run by make (they are gate tests, not commands anyone types). Deliverable rule to apply throughout: a domain that has only a router and a service does not get empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. Acceptance: no file unassigned, every ambiguous case carrying a one-line reason, and the verb list readable as the future output of reach --help. + +DONE 2026-08-31. tooling/DOMAINS.md committed: 15 domains, every file and executable assigned, judgment calls carrying their reasons. + +THREE CORRECTIONS FOUND BY COUNTING RATHER THAN READING, one of which reverses my own earlier survey. + +(1) THE BLENDER CARVE-OUT IS 35 FILES, NOT 13 — and the T-1250 description was RIGHT while my survey on this ticket was wrong. 13 blender_*.py sit at top level; 22 more are inside garment-fit/. My survey counted only the top level and told the epic it had overstated the number. It had not. +The consequence is not cosmetic. garment-fit/ is 23 files of which 22 are blender_author_* payloads, so it is a Blender payload directory wearing a domain''s name. `character` is therefore a MUCH smaller domain than directory sizes suggest, and T-1273''s carve-out is much larger. A per-domain ticket written from the directory listing would have been wrong about both. + +(2) THE "28 SINGLETON PREFIXES" WERE AN ARTEFACT OF MY OWN MEASUREMENT. Splitting filenames on the first token scattered coherent families: sculpt-star-map, tune-star-map-topology and generate-star-map* are ONE group (starmap), counted as three orphans. Counting families rather than prefixes, the genuinely ambiguous set is small and is now enumerated with reasons instead of being a number. + +(3) tooling/db/ IS MISNAMED and naming a domain after it would carry the misnomer forward. It holds the audio/image/Trellis connectors and wiki_sync.py; the actual database work is in economy-db/. Its contents split between a new `assets` domain (the tower-of-joy connectors) and `wiki`. + +JUDGMENT CALLS SETTLED, each with a recorded reason since each sets a precedent: +- Registries (canvas_sources.py, generator_sources.py) stay module-level data, NOT verbs. Nothing types them; putting them in reach --help would answer a question nobody has. +- schema_version.py goes to `db` where it is DEFINED, and `check` imports it. Shared but not equally owned — that cross-domain import is what a service layer is for. +- test_*.py do NOT become a domain. They are gate tests run by make, and `reach test ...` would imply a runner that does not exist. +- pql-migrate/ is PROVENANCE, not tooling. Moves to tooling/archive/, excluded from discovery. Deleting destroys migration provenance; keeping it importable implies it still runs. +- `pr` is a domain the epic did not list (tea-comment, pr-watchlist-diff, pql-board-html). Folding them into `dev` would make dev the drawer everything ambiguous goes into — the same rot D-263 warns about for core/. +- `character`, not D-263''s `garment`: the files cover bodies, hair, GLB and Quaternius as well as clothing. +- `atlas` IS OVERLOADED ACROSS THREE PLACES and must not be merged: top-level atlas-* executables (map data), planet-gen/atlas_*.py (terrain quality analysis), economy-db/atlas.py (systems.db index tables). Three concerns sharing a noun. Each stays with its owner. +- economy-db/errors.py predates core/errors.py and is not the same thing. Reconcile explicitly when db is ported; a silent merge or a second ReachError with different semantics is the risk. + +PER-DOMAIN PORT TICKETS can now be filed from this map rather than guessed. Recommended order, cheapest and most-verified first: check (4 remaining gates, already has the reference implementation and a parity test to copy), validate, godot, visual, then the large ones — planet, db, character, wiki, assets, atlas, starmap, generate, dev, pr.', NULL, '2026-08-31 13:26:38', '2026-08-31 13:26:38.138', '2026-08-31 13:26:38.138', NULL, '1622adb66a6f201827a11f7fb9f95c62', 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 ('06G5FZDN9YBP0MZ021N3TJCFXM', 'description', 'The one legitimate exception to the no-tooling-outside-reach rule, and it needs to be written down as such rather than discovered later as an inconsistency. The blender_*.py files (14 at top level by the survey, not the 35 the epic states — verify the true count against subdirectories before moving) run under Blender''s BUNDLED Python interpreter, which has no access to the repo venv and cannot import tooling.core. They are payloads, not modules. Deliverables: move them to tooling/scripts/blender/ as a clearly-marked payload directory; keep tooling/blender as the bash wrapper that invokes Blender with a payload; front the whole thing with reach so the user-facing form is a normal verb. The payload directory is EXCLUDED from the conformance test''s package scope — those files legitimately use bare print() and cannot carry @command, and holding them to a contract they physically cannot satisfy would either fail the gate forever or force the contract to be weakened for everyone. Record the exclusion in the test with the reason inline, so the next person does not read it as an oversight and ''fix'' it. This carve-out is also one of the two exceptions T-1255 must name in the CLAUDE.md rule; the other is the Rust crates.', 'The one legitimate exception to the no-tooling-outside-reach rule, and it needs to be written down as such rather than discovered later as an inconsistency. The blender_*.py files (14 at top level by the survey, not the 35 the epic states — verify the true count against subdirectories before moving) run under Blender''s BUNDLED Python interpreter, which has no access to the repo venv and cannot import tooling.core. They are payloads, not modules. Deliverables: move them to tooling/scripts/blender/ as a clearly-marked payload directory; keep tooling/blender as the bash wrapper that invokes Blender with a payload; front the whole thing with reach so the user-facing form is a normal verb. The payload directory is EXCLUDED from the conformance test''s package scope — those files legitimately use bare print() and cannot carry @command, and holding them to a contract they physically cannot satisfy would either fail the gate forever or force the contract to be weakened for everyone. Record the exclusion in the test with the reason inline, so the next person does not read it as an oversight and ''fix'' it. This carve-out is also one of the two exceptions T-1255 must name in the CLAUDE.md rule; the other is the Rust crates. + +CORRECTED 2026-08-31 by T-1271: the carve-out is 35 files, and the count in this ticket (which said 14 at top level, doubting the epic''s 35) was WRONG. 13 blender_*.py are at top level and 22 more are inside tooling/garment-fit/, which is a Blender payload directory wearing a domain''s name — 22 of its 23 files are blender_author_*. The epic''s figure of 35 was right. Practical consequence: garment-fit/ mostly moves to tooling/scripts/blender/ rather than becoming a character domain, so this ticket and the character port must be sequenced together or they will fight over the same directory.', NULL, '2026-08-31 13:26:41', '2026-08-31 13:26:41.808', '2026-08-31 13:26:41.808', NULL, '0e257fc4279ea88af717eabc09a6bcf9', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-08.sql b/.pql/changelog/ticket_idmap/2026-08.sql index d5a242950..9191d35c0 100644 --- a/.pql/changelog/ticket_idmap/2026-08.sql +++ b/.pql/changelog/ticket_idmap/2026-08.sql @@ -30,3 +30,7 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FC27A7ZV3ZEDYDA1R11PNC', 'T-1268', '2026-08-31 11:59:25.521', '2026-08-31 11:59:25.521', NULL, 'efca9b14f286a94fd396dfd50f3c589b', 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 ('06G5FC2VMM5EVM69YBK28MYG1W', 'T-1269', '2026-08-31 11:59:30.727', '2026-08-31 11:59:30.727', NULL, '6c099247b4909cfd075343bc178594f4', 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 ('06G5FC3YK5NDFABTF6P1FQSR28', 'T-1270', '2026-08-31 11:59:39.673', '2026-08-31 11:59:39.673', NULL, 'db8616bb91647fcdf1a46fb8f9747be5', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'T-1271', '2026-08-31 13:23:42.404', '2026-08-31 13:23:42.404', NULL, 'c7617f2b683540089fdc38b0feaf28f3', 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 ('06G5FZCVZXH6M00C57EXMZD0SG', 'T-1272', '2026-08-31 13:23:53.471', '2026-08-31 13:23:53.471', NULL, 'f193e26d0e4314a88014a2dc23a6e335', 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 ('06G5FZDN9YBP0MZ021N3TJCFXM', 'T-1273', '2026-08-31 13:23:59.951', '2026-08-31 13:23:59.951', NULL, 'b9643fcc4583630d76552c35dc0ed48d', 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 ('06G5FZEM6JT5KKVE1RZ68DENSC', 'T-1274', '2026-08-31 13:24:07.865', '2026-08-31 13:24:07.865', NULL, 'dfd2948f52c1342b42f88097c0c43274', 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; diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql index bc77cdee8..70068d8d8 100644 --- a/.pql/changelog/tickets/2026-08.sql +++ b/.pql/changelog/tickets/2026-08.sql @@ -1988,3 +1988,85 @@ Both name the file, the line and the reason. The suite also fails if NO commands are found, which would otherwise let assertions 3 and 4 pass vacuously. make test-tooling: PASS end to end with all three reach tests wired in.', 'done', 'high', NULL, NULL, 'D-263', '2026-08-31 11:59:39.673', '2026-08-31 13:03:17.405', NULL, '409ed433f3fd906ac24ca5a764108690', 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 ('06G1S39T92PW9JNBPWD0F4JFR0', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E2 — Decorators: shared logging, robust error handling, failures that teach', 'The requirement with the real teeth: every non-zero exit prints the command that would fix it, and still exits non-zero. Cross-cutting concerns are decorators, never call-site discipline. Pieces that become tickets: (1) core/errors.py — ReachError(message, fix=...) plus the @handle_errors decorator that renders message and remedy to stderr and PRESERVES the exit code; unrecognised exceptions still exit non-zero with the traceback behind --verbose; (2) core/logging.py — the shared logger plus @logged, one structured line per invocation (command, args, duration, outcome) to STDERR never stdout, quiet by default so hooks are not spammed; (3) the composed @command decorator so no command can carry one and not the other; (4) core/console.py as the single output path; (5) closed-set enumeration — whenever the accepted set is known, print it, which is the specific gap measured in pql (invalid status without naming the six valid ones); (6) --no-input plus TTY gating, hooks pass it unconditionally, because tea prompts crash in Claude Code with no TTY; (7) the conformance test: every registered command has help at its own level, every command carries @command, no typer/click outside main.py and router.py, no bare print outside core/console.py, no pydantic reachable from main.py. Done when the contract is a test, not a style guide. + +AMENDED 2026-08-20 — the streaming model (D-263 amendment, E9/T-1264) touches two things here. (1) The @logged decorator and core/console.py are now the SAME channel, not two: logged emits its one-line-per-invocation record as a JSONL event through console, rather than through a separate logging sink. Two output paths would drift, and console is already specified as the single one. (2) The @handle_errors decorator gains a responsibility — it renders the FINAL VERDICT, once, last, after any stream. That is what keeps streaming additive to the failure contract instead of dissolving it: a remedy emitted mid-stream at line 400 of 900 is technically printed and practically invisible, so the terminal event carrying outcome, exit code and remedy is the decorator job. Nothing else in this epic changes. + +Refined 2026-08-31 into six tasks: T-1265 core/errors.py and @handle_errors, T-1266 core/logging.py and @logged, T-1267 the composed @command decorator, T-1268 global --verbose and --no-input, T-1269 closed-set enumeration, T-1270 the conformance test. Item (4) of the description above — core/console.py as the single output path — is ALREADY DONE, delivered by T-1258 and reshaped as the JSONL event emitter by the streaming amendment; no task for it. Sequencing: T-1265 and T-1266 are unblocked and independent; T-1267 needs both; T-1268 needs T-1266; T-1269 needs T-1265; T-1270 needs T-1267. Ordering note recorded during refinement: this epic must land BEFORE T-1250 moves the tree. The check router today hand-rolls console.verdict plus raise typer.Exit at each exit — exactly the call-site discipline these decorators replace — and porting ~120 commands with that pattern means writing it 120 times and then unwinding it.', 'done', 'high', NULL, NULL, 'D-263', '2026-08-20 00:23:32.680', '2026-08-31 13:03:37.758', NULL, 'd5794fbe4db3dd48d3b4e218fb323c53', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E3 — domains/: move the 123-file tree into the layered package', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 13:20:21.937', NULL, 'd171daf9e0c3cae561e2e737c937e227', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E3 — domains/: move the 123-file tree into the layered package', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 13:23:30.519', NULL, '32e72a65f01a6ee07440ed9849a5fe25', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E3 — domains/: move the 123-file tree into the layered package', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 13:23:30.670', NULL, 'cc50d81b8581ea2365c1e128350039de', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'The domain map — every file assigned before any file moves', 'The blocking design work for this epic, and the reason per-domain port tickets are not filed yet. Produce a single committed artefact — tooling/DOMAINS.md or similar — assigning EVERY one of the ~128 legacy .py files and ~33 extensionless executables to a domain, with its future module path and CLI verb. Until that exists, a per-domain ticket cannot state what it moves. What makes this real work rather than clerical: ~51 top-level files have an obvious home from their prefix (blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2), but 28 SINGLETON prefixes do not, and several are genuinely ambiguous. Worked examples to settle explicitly, since each sets a precedent: canvas_sources.py and generator_sources.py are REGISTRIES consumed by gates, not commands — do they become check-domain helpers, or core, or stay module-level data; schema_version.py is consumed by the DB importer and by a gate, so it is shared between two domains; tea-comment and pr-watchlist-diff are PR workflow, which is NOT in the epic''s proposed domain list at all, so either a workflow domain is added or they go to dev. Also decide: does become a domain, or do the 5 test-prefixed files stay as standalone scripts run by make (they are gate tests, not commands anyone types). Deliverable rule to apply throughout: a domain that has only a router and a service does not get empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. Acceptance: no file unassigned, every ambiguous case carrying a one-line reason, and the verb list readable as the future output of reach --help.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-31 13:23:42.403', '2026-08-31 13:23:42.403', NULL, 'a47392206921fb53cbcf8cef87a20c68', 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 ('06G5FZCVZXH6M00C57EXMZD0SG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Rename the five hyphenated Python directories', 'economy-db, garment-fit, garment-qa, planet-gen and pql-migrate are not importable — a hyphen is not a valid Python identifier — so nothing under them can be reached from the package. Rename to economy_db, garment_fit, garment_qa, planet_gen, pql_migrate (final names to be confirmed by the domain map, T-1271, which may fold some of them into a domain rather than keep them as top-level packages). NOT renamed, and this corrects the epic description: tooling/econ-sim, tooling/line-previewer and tooling/test-client are RUST crates — Cargo.toml plus src/, zero .py files. A hyphen only matters for something Python must import, so all three keep their names and simply stay outside package discovery. The epic named only econ-sim; the other two were found in the survey. Use git mv so history follows, and expect the blast radius to be wide: the Makefile references these paths in many targets, the pre-push hook runs tests from planet-gen and economy-db by path, tooling/generator_sources.py registers economy-db source paths for the systems.db stamp, and .claude/settings.json has per-path permission entries. Grep for each old name across Makefile, .config/hooks, .claude, docs and tooling itself before declaring done. VERIFY BY RUNNING, not by grepping: make test-tooling must pass, since it invokes planet-gen and economy-db tests directly and is the only thing that will actually prove the paths still resolve.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-31 13:23:53.471', '2026-08-31 13:23:53.471', NULL, '695f033f2837d962c9cf5bd77249496c', 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 ('06G5FZDN9YBP0MZ021N3TJCFXM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'The Blender carve-out — payloads that must never import the package', 'The one legitimate exception to the no-tooling-outside-reach rule, and it needs to be written down as such rather than discovered later as an inconsistency. The blender_*.py files (14 at top level by the survey, not the 35 the epic states — verify the true count against subdirectories before moving) run under Blender''s BUNDLED Python interpreter, which has no access to the repo venv and cannot import tooling.core. They are payloads, not modules. Deliverables: move them to tooling/scripts/blender/ as a clearly-marked payload directory; keep tooling/blender as the bash wrapper that invokes Blender with a payload; front the whole thing with reach so the user-facing form is a normal verb. The payload directory is EXCLUDED from the conformance test''s package scope — those files legitimately use bare print() and cannot carry @command, and holding them to a contract they physically cannot satisfy would either fail the gate forever or force the contract to be weakened for everyone. Record the exclusion in the test with the reason inline, so the next person does not read it as an oversight and ''fix'' it. This carve-out is also one of the two exceptions T-1255 must name in the CLAUDE.md rule; the other is the Rust crates.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:23:59.951', '2026-08-31 13:23:59.951', NULL, '708f41a053be2679d4943a7a74c8356c', 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 ('06G5FZEM6JT5KKVE1RZ68DENSC', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Retire the ruff ignores the package makes unnecessary', 'The ruff config currently ignores E402 (module-level import not at top of file), E702 (multiple statements on one line) and F841 (unused local variable). The E402 debt is script-style sys.path.insert-before-imports, which is exactly what a real package removes — so most of it should evaporate once files move under tooling/domains/ and import normally. Deliverable: after the moves land, re-enable E402 and fix or explicitly per-file-ignore whatever remains; then assess E702 and F841 on their own evidence rather than assuming they go the same way. Do this LAST in the epic, since running it mid-move means fixing violations in files that are about to move again. Note the honest possibility that E402 cannot be fully re-enabled: the Blender payloads (T-1273) may legitimately need sys.path manipulation before imports because they run under Blender''s bundled interpreter — if so, a per-file ignore scoped to tooling/scripts/blender/ is the right answer, not carrying a global ignore for the whole tree. A narrow documented exception is worth more than a broad silent one.', 'backlog', 'low', NULL, NULL, 'D-263', '2026-08-31 13:24:07.860', '2026-08-31 13:24:07.860', NULL, '57ab6f39d9ac0de63856097114a169fa', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E3 — domains/: move the 123-file tree into the layered package', 'The mechanical bulk. Move every Python file into tooling/domains// split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. + +SURVEY 2026-08-31, before refining. The description above is right about the SHAPE and wrong about three counts. Re-measured: + +RUST CRATES: THREE, not one. The description names only tooling/econ-sim. There are also tooling/line-previewer and tooling/test-client — both Cargo.toml + src/ with zero .py files. All three are excluded from the move and from package discovery, and none needs renaming, since a hyphen only matters for something Python has to import. + +HYPHENATED PYTHON DIRECTORIES TO RENAME: FIVE, not four. economy-db (17 .py), garment-fit (23), garment-qa (1), planet-gen (30), pql-migrate (5). The description misses garment-qa. Already-importable subdirectories: db (9), wiki (1), plus the new core/ and domains/. + +FILE COUNT: ~128 legacy .py files plus ~33 extensionless executables, not 123 total. Top level alone holds 42 .py and ~33 executables; the subdirectories hold 76 more .py. + +WHAT THE PREFIX EVIDENCE ACTUALLY SUPPORTS — and this is the part that matters for how this epic is decomposed. Re-counted at the top level: blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2. That is ~51 files with an obvious home, and the description is right that the split is discovered rather than invented for them. +BUT there are 28 SINGLETON prefixes with no group to join: assign, canvas, clerk, convert, fill, generator, glb, inspect, migrate, patch, perf, pql, pr, process, render, schema, sculpt, setup, star, synth, tea, trellis, tune, wipe, worktree. Each needs a judgment call about which domain it belongs to, and several are genuinely ambiguous (canvas_sources.py and generator_sources.py are registries consumed by gates; schema_version.py is consumed by the DB importer; tea-comment and pr-watchlist-diff are PR workflow, which is not in the description''s domain list at all). + +CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev". Filing those nine now would be guessing, because ~28 files have no assigned domain and at least one domain (PR/workflow) is missing from the list. THE DOMAIN MAP IS THE BLOCKING DESIGN WORK and it gets its own ticket; per-domain port tickets are filed once it exists, so their boundaries are read off the map rather than invented and then renegotiated mid-move. + +REFINED 2026-08-31 — four cross-cutting tasks filed; the per-domain port tickets are DELIBERATELY NOT filed yet. T-1271 the domain map (blocking design — every file assigned before any file moves), T-1272 rename the five hyphenated Python directories, T-1273 the Blender carve-out, T-1274 retire the ruff ignores the package makes unnecessary. T-1272 and T-1273 are blocked on T-1271; T-1274 runs LAST, since re-enabling E402 mid-move means fixing violations in files that are about to move again. The per-domain tickets get filed from the map rather than from the epic''s proposed list, because that list is missing at least one domain (PR/workflow — tea-comment, pr-watchlist-diff) and leaves 28 singleton-prefix files unassigned. Filing nine tickets now and renegotiating their boundaries mid-move is the failure this ordering avoids.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 13:24:15.758', NULL, '7f3d4fb68aa7fc76fd25d9a7a21e0ac7', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'The domain map — every file assigned before any file moves', 'The blocking design work for this epic, and the reason per-domain port tickets are not filed yet. Produce a single committed artefact — tooling/DOMAINS.md or similar — assigning EVERY one of the ~128 legacy .py files and ~33 extensionless executables to a domain, with its future module path and CLI verb. Until that exists, a per-domain ticket cannot state what it moves. What makes this real work rather than clerical: ~51 top-level files have an obvious home from their prefix (blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2), but 28 SINGLETON prefixes do not, and several are genuinely ambiguous. Worked examples to settle explicitly, since each sets a precedent: canvas_sources.py and generator_sources.py are REGISTRIES consumed by gates, not commands — do they become check-domain helpers, or core, or stay module-level data; schema_version.py is consumed by the DB importer and by a gate, so it is shared between two domains; tea-comment and pr-watchlist-diff are PR workflow, which is NOT in the epic''s proposed domain list at all, so either a workflow domain is added or they go to dev. Also decide: does become a domain, or do the 5 test-prefixed files stay as standalone scripts run by make (they are gate tests, not commands anyone types). Deliverable rule to apply throughout: a domain that has only a router and a service does not get empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. Acceptance: no file unassigned, every ambiguous case carrying a one-line reason, and the verb list readable as the future output of reach --help.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-08-31 13:23:42.403', '2026-08-31 13:24:23.248', NULL, '2d5ac4ad8ae62bd30765d536d14e9fd0', 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 ('06G5FZBGREDJ12W3Q9GNB8C7PC', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'The domain map — every file assigned before any file moves', 'The blocking design work for this epic, and the reason per-domain port tickets are not filed yet. Produce a single committed artefact — tooling/DOMAINS.md or similar — assigning EVERY one of the ~128 legacy .py files and ~33 extensionless executables to a domain, with its future module path and CLI verb. Until that exists, a per-domain ticket cannot state what it moves. What makes this real work rather than clerical: ~51 top-level files have an obvious home from their prefix (blender 14, atlas 8, generate 7, check 7, test 5, visual 3, validate 3, install 2, godot 2), but 28 SINGLETON prefixes do not, and several are genuinely ambiguous. Worked examples to settle explicitly, since each sets a precedent: canvas_sources.py and generator_sources.py are REGISTRIES consumed by gates, not commands — do they become check-domain helpers, or core, or stay module-level data; schema_version.py is consumed by the DB importer and by a gate, so it is shared between two domains; tea-comment and pr-watchlist-diff are PR workflow, which is NOT in the epic''s proposed domain list at all, so either a workflow domain is added or they go to dev. Also decide: does become a domain, or do the 5 test-prefixed files stay as standalone scripts run by make (they are gate tests, not commands anyone types). Deliverable rule to apply throughout: a domain that has only a router and a service does not get empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota. Acceptance: no file unassigned, every ambiguous case carrying a one-line reason, and the verb list readable as the future output of reach --help. + +DONE 2026-08-31. tooling/DOMAINS.md committed: 15 domains, every file and executable assigned, judgment calls carrying their reasons. + +THREE CORRECTIONS FOUND BY COUNTING RATHER THAN READING, one of which reverses my own earlier survey. + +(1) THE BLENDER CARVE-OUT IS 35 FILES, NOT 13 — and the T-1250 description was RIGHT while my survey on this ticket was wrong. 13 blender_*.py sit at top level; 22 more are inside garment-fit/. My survey counted only the top level and told the epic it had overstated the number. It had not. +The consequence is not cosmetic. garment-fit/ is 23 files of which 22 are blender_author_* payloads, so it is a Blender payload directory wearing a domain''s name. `character` is therefore a MUCH smaller domain than directory sizes suggest, and T-1273''s carve-out is much larger. A per-domain ticket written from the directory listing would have been wrong about both. + +(2) THE "28 SINGLETON PREFIXES" WERE AN ARTEFACT OF MY OWN MEASUREMENT. Splitting filenames on the first token scattered coherent families: sculpt-star-map, tune-star-map-topology and generate-star-map* are ONE group (starmap), counted as three orphans. Counting families rather than prefixes, the genuinely ambiguous set is small and is now enumerated with reasons instead of being a number. + +(3) tooling/db/ IS MISNAMED and naming a domain after it would carry the misnomer forward. It holds the audio/image/Trellis connectors and wiki_sync.py; the actual database work is in economy-db/. Its contents split between a new `assets` domain (the tower-of-joy connectors) and `wiki`. + +JUDGMENT CALLS SETTLED, each with a recorded reason since each sets a precedent: +- Registries (canvas_sources.py, generator_sources.py) stay module-level data, NOT verbs. Nothing types them; putting them in reach --help would answer a question nobody has. +- schema_version.py goes to `db` where it is DEFINED, and `check` imports it. Shared but not equally owned — that cross-domain import is what a service layer is for. +- test_*.py do NOT become a domain. They are gate tests run by make, and `reach test ...` would imply a runner that does not exist. +- pql-migrate/ is PROVENANCE, not tooling. Moves to tooling/archive/, excluded from discovery. Deleting destroys migration provenance; keeping it importable implies it still runs. +- `pr` is a domain the epic did not list (tea-comment, pr-watchlist-diff, pql-board-html). Folding them into `dev` would make dev the drawer everything ambiguous goes into — the same rot D-263 warns about for core/. +- `character`, not D-263''s `garment`: the files cover bodies, hair, GLB and Quaternius as well as clothing. +- `atlas` IS OVERLOADED ACROSS THREE PLACES and must not be merged: top-level atlas-* executables (map data), planet-gen/atlas_*.py (terrain quality analysis), economy-db/atlas.py (systems.db index tables). Three concerns sharing a noun. Each stays with its owner. +- economy-db/errors.py predates core/errors.py and is not the same thing. Reconcile explicitly when db is ported; a silent merge or a second ReachError with different semantics is the risk. + +PER-DOMAIN PORT TICKETS can now be filed from this map rather than guessed. Recommended order, cheapest and most-verified first: check (4 remaining gates, already has the reference implementation and a parity test to copy), validate, godot, visual, then the large ones — planet, db, character, wiki, assets, atlas, starmap, generate, dev, pr.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-08-31 13:23:42.403', '2026-08-31 13:26:38.138', NULL, '7d9104b1b6b7710a8a5fe7fef483b882', 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 ('06G5FZDN9YBP0MZ021N3TJCFXM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'The Blender carve-out — payloads that must never import the package', 'The one legitimate exception to the no-tooling-outside-reach rule, and it needs to be written down as such rather than discovered later as an inconsistency. The blender_*.py files (14 at top level by the survey, not the 35 the epic states — verify the true count against subdirectories before moving) run under Blender''s BUNDLED Python interpreter, which has no access to the repo venv and cannot import tooling.core. They are payloads, not modules. Deliverables: move them to tooling/scripts/blender/ as a clearly-marked payload directory; keep tooling/blender as the bash wrapper that invokes Blender with a payload; front the whole thing with reach so the user-facing form is a normal verb. The payload directory is EXCLUDED from the conformance test''s package scope — those files legitimately use bare print() and cannot carry @command, and holding them to a contract they physically cannot satisfy would either fail the gate forever or force the contract to be weakened for everyone. Record the exclusion in the test with the reason inline, so the next person does not read it as an oversight and ''fix'' it. This carve-out is also one of the two exceptions T-1255 must name in the CLAUDE.md rule; the other is the Rust crates. + +CORRECTED 2026-08-31 by T-1271: the carve-out is 35 files, and the count in this ticket (which said 14 at top level, doubting the epic''s 35) was WRONG. 13 blender_*.py are at top level and 22 more are inside tooling/garment-fit/, which is a Blender payload directory wearing a domain''s name — 22 of its 23 files are blender_author_*. The epic''s figure of 35 was right. Practical consequence: garment-fit/ mostly moves to tooling/scripts/blender/ rather than becoming a character domain, so this ticket and the character port must be sequenced together or they will fight over the same directory.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:23:59.951', '2026-08-31 13:26:41.808', NULL, '27621bcf97a4f061f3f89f7868004932', 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; diff --git a/tooling/DOMAINS.md b/tooling/DOMAINS.md new file mode 100644 index 000000000..8800a75bd --- /dev/null +++ b/tooling/DOMAINS.md @@ -0,0 +1,129 @@ +# The domain map (T-1271, D-263) + +Every Python file and every executable in `tooling/`, assigned to a domain +before anything moves. This is the artefact the per-domain port tickets are +written from — without it their boundaries would be guesses, renegotiated +halfway through the move. + +**Status:** map only. Nothing here has moved yet except `check client-version`, +which shipped in T-1262 as the reference implementation. + +## What the survey changed + +Two corrections to the T-1250 description, both found by counting rather than +reading: + +- **Three Rust crates, not one.** `econ-sim`, `line-previewer` and `test-client` + are all Cargo projects with zero `.py` files. None is renamed and none moves; + a hyphen only matters for something Python must import. +- **The "28 singleton prefixes" were an artefact of my own measurement.** + Splitting filenames on the first token scattered coherent families: + `sculpt-star-map`, `tune-star-map-topology` and `generate-star-map*` are one + group, not three orphans. Counting families instead of prefixes, the genuinely + ambiguous set is small and is listed under *Judgment calls* below. + +And two found while reading the tree: + +- **`garment-fit/` is mostly a Blender payload directory.** 22 of its 23 files + are `blender_author_*.py`, leaving one real module. So the Blender carve-out + is **35 files**, not the 13 visible at top level — the T-1250 description was + right about 35 and an intermediate survey of mine was wrong, because it + counted only the top level. The consequence is not cosmetic: `character` + is a much smaller domain than the directory sizes imply, and the carve-out is + much larger. +- **`tooling/db/` is misnamed.** It holds the audio/image/Trellis connectors and + `wiki_sync.py` — the actual database work is in `economy-db/`. Naming a domain + `db` after that directory would carry the misnomer forward, so its contents + split between `assets` and `wiki` instead. + +## Domains + +| domain | what it is | sources | +|---|---|---| +| `check` | repo consistency gates the push hook runs | `check-client-version` ✅, `check-canvas-version`, `check-systems-db-stamp`, `check-fact-ids`, `check-dataflow-graph.py` | +| `validate` | content and schema validation | `validate-content`, `validate-checklist`, `validate-ron` | +| `atlas` | the Atlas map data surface | `atlas`, `atlas-check`, `atlas-names`, `atlas-commit-and-sync`, `atlas-systems-done`, `atlas-update-field`, `atlas-verify`, `atlas-flatness` | +| `starmap` | the star map and its renders | `generate-star-map.py`, `generate-star-map-data.py`, `generate-star-map-svg.py`, `sculpt-star-map.py`, `tune-star-map-topology.py`, `star-map-seed.json` | +| `planet` | planetary generation and its guards | `planet-gen/` (30 files) | +| `db` | the economics DB pipeline | `economy-db/` (17 files), `schema_version.py` | +| `wiki` | wiki sync and content maintenance | `wiki/`, `db/wiki_sync.py`, `db/populate_gttr_hook.py`, `db/backfill_cultural_corridor.py`, `assign-astro-ids.py`, `fill-missing-globes.py`, `migrate-s-to-gj.py`, `patch-core-sector.py`, `process-wiki-system-changes` | +| `assets` | connectors to the tower-of-joy generators | `db/audio_*.py`, `db/audio-*`, `db/image_connector.py`, `db/trellis_connector.py`, `db/common.py`, `trellis-batch.sh`, `synth_ui_sounds.py` | +| `character` | bodies, garments, GLB handling | `garment-fit/make_logo.py`, `garment-qa/analyze_captures.py`, `convert_outfit.py`, `glb_strip_utility_nodes.py`, `inspect_glb.py`, `check_hair_symmetry.py`, `check_icosphere.py`, `render_quaternius_test.py`, `setup_clothing_metadata.py` — **note this is far smaller than `garment-fit/`'s file count suggests; 22 of its 23 files are Blender payloads and belong to the carve-out** | +| `visual` | screenshot and render comparison | `visual-diff`, `visual-thumbnail`, `visual-blank-check` | +| `godot` | Godot parse and cold-start checks | `godot-parse-sweep`, `godot-cold-parse` | +| `generate` | content generators not owned elsewhere | `generate-brands`, `generate-corporations`, `generate-character-manifest`, `generate_corp_stubs.py` | +| `dev` | developer environment and workflow | `install-rust`, `install-godot`, `worktree-setup`, `perf-baseline`, `clerk-review` | +| `pr` | the PR/review loop | `tea-comment`, `pr-watchlist-diff`, `pql-board-html` | +| `blender` | **carve-out** — payloads run by Blender | **35** `blender_*.py` (13 top-level + 22 in `garment-fit/`), `blender` wrapper → `tooling/scripts/blender/` | + +## Judgment calls, with reasons + +Each of these sets a precedent, so the reasoning matters more than the answer. + +**Registries stay module-level data, not commands.** `canvas_sources.py` and +`generator_sources.py` are lists of paths consumed by gates — nothing types them. +They move beside the domain that reads them (`check` and `db` respectively) as +plain modules, not verbs. Making them commands would put something in +`reach --help` that answers no question a person has. + +**`schema_version.py` belongs to `db`, and `check` imports it.** It is consumed +by the economics importer *and* by the stamp gate. Shared, but not equally +owned: the importer defines the version, the gate reads it. It goes where it is +defined, and the cross-domain import is legitimate — that is what a service +layer is for. + +**`test_*.py` files do NOT become a domain.** They are gate tests run by +`make test-tooling`, not commands anyone types. `reach test …` would imply a +test runner that does not exist. They stay standalone scripts. + +**`pql-migrate/` is provenance, not tooling.** One-shot scripts from a completed +migration, already documented as such in `.claude/rules/project-structure.md`. +They are not live tools and must not become verbs. Move to `tooling/archive/`, +excluded from package discovery — deleting them would destroy migration +provenance, and keeping them importable would imply they still run. + +**`pr` is a domain the epic did not list.** `tea-comment` and `pr-watchlist-diff` +are the PR/review loop, which is neither `dev` (environment setup) nor anything +else on the original list. Folding them into `dev` would make `dev` the drawer +everything ambiguous goes into — which is how `core/` rots, and the same +argument applies here. + +**`atlas` is overloaded, and the port must not merge the three uses.** The word +appears in three unrelated places: the top-level `atlas-*` executables (the map +data surface), `planet-gen/atlas_*.py` (`atlas_cohesion_audit`, +`atlas_common`, `atlas_quality_analysis` — quality analysis of generated +terrain), and `economy-db/atlas.py` (the atlas index tables in `systems.db`). +These are three concerns sharing a noun, not one domain in three places. Each +stays with its owner — `atlas`, `planet` and `db` respectively — and the port +should resist the pull to collect them, which would produce a domain whose only +common thread is a word. + +**`economy-db/errors.py` predates `core/errors.py` and is not the same thing.** +Domain-local error types are fine; what must not happen is a silent merge, or a +second `ReachError` with different semantics. Reconcile explicitly when `db` is +ported. + +**`character` rather than `garment`.** D-263's sketch says `garment`, but the +files cover bodies, hair, GLB utilities and Quaternius imports as well as +clothing. Naming it for one of its five concerns would leave the other four +looking misfiled. + +## Not moving + +| what | why | +|---|---| +| `econ-sim/`, `line-previewer/`, `test-client/` | Rust crates. Excluded from package discovery; hyphens are harmless. | +| `scripts/blender/` (after T-1273) | Run under Blender's bundled Python; cannot import `tooling.core`. Excluded from the conformance scope for the same reason. | +| `archive/` (was `pql-migrate/`) | Completed-migration provenance. | +| `test_*.py` | Gate tests, not commands. | + +## Naming, applied on the way in + +- **Modules** `snake_case`: `check-dataflow-graph.py` → `dataflow_graph.py`. +- **Verbs** `kebab-case`: `reach check dataflow-graph`. +- **No prefix-as-namespace.** The domain directory supplies what the `blender_`, + `atlas-` and `check-` prefixes were doing by hand. `atlas-verify` becomes + `reach atlas verify`, not `reach atlas atlas-verify`. +- **Layering is a vocabulary, not a quota.** A domain with only a router and a + service gets exactly those two files. Empty `schemas.py` and `dependencies.py` + are worse than absent ones — they suggest a shape the domain does not have.