|
|
|
@@ -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/<name>/ 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/<name>/ 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/<name>/ 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/<name>/ 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;
|
|
|
|
|