chore(meta): T-1250 — E3 done, the tree is ported

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 20:05:20 +02:00
co-authored by Claude Opus 5.5
parent 6fb0ba0e3d
commit 5a01450585
2 changed files with 114 additions and 0 deletions
+54
View File
@@ -568,3 +568,57 @@ E402: the planet modules'' late imports hoisted. They only sat low because they
F841: 10 dead locals removed from live code, each checked for side effects. logo_uv keeps its call, since creating the UV layer IS the effect. One FINDING, left as a note: planet_renderer computed an oblate-spheroid ray scale and never used it, so `oblateness` does not shape any globe. Wiring it in would change every globe render, so that is a decision for later, not a lint fix. Globe renders were checked pixel-identical before/after for an oceanic, a frozen and a gas-giant body. The one ledger edit (specialization.py) is a stamped source: regen run, stamp fresh, generated_brands.toml unchanged.', NULL, '2026-09-23 18:04:57', '2026-09-23 18:04:57.989', '2026-09-23 18:04:57.989', NULL, '3872d462d93fc083a89ea7995aa4a92f', 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 ('06G5FZCVZXH6M00C57EXMZD0SG', 'status', 'backlog', 'done', NULL, '2026-09-23 18:04:58', '2026-09-23 18:04:58.465', '2026-09-23 18:04:58.465', NULL, '0193b698d7a17d0fb079832ba05810ce', 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 ('06G5FZEM6JT5KKVE1RZ68DENSC', 'status', 'backlog', 'done', NULL, '2026-09-23 18:04:58', '2026-09-23 18:04:58.472', '2026-09-23 18:04:58.472', NULL, 'ce88f7efd90155d73670ce9c27d47b2c', 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/<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.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test — not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed — no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.
WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T-1283 godot and visual together. Deliberately NOT filing all fourteen domains at once — the standard port acceptance is defined once on T-1281 and referenced, and the remaining tickets (atlas, starmap, planet, db, wiki, assets, character, generate, dev, pr) get written after wave 1 lands, when the real friction of a port is known rather than guessed. Four descriptions written from experience beat ten written from a map. T-1272 was also RESCOPED today: the hyphenated directories are emptied by the domain moves rather than renamed, so it becomes a closing verification instead of an opening step.
SCOPE CORRECTION 2026-08-31, found on starting the first port and materially larger than the epic assumed. SEVENTEEN of the 33 extensionless executables are BASH, not Python — about 900 lines. This description and the domain map both assumed a Python tree, so those are REWRITES, not moves. Decided with Jeroen: rewrite all of them in Python rather than wrap any. Wrapping would achieve one door while leaving half the CLI surface outside the contract — no @command, no remedy on failure, no streaming, no testable service — so reach --help would list verbs that behave differently from the ones beside them, which is worse than two doors because the inconsistency is invisible until something fails. D-263 amended. The bash inventory, by shape: THIN WRAPPERS that mostly invoke something else (atlas-systems-done 9, atlas-names 17, generate-brands 23, atlas 24, generate-corporations 23, blender 28) — these become routers calling the same thing, nearly free. LOGIC that computes a verdict (check-fact-ids 89, validate-ron 137, godot-parse-sweep 64, godot-cold-parse 96, pr-watchlist-diff 37, atlas-update-field 59, atlas-commit-and-sync 60, tea-comment 41) — these gain the most from becoming services. ENVIRONMENT scripts (install-godot 108, install-rust 33, worktree-setup 49) — these gain least and carry the most regression risk, because downloading and unzipping a Godot build or driving rustup is awkward to exercise in a gate; port their DECISION logic into a testable service and keep the irreducible external calls behind core/process. Per-domain port tickets must state which of their sources are bash, since a bash source changes a port from mechanical to a rewrite needing its own parity evidence.
GUARDED EXEC RULE, clarified by Jeroen 2026-08-31 and now the operative reading of the rewrite decision: ''for proper OS level interactions I am okay with system calls in a well guarded exec here or there, but the logic needs to go into python''. So rewrite-in-Python does NOT mean reimplementing the operating system. A guarded exec is correct for rustup, curl, unzip, git, godot, blender. What must become Python is the LOGIC — which version is wanted, whether it is already present, what the output means, what to do when it fails. THE TEST OF A CORRECT PORT is not whether it calls anything external but whether the DECISIONS can be exercised without performing them. Delivered ahead of the remaining ports, since every one of them needs it: core/process.run() is the single sanctioned exec, with four guards that each exist because a per-domain subprocess call is where they go missing — (1) an argv LIST, never a shell string, with a string rejected outright rather than helpfully split, since the helpful split is the vulnerability; (2) shell=False always; (3) a non-zero exit becomes a ReachError naming the command, carrying its output and PRESERVING its exit code; (4) a missing binary reports WHAT TO INSTALL rather than raising FileNotFoundError at someone who wanted to know the next step. All four verified against real commands. New conformance invariant enforces the single door: nothing outside core/process.py may import subprocess or call os.system/popen/exec*. Proven to fail by importing subprocess into a domain service.', '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.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test — not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed — no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.
WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T-1283 godot and visual together. Deliberately NOT filing all fourteen domains at once — the standard port acceptance is defined once on T-1281 and referenced, and the remaining tickets (atlas, starmap, planet, db, wiki, assets, character, generate, dev, pr) get written after wave 1 lands, when the real friction of a port is known rather than guessed. Four descriptions written from experience beat ten written from a map. T-1272 was also RESCOPED today: the hyphenated directories are emptied by the domain moves rather than renamed, so it becomes a closing verification instead of an opening step.
SCOPE CORRECTION 2026-08-31, found on starting the first port and materially larger than the epic assumed. SEVENTEEN of the 33 extensionless executables are BASH, not Python — about 900 lines. This description and the domain map both assumed a Python tree, so those are REWRITES, not moves. Decided with Jeroen: rewrite all of them in Python rather than wrap any. Wrapping would achieve one door while leaving half the CLI surface outside the contract — no @command, no remedy on failure, no streaming, no testable service — so reach --help would list verbs that behave differently from the ones beside them, which is worse than two doors because the inconsistency is invisible until something fails. D-263 amended. The bash inventory, by shape: THIN WRAPPERS that mostly invoke something else (atlas-systems-done 9, atlas-names 17, generate-brands 23, atlas 24, generate-corporations 23, blender 28) — these become routers calling the same thing, nearly free. LOGIC that computes a verdict (check-fact-ids 89, validate-ron 137, godot-parse-sweep 64, godot-cold-parse 96, pr-watchlist-diff 37, atlas-update-field 59, atlas-commit-and-sync 60, tea-comment 41) — these gain the most from becoming services. ENVIRONMENT scripts (install-godot 108, install-rust 33, worktree-setup 49) — these gain least and carry the most regression risk, because downloading and unzipping a Godot build or driving rustup is awkward to exercise in a gate; port their DECISION logic into a testable service and keep the irreducible external calls behind core/process. Per-domain port tickets must state which of their sources are bash, since a bash source changes a port from mechanical to a rewrite needing its own parity evidence.
GUARDED EXEC RULE, clarified by Jeroen 2026-08-31 and now the operative reading of the rewrite decision: ''for proper OS level interactions I am okay with system calls in a well guarded exec here or there, but the logic needs to go into python''. So rewrite-in-Python does NOT mean reimplementing the operating system. A guarded exec is correct for rustup, curl, unzip, git, godot, blender. What must become Python is the LOGIC — which version is wanted, whether it is already present, what the output means, what to do when it fails. THE TEST OF A CORRECT PORT is not whether it calls anything external but whether the DECISIONS can be exercised without performing them. Delivered ahead of the remaining ports, since every one of them needs it: core/process.run() is the single sanctioned exec, with four guards that each exist because a per-domain subprocess call is where they go missing — (1) an argv LIST, never a shell string, with a string rejected outright rather than helpfully split, since the helpful split is the vulnerability; (2) shell=False always; (3) a non-zero exit becomes a ReachError naming the command, carrying its output and PRESERVING its exit code; (4) a missing binary reports WHAT TO INSTALL rather than raising FileNotFoundError at someone who wanted to know the next step. All four verified against real commands. New conformance invariant enforces the single door: nothing outside core/process.py may import subprocess or call os.system/popen/exec*. Proven to fail by importing subprocess into a domain service.
DONE 2026-09-23. Every legacy file under tooling/ is now in one of four places: a reach domain (14: check, validate, atlas {map, planet}, ledger, wiki, assets, character, godot, visual, generate, blender, pr, jobs, dev), the Blender carve-out (41 payloads), tooling/archive/ (provenance with a README saying why each must not run), or a registry module (canvas_sources, generator_sources, per the map''s ruling). Closing tickets this session: T-1288 planet, T-1289 ledger, T-1290 wiki/assets/character, T-1293 atlas map (filed today; it never had a ticket), T-1272, T-1274.
Recurring finding across the ports: tools whose failure read as success. Exit 0 with failures in batch --verify-determinism, import-provinces, audio batch, trellis batch and the garment-QA analyzer; a crash on trellis generate; glb strip re-reporting orphans. All fixed in the port that found them.
Two tools archived because RUNNING them is destructive against today''s committed data: wiki_sync.generate_wiki (T-1292) and generate-star-map (T-1294). In both cases the docs and rules were telling agents to run them.', NULL, '2026-09-23 18:05:18', '2026-09-23 18:05:18.760', '2026-09-23 18:05:18.760', NULL, '1ce5c9f62390a48d046d640c0f3ee46c', 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', 'done', NULL, '2026-09-23 18:05:19', '2026-09-23 18:05:19.260', '2026-09-23 18:05:19.260', NULL, '4178a2e1f60ae93db618f7ee7d77efd6', 2) ON CONFLICT(hash) DO NOTHING;
+60
View File
@@ -710,3 +710,63 @@ DONE 2026-09-23. E402, E702 and F841 re-enabled tree-wide (they were ignored for
E402: the planet modules'' late imports hoisted. They only sat low because they used to follow a sys.path insert (gone since T-1288).
F841: 10 dead locals removed from live code, each checked for side effects. logo_uv keeps its call, since creating the UV layer IS the effect. One FINDING, left as a note: planet_renderer computed an oblate-spheroid ray scale and never used it, so `oblateness` does not shape any globe. Wiring it in would change every globe render, so that is a decision for later, not a lint fix. Globe renders were checked pixel-identical before/after for an oceanic, a frozen and a gas-giant body. The one ledger edit (specialization.py) is a stamped source: regen run, stamp fresh, generated_brands.toml unchanged.', 'done', 'low', NULL, NULL, 'D-263', '2026-08-31 13:24:07.860', '2026-09-23 18:04:58.472', NULL, 'b2db3b5f93dd2d2f04a4981bb30a2813', 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.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test — not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed — no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.
WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T-1283 godot and visual together. Deliberately NOT filing all fourteen domains at once — the standard port acceptance is defined once on T-1281 and referenced, and the remaining tickets (atlas, starmap, planet, db, wiki, assets, character, generate, dev, pr) get written after wave 1 lands, when the real friction of a port is known rather than guessed. Four descriptions written from experience beat ten written from a map. T-1272 was also RESCOPED today: the hyphenated directories are emptied by the domain moves rather than renamed, so it becomes a closing verification instead of an opening step.
SCOPE CORRECTION 2026-08-31, found on starting the first port and materially larger than the epic assumed. SEVENTEEN of the 33 extensionless executables are BASH, not Python — about 900 lines. This description and the domain map both assumed a Python tree, so those are REWRITES, not moves. Decided with Jeroen: rewrite all of them in Python rather than wrap any. Wrapping would achieve one door while leaving half the CLI surface outside the contract — no @command, no remedy on failure, no streaming, no testable service — so reach --help would list verbs that behave differently from the ones beside them, which is worse than two doors because the inconsistency is invisible until something fails. D-263 amended. The bash inventory, by shape: THIN WRAPPERS that mostly invoke something else (atlas-systems-done 9, atlas-names 17, generate-brands 23, atlas 24, generate-corporations 23, blender 28) — these become routers calling the same thing, nearly free. LOGIC that computes a verdict (check-fact-ids 89, validate-ron 137, godot-parse-sweep 64, godot-cold-parse 96, pr-watchlist-diff 37, atlas-update-field 59, atlas-commit-and-sync 60, tea-comment 41) — these gain the most from becoming services. ENVIRONMENT scripts (install-godot 108, install-rust 33, worktree-setup 49) — these gain least and carry the most regression risk, because downloading and unzipping a Godot build or driving rustup is awkward to exercise in a gate; port their DECISION logic into a testable service and keep the irreducible external calls behind core/process. Per-domain port tickets must state which of their sources are bash, since a bash source changes a port from mechanical to a rewrite needing its own parity evidence.
GUARDED EXEC RULE, clarified by Jeroen 2026-08-31 and now the operative reading of the rewrite decision: ''for proper OS level interactions I am okay with system calls in a well guarded exec here or there, but the logic needs to go into python''. So rewrite-in-Python does NOT mean reimplementing the operating system. A guarded exec is correct for rustup, curl, unzip, git, godot, blender. What must become Python is the LOGIC — which version is wanted, whether it is already present, what the output means, what to do when it fails. THE TEST OF A CORRECT PORT is not whether it calls anything external but whether the DECISIONS can be exercised without performing them. Delivered ahead of the remaining ports, since every one of them needs it: core/process.run() is the single sanctioned exec, with four guards that each exist because a per-domain subprocess call is where they go missing — (1) an argv LIST, never a shell string, with a string rejected outright rather than helpfully split, since the helpful split is the vulnerability; (2) shell=False always; (3) a non-zero exit becomes a ReachError naming the command, carrying its output and PRESERVING its exit code; (4) a missing binary reports WHAT TO INSTALL rather than raising FileNotFoundError at someone who wanted to know the next step. All four verified against real commands. New conformance invariant enforces the single door: nothing outside core/process.py may import subprocess or call os.system/popen/exec*. Proven to fail by importing subprocess into a domain service.
DONE 2026-09-23. Every legacy file under tooling/ is now in one of four places: a reach domain (14: check, validate, atlas {map, planet}, ledger, wiki, assets, character, godot, visual, generate, blender, pr, jobs, dev), the Blender carve-out (41 payloads), tooling/archive/ (provenance with a README saying why each must not run), or a registry module (canvas_sources, generator_sources, per the map''s ruling). Closing tickets this session: T-1288 planet, T-1289 ledger, T-1290 wiki/assets/character, T-1293 atlas map (filed today; it never had a ticket), T-1272, T-1274.
Recurring finding across the ports: tools whose failure read as success. Exit 0 with failures in batch --verify-determinism, import-provinces, audio batch, trellis batch and the garment-QA analyzer; a crash on trellis generate; glb strip re-reporting orphans. All fixed in the port that found them.
Two tools archived because RUNNING them is destructive against today''s committed data: wiki_sync.generate_wiki (T-1292) and generate-star-map (T-1294). In both cases the docs and rules were telling agents to run them.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-09-23 18:05:18.760', NULL, 'efdfe47c0ed50cc43c12d87e12f4d0eb', 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.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test — not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed — no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.
WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T-1283 godot and visual together. Deliberately NOT filing all fourteen domains at once — the standard port acceptance is defined once on T-1281 and referenced, and the remaining tickets (atlas, starmap, planet, db, wiki, assets, character, generate, dev, pr) get written after wave 1 lands, when the real friction of a port is known rather than guessed. Four descriptions written from experience beat ten written from a map. T-1272 was also RESCOPED today: the hyphenated directories are emptied by the domain moves rather than renamed, so it becomes a closing verification instead of an opening step.
SCOPE CORRECTION 2026-08-31, found on starting the first port and materially larger than the epic assumed. SEVENTEEN of the 33 extensionless executables are BASH, not Python — about 900 lines. This description and the domain map both assumed a Python tree, so those are REWRITES, not moves. Decided with Jeroen: rewrite all of them in Python rather than wrap any. Wrapping would achieve one door while leaving half the CLI surface outside the contract — no @command, no remedy on failure, no streaming, no testable service — so reach --help would list verbs that behave differently from the ones beside them, which is worse than two doors because the inconsistency is invisible until something fails. D-263 amended. The bash inventory, by shape: THIN WRAPPERS that mostly invoke something else (atlas-systems-done 9, atlas-names 17, generate-brands 23, atlas 24, generate-corporations 23, blender 28) — these become routers calling the same thing, nearly free. LOGIC that computes a verdict (check-fact-ids 89, validate-ron 137, godot-parse-sweep 64, godot-cold-parse 96, pr-watchlist-diff 37, atlas-update-field 59, atlas-commit-and-sync 60, tea-comment 41) — these gain the most from becoming services. ENVIRONMENT scripts (install-godot 108, install-rust 33, worktree-setup 49) — these gain least and carry the most regression risk, because downloading and unzipping a Godot build or driving rustup is awkward to exercise in a gate; port their DECISION logic into a testable service and keep the irreducible external calls behind core/process. Per-domain port tickets must state which of their sources are bash, since a bash source changes a port from mechanical to a rewrite needing its own parity evidence.
GUARDED EXEC RULE, clarified by Jeroen 2026-08-31 and now the operative reading of the rewrite decision: ''for proper OS level interactions I am okay with system calls in a well guarded exec here or there, but the logic needs to go into python''. So rewrite-in-Python does NOT mean reimplementing the operating system. A guarded exec is correct for rustup, curl, unzip, git, godot, blender. What must become Python is the LOGIC — which version is wanted, whether it is already present, what the output means, what to do when it fails. THE TEST OF A CORRECT PORT is not whether it calls anything external but whether the DECISIONS can be exercised without performing them. Delivered ahead of the remaining ports, since every one of them needs it: core/process.run() is the single sanctioned exec, with four guards that each exist because a per-domain subprocess call is where they go missing — (1) an argv LIST, never a shell string, with a string rejected outright rather than helpfully split, since the helpful split is the vulnerability; (2) shell=False always; (3) a non-zero exit becomes a ReachError naming the command, carrying its output and PRESERVING its exit code; (4) a missing binary reports WHAT TO INSTALL rather than raising FileNotFoundError at someone who wanted to know the next step. All four verified against real commands. New conformance invariant enforces the single door: nothing outside core/process.py may import subprocess or call os.system/popen/exec*. Proven to fail by importing subprocess into a domain service.
DONE 2026-09-23. Every legacy file under tooling/ is now in one of four places: a reach domain (14: check, validate, atlas {map, planet}, ledger, wiki, assets, character, godot, visual, generate, blender, pr, jobs, dev), the Blender carve-out (41 payloads), tooling/archive/ (provenance with a README saying why each must not run), or a registry module (canvas_sources, generator_sources, per the map''s ruling). Closing tickets this session: T-1288 planet, T-1289 ledger, T-1290 wiki/assets/character, T-1293 atlas map (filed today; it never had a ticket), T-1272, T-1274.
Recurring finding across the ports: tools whose failure read as success. Exit 0 with failures in batch --verify-determinism, import-provinces, audio batch, trellis batch and the garment-QA analyzer; a crash on trellis generate; glb strip re-reporting orphans. All fixed in the port that found them.
Two tools archived because RUNNING them is destructive against today''s committed data: wiki_sync.generate_wiki (T-1292) and generate-star-map (T-1294). In both cases the docs and rules were telling agents to run them.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-09-23 18:05:19.260', NULL, 'c1e720e40b6c411fd917c8423d1c42d9', 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;