docs(tooling): T-1256 — adoption: the reach skill, the standing rule, the diagram

The consolidation has to be the only way to add tooling, or it re-fragments
the way the tree did — ~160 files and four naming conventions, because
nothing ruled out adding one more.

- Skill(reach) (T-1254), for both audiences. Using it: discover through
  --help, the domain index, the global flags an agent needs (--no-input,
  --detach with `jobs log --follow`), the stdout/stderr/verdict contract, and
  "read the exit status, never a field". Extending it: add a verb, add a
  domain, the seven invariants the conformance test enforces, a worked
  ReachError-with-fix example, the parity-first porting method, and the
  traps this repo has already paid for (the stamp registry, the canvas
  registry, archived generators that destroy data if run).
- CLAUDE.md (T-1255): no tooling is developed outside the CLI. The two
  exceptions are named so they aren't relitigated: Blender payloads and the
  Rust crates. Wording as confirmed by Jeroen, 2026-08-20.
- docs/diagrams/architecture/reach-cli.d2: the door, core, the domains, the
  carve-out, the one guarded exec, and the enforced invariants as a legend.
  D-263 now links to it; the amendment that had removed the dangling forward
  reference asked for exactly this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 20:21:27 +02:00
co-authored by Claude Opus 5.5
parent c597ec9131
commit 876cb8256e
7 changed files with 381 additions and 1 deletions
+16
View File
@@ -622,3 +622,19 @@ Recurring finding across the ports: tools whose failure read as success. Exit 0
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;
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 ('06G1S3DTRHSBYJS55NDT9YR4R0', 'description', 'The highest-frequency, highest-friction surface, and the one with a hard number attached. Baseline measured 2026-08-20: check-client-version 20 ms, check-dataflow-graph 38 ms, check-canvas-version 46 ms — about 104 ms for the three unconditional checks; check-systems-db-stamp runs only when systems.db is in the push. The ported gates must not exceed that. Pieces that become tickets: (1) port the four checks into domains/check/ — client-version, canvas-version, dataflow-graph, systems-db-stamp; (2) port validate-content, validate-checklist, validate-ron, check-fact-ids into domains/validate/; (3) rewire .config/hooks/pre-push to call reach, keeping the fail_check by-name reporting that hook deliberately has; (4) prove each gate still FAILS — a check that explains itself and exits 0 silently disables its own gate, which is exactly the clide failure recorded in D-263; test each one against a deliberately broken tree, not just a clean one; (5) the wall-clock assertion that turns the budget into a test; (6) confirm domains/check/ imports no pydantic — 87 ms against a 20 ms check is a 3.5x regression bought for nothing, four times per push. Depends on E1 (lazy registration) and E2 (the error contract) being real first.
AMENDED 2026-08-20 — the 104 ms ceiling in the description above is WITHDRAWN. D-263 was amended the same day: the acceptance criterion is OUTPUT PARITY, not timing parity. A ported check must produce the same output and the same exit code as the script it replaces; it is not required to be as fast. Reason: the only automatic consumer is the pre-push hook, which on a server or client push already runs cargo test or the gdUnit4 suite (minutes), and on a governance-only push totals about a second — so a few hundred ms is invisible either way. The replacement budget is a ceiling with headroom: a reach invocation should feel instant to a human, under about 250 ms, and the unconditional gate set stays comfortably under a second. Item (5) of this ticket, the wall-clock assertion, is REPLACED by an import-graph assertion — a timing test is flaky on a loaded machine and reports that something got slow rather than what; asserting sys.modules after reach --help names the offender. Item (6), the no-pydantic-in-check rule, is DROPPED — pydantic at 87 ms is unremarkable against a 250 ms ceiling, and the carve-out bought 87 ms at the cost of an exception to the layering.
FROM T-1262 (2026-08-31) — what ''output parity'' means was sharpened while porting the first gate, and it applies to every port in this epic. Byte-for-byte parity is IMPOSSIBLE under the streaming model and should not be attempted: D-263 puts the verdict on stderr as part of the event stream, while the scripts being replaced write their success line to stdout. Matching both would mean abandoning streaming or special-casing each ported gate. The enforceable definition, now in D-263: (a) EXIT CODES MATCH EXACTLY — the hook gates on this and it is the only part a caller can act on programmatically; (b) NO FACT IS LOST — every version number, path and reason the old message carried must appear in the new one, since a migration that silently drops a detail makes the failure harder to fix than before; (c) FAILURES NAME A REMEDY as a structured field, which the old scripts did only in prose. In text mode the success line is in fact byte-identical; only the stream differs. tooling/test_check_parity.py is the working pattern to copy for the other three checks — it builds a throwaway fixture repo (sentinel project.yaml plus client/project.godot), copies the OLD script into it so its __file__-relative root resolves there, points the new command at the same fixture via SR_REPO_ROOT, and compares. Note the asymmetry it exposes: the old scripts have NO root override, which is precisely why they are hard to test and part of why the port is worth doing.', 'The highest-frequency, highest-friction surface, and the one with a hard number attached. Baseline measured 2026-08-20: check-client-version 20 ms, check-dataflow-graph 38 ms, check-canvas-version 46 ms — about 104 ms for the three unconditional checks; check-systems-db-stamp runs only when systems.db is in the push. The ported gates must not exceed that. Pieces that become tickets: (1) port the four checks into domains/check/ — client-version, canvas-version, dataflow-graph, systems-db-stamp; (2) port validate-content, validate-checklist, validate-ron, check-fact-ids into domains/validate/; (3) rewire .config/hooks/pre-push to call reach, keeping the fail_check by-name reporting that hook deliberately has; (4) prove each gate still FAILS — a check that explains itself and exits 0 silently disables its own gate, which is exactly the clide failure recorded in D-263; test each one against a deliberately broken tree, not just a clean one; (5) the wall-clock assertion that turns the budget into a test; (6) confirm domains/check/ imports no pydantic — 87 ms against a 20 ms check is a 3.5x regression bought for nothing, four times per push. Depends on E1 (lazy registration) and E2 (the error contract) being real first.
AMENDED 2026-08-20 — the 104 ms ceiling in the description above is WITHDRAWN. D-263 was amended the same day: the acceptance criterion is OUTPUT PARITY, not timing parity. A ported check must produce the same output and the same exit code as the script it replaces; it is not required to be as fast. Reason: the only automatic consumer is the pre-push hook, which on a server or client push already runs cargo test or the gdUnit4 suite (minutes), and on a governance-only push totals about a second — so a few hundred ms is invisible either way. The replacement budget is a ceiling with headroom: a reach invocation should feel instant to a human, under about 250 ms, and the unconditional gate set stays comfortably under a second. Item (5) of this ticket, the wall-clock assertion, is REPLACED by an import-graph assertion — a timing test is flaky on a loaded machine and reports that something got slow rather than what; asserting sys.modules after reach --help names the offender. Item (6), the no-pydantic-in-check rule, is DROPPED — pydantic at 87 ms is unremarkable against a 250 ms ceiling, and the carve-out bought 87 ms at the cost of an exception to the layering.
FROM T-1262 (2026-08-31) — what ''output parity'' means was sharpened while porting the first gate, and it applies to every port in this epic. Byte-for-byte parity is IMPOSSIBLE under the streaming model and should not be attempted: D-263 puts the verdict on stderr as part of the event stream, while the scripts being replaced write their success line to stdout. Matching both would mean abandoning streaming or special-casing each ported gate. The enforceable definition, now in D-263: (a) EXIT CODES MATCH EXACTLY — the hook gates on this and it is the only part a caller can act on programmatically; (b) NO FACT IS LOST — every version number, path and reason the old message carried must appear in the new one, since a migration that silently drops a detail makes the failure harder to fix than before; (c) FAILURES NAME A REMEDY as a structured field, which the old scripts did only in prose. In text mode the success line is in fact byte-identical; only the stream differs. tooling/test_check_parity.py is the working pattern to copy for the other three checks — it builds a throwaway fixture repo (sentinel project.yaml plus client/project.godot), copies the OLD script into it so its __file__-relative root resolves there, points the new command at the same fixture via SR_REPO_ROOT, and compares. Note the asymmetry it exposes: the old scripts have NO root override, which is precisely why they are hard to test and part of why the port is worth doing.
DONE 2026-09-23, audited against the items rather than assumed. (1)+(2) the gate ports landed as T-1281 (check) and T-1282 (validate). (3) .config/hooks/pre-push and pre-commit invoke only `reach --no-input …` for every gate (plus ruff and json.tool); no tooling/ script path survives in any hook. (4) gates proven to FAIL: test_check.py runs 19 cases across five gates, failures included; this session the canvas-version gate did its job for real, rejecting a push of a comment-only change to a registered file (hence 0.4.14). (5) replaced by the import-graph assertion, which exists: test_lazy_domains.py asserts that after `reach --help` no tooling.domains module and none of rich/pygments/numpy/scipy/pydantic/PIL is imported, with a positive control. (6) dropped by the 2026-08-20 amendment.', NULL, '2026-09-23 18:16:36', '2026-09-23 18:16:36.200', '2026-09-23 18:16:36.200', NULL, '36015bf18eb0617695d7b3d69838b857', 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 ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'description', 'The riskiest epic, and the one with a hard sequencing constraint. tooling/generator_sources.py SHAs the concatenated bytes of the generator sources SORTED BY PATH, so renaming a file changes the stamp even when its content is byte-identical. The relocation must therefore land as ONE commit — registry paths updated, make regen-db run, stamp verified — never split across pushes, or the pre-push gate rejects an intermediate state that is in fact correct. Pieces that become tickets: (1) move import_economics.py and the economy_import package into domains/db/, update generator_sources.py in the same change, regen and verify the stamp; (2) the generate_brands Rust binary is a SUBROUTINE of the Python importer, not an independent generator — keep that relationship intact; (3) canvas_sources.py and schema_version.py move with their consumers, and check-canvas-version must keep resolving the registry; (4) planet-gen into domains/planet/ — import_heightmaps, import_province_boundaries, planet_simulation, scaffold_bodies, render_heightmap; these are one-time build imports baked into the committed DB, intentionally unstamped, and they lean on numpy/scipy/PIL; (5) garment-fit into domains/garment/ with its 23 blender_author_* files going to scripts/blender/ per the carve-out; (6) db/wiki_sync.py and the audio/image/trellis connectors, which resolve a venv explicitly today in tooling/db/common.py and should stop needing to; (7) test_sim_determinism and test_oasis_ring_scaling keep passing — they are the guard that the numerics did not move under us. NOTHING here is rewritten. Code is relocated and re-fronted; no numerical behaviour changes, which is precisely why Rust was rejected.', 'The riskiest epic, and the one with a hard sequencing constraint. tooling/generator_sources.py SHAs the concatenated bytes of the generator sources SORTED BY PATH, so renaming a file changes the stamp even when its content is byte-identical. The relocation must therefore land as ONE commit — registry paths updated, make regen-db run, stamp verified — never split across pushes, or the pre-push gate rejects an intermediate state that is in fact correct. Pieces that become tickets: (1) move import_economics.py and the economy_import package into domains/db/, update generator_sources.py in the same change, regen and verify the stamp; (2) the generate_brands Rust binary is a SUBROUTINE of the Python importer, not an independent generator — keep that relationship intact; (3) canvas_sources.py and schema_version.py move with their consumers, and check-canvas-version must keep resolving the registry; (4) planet-gen into domains/planet/ — import_heightmaps, import_province_boundaries, planet_simulation, scaffold_bodies, render_heightmap; these are one-time build imports baked into the committed DB, intentionally unstamped, and they lean on numpy/scipy/PIL; (5) garment-fit into domains/garment/ with its 23 blender_author_* files going to scripts/blender/ per the carve-out; (6) db/wiki_sync.py and the audio/image/trellis connectors, which resolve a venv explicitly today in tooling/db/common.py and should stop needing to; (7) test_sim_determinism and test_oasis_ring_scaling keep passing — they are the guard that the numerics did not move under us. NOTHING here is rewritten. Code is relocated and re-fronted; no numerical behaviour changes, which is precisely why Rust was rejected.
DONE 2026-09-23 — delivered by the E3 ports, item by item. (1) import_economics + economy_import moved to domains/ledger/ (the map renamed db to ledger), generator_sources.py updated in the same commit, regen run, stamp verified: T-1289. (2) generate_brands kept as a subroutine of the importer, and the relationship is now tighter: cargo_binary rebuilds a stale binary, where before a Rust edit could be stamped over output from the old binary (fix commit e3daf561d). (3) schema_version.py moved with the ledger; canvas_sources.py stays a top-level registry module per the domain map, and check-canvas-version still resolves it. (4) planet-gen became domains/atlas/planet (T-1288); the one-time importers stayed unstamped. (5) garment-fit: 22 payloads to scripts/blender (T-1273) + make_logo to domains/character (T-1290). (6) wiki_sync + connectors to domains/wiki and domains/assets; ensure_venv is gone (T-1290). (7) test_sim_determinism and test_oasis_ring_scaling moved to tooling/test_planet_*.py and pass; globe renders were pixel-identical across the T-1274 cleanup. Nothing numeric changed: every byte-comparable generator output was checked identical before/after.', NULL, '2026-09-23 18:16:36', '2026-09-23 18:16:36.730', '2026-09-23 18:16:36.730', NULL, '94372e7d3f4ec83ccfda9cf5844e0b93', 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 ('06G1S3DTRHSBYJS55NDT9YR4R0', 'status', 'backlog', 'done', NULL, '2026-09-23 18:16:37', '2026-09-23 18:16:37.323', '2026-09-23 18:16:37.323', NULL, '3f5b676d54ad0b58a6f0879f9a5c7642', 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 ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'status', 'backlog', 'done', NULL, '2026-09-23 18:16:37', '2026-09-23 18:16:37.331', '2026-09-23 18:16:37.331', NULL, '28db5755c043d2ee316b03594cbc8de1', 2) ON CONFLICT(hash) DO NOTHING;
+20
View File
@@ -770,3 +770,23 @@ DONE 2026-09-23. Every legacy file under tooling/ is now in one of four places:
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;
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 ('06G1S3DTRHSBYJS55NDT9YR4R0', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E4 — The gate family and the push hook, at output parity', 'The highest-frequency, highest-friction surface, and the one with a hard number attached. Baseline measured 2026-08-20: check-client-version 20 ms, check-dataflow-graph 38 ms, check-canvas-version 46 ms — about 104 ms for the three unconditional checks; check-systems-db-stamp runs only when systems.db is in the push. The ported gates must not exceed that. Pieces that become tickets: (1) port the four checks into domains/check/ — client-version, canvas-version, dataflow-graph, systems-db-stamp; (2) port validate-content, validate-checklist, validate-ron, check-fact-ids into domains/validate/; (3) rewire .config/hooks/pre-push to call reach, keeping the fail_check by-name reporting that hook deliberately has; (4) prove each gate still FAILS — a check that explains itself and exits 0 silently disables its own gate, which is exactly the clide failure recorded in D-263; test each one against a deliberately broken tree, not just a clean one; (5) the wall-clock assertion that turns the budget into a test; (6) confirm domains/check/ imports no pydantic — 87 ms against a 20 ms check is a 3.5x regression bought for nothing, four times per push. Depends on E1 (lazy registration) and E2 (the error contract) being real first.
AMENDED 2026-08-20 — the 104 ms ceiling in the description above is WITHDRAWN. D-263 was amended the same day: the acceptance criterion is OUTPUT PARITY, not timing parity. A ported check must produce the same output and the same exit code as the script it replaces; it is not required to be as fast. Reason: the only automatic consumer is the pre-push hook, which on a server or client push already runs cargo test or the gdUnit4 suite (minutes), and on a governance-only push totals about a second — so a few hundred ms is invisible either way. The replacement budget is a ceiling with headroom: a reach invocation should feel instant to a human, under about 250 ms, and the unconditional gate set stays comfortably under a second. Item (5) of this ticket, the wall-clock assertion, is REPLACED by an import-graph assertion — a timing test is flaky on a loaded machine and reports that something got slow rather than what; asserting sys.modules after reach --help names the offender. Item (6), the no-pydantic-in-check rule, is DROPPED — pydantic at 87 ms is unremarkable against a 250 ms ceiling, and the carve-out bought 87 ms at the cost of an exception to the layering.
FROM T-1262 (2026-08-31) — what ''output parity'' means was sharpened while porting the first gate, and it applies to every port in this epic. Byte-for-byte parity is IMPOSSIBLE under the streaming model and should not be attempted: D-263 puts the verdict on stderr as part of the event stream, while the scripts being replaced write their success line to stdout. Matching both would mean abandoning streaming or special-casing each ported gate. The enforceable definition, now in D-263: (a) EXIT CODES MATCH EXACTLY — the hook gates on this and it is the only part a caller can act on programmatically; (b) NO FACT IS LOST — every version number, path and reason the old message carried must appear in the new one, since a migration that silently drops a detail makes the failure harder to fix than before; (c) FAILURES NAME A REMEDY as a structured field, which the old scripts did only in prose. In text mode the success line is in fact byte-identical; only the stream differs. tooling/test_check_parity.py is the working pattern to copy for the other three checks — it builds a throwaway fixture repo (sentinel project.yaml plus client/project.godot), copies the OLD script into it so its __file__-relative root resolves there, points the new command at the same fixture via SR_REPO_ROOT, and compares. Note the asymmetry it exposes: the old scripts have NO root override, which is precisely why they are hard to test and part of why the port is worth doing.
DONE 2026-09-23, audited against the items rather than assumed. (1)+(2) the gate ports landed as T-1281 (check) and T-1282 (validate). (3) .config/hooks/pre-push and pre-commit invoke only `reach --no-input …` for every gate (plus ruff and json.tool); no tooling/ script path survives in any hook. (4) gates proven to FAIL: test_check.py runs 19 cases across five gates, failures included; this session the canvas-version gate did its job for real, rejecting a push of a comment-only change to a registered file (hence 0.4.14). (5) replaced by the import-graph assertion, which exists: test_lazy_domains.py asserts that after `reach --help` no tooling.domains module and none of rich/pygments/numpy/scipy/pydantic/PIL is imported, with a positive control. (6) dropped by the 2026-08-20 amendment.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-20 00:24:05.572', '2026-09-23 18:16:36.200', NULL, '4da4c39a424c323ff32d460d739cb130', 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 ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E5 — Generators and numerics, without breaking the systems.db stamp', 'The riskiest epic, and the one with a hard sequencing constraint. tooling/generator_sources.py SHAs the concatenated bytes of the generator sources SORTED BY PATH, so renaming a file changes the stamp even when its content is byte-identical. The relocation must therefore land as ONE commit — registry paths updated, make regen-db run, stamp verified — never split across pushes, or the pre-push gate rejects an intermediate state that is in fact correct. Pieces that become tickets: (1) move import_economics.py and the economy_import package into domains/db/, update generator_sources.py in the same change, regen and verify the stamp; (2) the generate_brands Rust binary is a SUBROUTINE of the Python importer, not an independent generator — keep that relationship intact; (3) canvas_sources.py and schema_version.py move with their consumers, and check-canvas-version must keep resolving the registry; (4) planet-gen into domains/planet/ — import_heightmaps, import_province_boundaries, planet_simulation, scaffold_bodies, render_heightmap; these are one-time build imports baked into the committed DB, intentionally unstamped, and they lean on numpy/scipy/PIL; (5) garment-fit into domains/garment/ with its 23 blender_author_* files going to scripts/blender/ per the carve-out; (6) db/wiki_sync.py and the audio/image/trellis connectors, which resolve a venv explicitly today in tooling/db/common.py and should stop needing to; (7) test_sim_determinism and test_oasis_ring_scaling keep passing — they are the guard that the numerics did not move under us. NOTHING here is rewritten. Code is relocated and re-fronted; no numerical behaviour changes, which is precisely why Rust was rejected.
DONE 2026-09-23 — delivered by the E3 ports, item by item. (1) import_economics + economy_import moved to domains/ledger/ (the map renamed db to ledger), generator_sources.py updated in the same commit, regen run, stamp verified: T-1289. (2) generate_brands kept as a subroutine of the importer, and the relationship is now tighter: cargo_binary rebuilds a stale binary, where before a Rust edit could be stamped over output from the old binary (fix commit e3daf561d). (3) schema_version.py moved with the ledger; canvas_sources.py stays a top-level registry module per the domain map, and check-canvas-version still resolves it. (4) planet-gen became domains/atlas/planet (T-1288); the one-time importers stayed unstamped. (5) garment-fit: 22 payloads to scripts/blender (T-1273) + make_logo to domains/character (T-1290). (6) wiki_sync + connectors to domains/wiki and domains/assets; ensure_venv is gone (T-1290). (7) test_sim_determinism and test_oasis_ring_scaling moved to tooling/test_planet_*.py and pass; globe renders were pixel-identical across the T-1274 cleanup. Nothing numeric changed: every byte-comparable generator output was checked identical before/after.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:16.844', '2026-09-23 18:16:36.730', NULL, 'c49d623a9c7d0357d7c650ed17db5c3f', 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 ('06G1S3DTRHSBYJS55NDT9YR4R0', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E4 — The gate family and the push hook, at output parity', 'The highest-frequency, highest-friction surface, and the one with a hard number attached. Baseline measured 2026-08-20: check-client-version 20 ms, check-dataflow-graph 38 ms, check-canvas-version 46 ms — about 104 ms for the three unconditional checks; check-systems-db-stamp runs only when systems.db is in the push. The ported gates must not exceed that. Pieces that become tickets: (1) port the four checks into domains/check/ — client-version, canvas-version, dataflow-graph, systems-db-stamp; (2) port validate-content, validate-checklist, validate-ron, check-fact-ids into domains/validate/; (3) rewire .config/hooks/pre-push to call reach, keeping the fail_check by-name reporting that hook deliberately has; (4) prove each gate still FAILS — a check that explains itself and exits 0 silently disables its own gate, which is exactly the clide failure recorded in D-263; test each one against a deliberately broken tree, not just a clean one; (5) the wall-clock assertion that turns the budget into a test; (6) confirm domains/check/ imports no pydantic — 87 ms against a 20 ms check is a 3.5x regression bought for nothing, four times per push. Depends on E1 (lazy registration) and E2 (the error contract) being real first.
AMENDED 2026-08-20 — the 104 ms ceiling in the description above is WITHDRAWN. D-263 was amended the same day: the acceptance criterion is OUTPUT PARITY, not timing parity. A ported check must produce the same output and the same exit code as the script it replaces; it is not required to be as fast. Reason: the only automatic consumer is the pre-push hook, which on a server or client push already runs cargo test or the gdUnit4 suite (minutes), and on a governance-only push totals about a second — so a few hundred ms is invisible either way. The replacement budget is a ceiling with headroom: a reach invocation should feel instant to a human, under about 250 ms, and the unconditional gate set stays comfortably under a second. Item (5) of this ticket, the wall-clock assertion, is REPLACED by an import-graph assertion — a timing test is flaky on a loaded machine and reports that something got slow rather than what; asserting sys.modules after reach --help names the offender. Item (6), the no-pydantic-in-check rule, is DROPPED — pydantic at 87 ms is unremarkable against a 250 ms ceiling, and the carve-out bought 87 ms at the cost of an exception to the layering.
FROM T-1262 (2026-08-31) — what ''output parity'' means was sharpened while porting the first gate, and it applies to every port in this epic. Byte-for-byte parity is IMPOSSIBLE under the streaming model and should not be attempted: D-263 puts the verdict on stderr as part of the event stream, while the scripts being replaced write their success line to stdout. Matching both would mean abandoning streaming or special-casing each ported gate. The enforceable definition, now in D-263: (a) EXIT CODES MATCH EXACTLY — the hook gates on this and it is the only part a caller can act on programmatically; (b) NO FACT IS LOST — every version number, path and reason the old message carried must appear in the new one, since a migration that silently drops a detail makes the failure harder to fix than before; (c) FAILURES NAME A REMEDY as a structured field, which the old scripts did only in prose. In text mode the success line is in fact byte-identical; only the stream differs. tooling/test_check_parity.py is the working pattern to copy for the other three checks — it builds a throwaway fixture repo (sentinel project.yaml plus client/project.godot), copies the OLD script into it so its __file__-relative root resolves there, points the new command at the same fixture via SR_REPO_ROOT, and compares. Note the asymmetry it exposes: the old scripts have NO root override, which is precisely why they are hard to test and part of why the port is worth doing.
DONE 2026-09-23, audited against the items rather than assumed. (1)+(2) the gate ports landed as T-1281 (check) and T-1282 (validate). (3) .config/hooks/pre-push and pre-commit invoke only `reach --no-input …` for every gate (plus ruff and json.tool); no tooling/ script path survives in any hook. (4) gates proven to FAIL: test_check.py runs 19 cases across five gates, failures included; this session the canvas-version gate did its job for real, rejecting a push of a comment-only change to a registered file (hence 0.4.14). (5) replaced by the import-graph assertion, which exists: test_lazy_domains.py asserts that after `reach --help` no tooling.domains module and none of rich/pygments/numpy/scipy/pydantic/PIL is imported, with a positive control. (6) dropped by the 2026-08-20 amendment.', 'done', 'high', NULL, NULL, 'D-263', '2026-08-20 00:24:05.572', '2026-09-23 18:16:37.317', NULL, '4d2efdd656d6c8e09b09f1814417aeb1', 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 ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E5 — Generators and numerics, without breaking the systems.db stamp', 'The riskiest epic, and the one with a hard sequencing constraint. tooling/generator_sources.py SHAs the concatenated bytes of the generator sources SORTED BY PATH, so renaming a file changes the stamp even when its content is byte-identical. The relocation must therefore land as ONE commit — registry paths updated, make regen-db run, stamp verified — never split across pushes, or the pre-push gate rejects an intermediate state that is in fact correct. Pieces that become tickets: (1) move import_economics.py and the economy_import package into domains/db/, update generator_sources.py in the same change, regen and verify the stamp; (2) the generate_brands Rust binary is a SUBROUTINE of the Python importer, not an independent generator — keep that relationship intact; (3) canvas_sources.py and schema_version.py move with their consumers, and check-canvas-version must keep resolving the registry; (4) planet-gen into domains/planet/ — import_heightmaps, import_province_boundaries, planet_simulation, scaffold_bodies, render_heightmap; these are one-time build imports baked into the committed DB, intentionally unstamped, and they lean on numpy/scipy/PIL; (5) garment-fit into domains/garment/ with its 23 blender_author_* files going to scripts/blender/ per the carve-out; (6) db/wiki_sync.py and the audio/image/trellis connectors, which resolve a venv explicitly today in tooling/db/common.py and should stop needing to; (7) test_sim_determinism and test_oasis_ring_scaling keep passing — they are the guard that the numerics did not move under us. NOTHING here is rewritten. Code is relocated and re-fronted; no numerical behaviour changes, which is precisely why Rust was rejected.
DONE 2026-09-23 — delivered by the E3 ports, item by item. (1) import_economics + economy_import moved to domains/ledger/ (the map renamed db to ledger), generator_sources.py updated in the same commit, regen run, stamp verified: T-1289. (2) generate_brands kept as a subroutine of the importer, and the relationship is now tighter: cargo_binary rebuilds a stale binary, where before a Rust edit could be stamped over output from the old binary (fix commit e3daf561d). (3) schema_version.py moved with the ledger; canvas_sources.py stays a top-level registry module per the domain map, and check-canvas-version still resolves it. (4) planet-gen became domains/atlas/planet (T-1288); the one-time importers stayed unstamped. (5) garment-fit: 22 payloads to scripts/blender (T-1273) + make_logo to domains/character (T-1290). (6) wiki_sync + connectors to domains/wiki and domains/assets; ensure_venv is gone (T-1290). (7) test_sim_determinism and test_oasis_ring_scaling moved to tooling/test_planet_*.py and pass; globe renders were pixel-identical across the T-1274 cleanup. Nothing numeric changed: every byte-comparable generator output was checked identical before/after.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:16.844', '2026-09-23 18:16:37.328', NULL, 'c67cf493bc8411754fdd7d1e7871440f', 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;