feat(config): T-1281 — dataflow-graph and systems-db-stamp join the check domain
Both were already Python, so these are moves rather than rewrites, and both produce byte-identical output to their originals on the live tree with the same exit codes. The E402 debt evaporated on contact, which is the first concrete evidence for T-1274's premise. check-systems-db-stamp reached generator_sources through a sys.path.insert and a noqa suppression, because tooling/ was not a package. It now imports as `from tooling import generator_sources` — no hack, no suppression. The stamp gate's six failure modes are preserved as a StampState enum rather than collapsed into pass/fail, because they carry different remedies and one carries a different exit code: UNSTAMPED exits 2 while every other failure exits 1, and the pre-push hook has relied on that distinction since T-857. One deliberate behavioural difference, flagged rather than hidden: the old stamp script was silent on success unless given --verbose, and the new one always prints its verdict. No fact is lost, so parity holds, and it makes the gate consistent with client-version and dataflow-graph which both always print — the old script was the odd one out. Its per-command --verbose gives way to the global one, which is the consolidation this initiative is for. Also corrects a claim in the ticket itself: check-dataflow-graph.py does not parse git output, it globs the filesystem. Only check-canvas-version parses git, so only that fixture needs a real repo. Still open and recorded as such: check-canvas-version, and parity tests for these two — both were verified side by side on the live tree, which proves the happy path and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2133,3 +2133,23 @@ WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T
|
||||
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.', NULL, '2026-08-31 15:48:10', '2026-08-31 15:48:10.137', '2026-08-31 15:48:10.137', NULL, '62fd6c01a7403e4134ec19473d8893fa', 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 ('06G5GWCTS89VSST07DFVBVG064', 'description', 'Finish what T-1262 started. domains/check/ currently has one verb (client-version); the other four gates join it: check-canvas-version, check-systems-db-stamp, check-fact-ids and check-dataflow-graph.py. Per the domain map, canvas_sources.py and generator_sources.py move alongside as module-level REGISTRIES, not verbs — nothing types them, and putting them in reach --help would answer a question nobody has. schema_version.py goes to the db domain where it is defined; check imports it, which is what a service layer is for. STANDARD PORT ACCEPTANCE, applying to every per-domain ticket under this epic: (a) service.py holds the logic and is transport-agnostic — no printing, no sys.exit, no typer; (b) router.py holds no logic and every command wears @command; (c) a parity test per gate, copying the pattern in tooling/test_check_parity.py, which builds a throwaway fixture repo and runs BOTH implementations — exit codes must match exactly and no fact the old message carried may be lost; (d) the old scripts are RETIRED once parity passes, not left alongside; (e) that domain''s tooling make targets are retired too rather than left as wrappers, per the D-263 make/reach split — build orchestration stays with make. Note check-dataflow-graph.py and check-canvas-version both parse git output, so their fixtures need a git repo rather than a bare directory; that is the one thing here harder than client-version was.
|
||||
|
||||
PROGRESS 2026-08-31 — fact-ids ported, the first bash-to-Python rewrite under the D-263 decision. It was 89 lines of grep/sed pipeline; it is now service.fact_ids() returning a FactIdCheck, with a router that renders it. PARITY IS EXACT ON THE LIVE TREE: both implementations print ''check-fact-ids: OK — 6 references validated against 61 canonical facts'' and exit 0. The matching COUNTS are the real evidence — if the line-matching regex differed from the grep chain even slightly, 6 and 61 would diverge. Kept line-matched rather than YAML-parsed on purpose: parsing properly would change which lines count (anchors, merge keys, multi-document files would start contributing ids the old check never saw), which is a different check wearing the same name. Three parity cases added to test_check_parity.py — ok, unknown fact_id, and the advisory mode where catalogs are unpopulated. Proven to fail: removing the entity-attributes.yaml exclusion was caught, and interestingly by the ADVISORY case rather than the exclusion assertion — including that file made the catalog non-empty, so the new implementation went to enforcing mode while the old stayed advisory. A real behavioural divergence, caught by exit code. RETIREMENT DEFERRED to the end of this ticket, and there is a reason worth recording. The rule is retire per DOMAIN, and three gates remain. It also resolves a tension: the parity test COPIES the old script into its fixture, so deleting the script early would delete the test''s own subject. A parity test is scaffolding with a defined lifetime — when the old script goes, its parity case goes with it and is replaced by a unit test of the new behaviour. Same will apply to every port under T-1250. NOTE the pre-commit hook calls tooling/check-fact-ids directly, so retirement means editing that hook — and a mistake there is felt on every commit rather than every push. Do it together with validate-content (T-1282), which is in the same hook, so it is edited once.', 'Finish what T-1262 started. domains/check/ currently has one verb (client-version); the other four gates join it: check-canvas-version, check-systems-db-stamp, check-fact-ids and check-dataflow-graph.py. Per the domain map, canvas_sources.py and generator_sources.py move alongside as module-level REGISTRIES, not verbs — nothing types them, and putting them in reach --help would answer a question nobody has. schema_version.py goes to the db domain where it is defined; check imports it, which is what a service layer is for. STANDARD PORT ACCEPTANCE, applying to every per-domain ticket under this epic: (a) service.py holds the logic and is transport-agnostic — no printing, no sys.exit, no typer; (b) router.py holds no logic and every command wears @command; (c) a parity test per gate, copying the pattern in tooling/test_check_parity.py, which builds a throwaway fixture repo and runs BOTH implementations — exit codes must match exactly and no fact the old message carried may be lost; (d) the old scripts are RETIRED once parity passes, not left alongside; (e) that domain''s tooling make targets are retired too rather than left as wrappers, per the D-263 make/reach split — build orchestration stays with make. Note check-dataflow-graph.py and check-canvas-version both parse git output, so their fixtures need a git repo rather than a bare directory; that is the one thing here harder than client-version was.
|
||||
|
||||
PROGRESS 2026-08-31 — fact-ids ported, the first bash-to-Python rewrite under the D-263 decision. It was 89 lines of grep/sed pipeline; it is now service.fact_ids() returning a FactIdCheck, with a router that renders it. PARITY IS EXACT ON THE LIVE TREE: both implementations print ''check-fact-ids: OK — 6 references validated against 61 canonical facts'' and exit 0. The matching COUNTS are the real evidence — if the line-matching regex differed from the grep chain even slightly, 6 and 61 would diverge. Kept line-matched rather than YAML-parsed on purpose: parsing properly would change which lines count (anchors, merge keys, multi-document files would start contributing ids the old check never saw), which is a different check wearing the same name. Three parity cases added to test_check_parity.py — ok, unknown fact_id, and the advisory mode where catalogs are unpopulated. Proven to fail: removing the entity-attributes.yaml exclusion was caught, and interestingly by the ADVISORY case rather than the exclusion assertion — including that file made the catalog non-empty, so the new implementation went to enforcing mode while the old stayed advisory. A real behavioural divergence, caught by exit code. RETIREMENT DEFERRED to the end of this ticket, and there is a reason worth recording. The rule is retire per DOMAIN, and three gates remain. It also resolves a tension: the parity test COPIES the old script into its fixture, so deleting the script early would delete the test''s own subject. A parity test is scaffolding with a defined lifetime — when the old script goes, its parity case goes with it and is replaced by a unit test of the new behaviour. Same will apply to every port under T-1250. NOTE the pre-commit hook calls tooling/check-fact-ids directly, so retirement means editing that hook — and a mistake there is felt on every commit rather than every push. Do it together with validate-content (T-1282), which is in the same hook, so it is edited once.
|
||||
|
||||
PROGRESS 2026-08-31, second increment — dataflow-graph and systems-db-stamp ported. Both were already Python, so these are moves rather than rewrites, and both produce BYTE-IDENTICAL output to their originals on the live tree: "check-dataflow-graph: OK — 1 diagram(s)" and "check-systems-db-stamp: OK — 1 generator(s) up to date", same exit codes.
|
||||
|
||||
CORRECTION TO THIS TICKET''S OWN NOTE: it says check-dataflow-graph.py parses git output. It does not — it reads .d2 files and globs the filesystem. Only check-canvas-version parses git, so only that one needs a git repo in its fixture.
|
||||
|
||||
THE E402 DEBT EVAPORATED, exactly as T-1274 predicted. check-systems-db-stamp reached generator_sources through sys.path.insert plus a noqa: E402, because tooling/ was not a package. It now imports as `from tooling import generator_sources` with no hack and no suppression. That is the first concrete evidence for T-1274''s premise; the map''s plan to move generator_sources into the db domain still stands, but it is already importable where it sits.
|
||||
|
||||
SIX FAILURE MODES PRESERVED in the stamp port, modelled as a StampState enum rather than collapsed to pass/fail, because they carry different remedies and one carries a different EXIT CODE: UNSTAMPED exits 2 while every other failure exits 1, and the pre-push hook has relied on that distinction since T-857. The states are ok / absent / unstamped / bad_version / conflict / unknown / broken / stale, each with its own remedy string.
|
||||
|
||||
ONE DELIBERATE BEHAVIOURAL DIFFERENCE, flagged rather than hidden: the old stamp script was SILENT on success unless given --verbose; the new one always prints its verdict. That is not a lost fact, so it does not violate the parity definition, and it makes the gate consistent with client-version and dataflow-graph, which both always print. The old script was the odd one out. Its per-command --verbose also disappears in favour of the global one, which is the consolidation the whole initiative is for.
|
||||
|
||||
STILL OPEN on this ticket, and honest about it:
|
||||
- check-canvas-version is not ported. It is the one that genuinely parses git, so its fixture needs a real git repo — the hardest of the four.
|
||||
- NO PARITY TESTS yet for dataflow-graph or systems-db-stamp. Both were verified by running the two implementations side by side on the live tree, which proves the happy path only. The stamp gate''s six failure modes are entirely unexercised, and its fixture is awkward because the old script imports generator_sources from its own directory, so a fixture must copy both files and build a sqlite DB with a meta table.
|
||||
- Retirement (scripts, parity cases, make targets, the pre-commit hook entry for fact-ids) waits for the whole domain, per the per-domain rule.', NULL, '2026-08-31 15:55:15', '2026-08-31 15:55:15.374', '2026-08-31 15:55:15.374', NULL, 'eed8836144005a06232829ab3dfad2e6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -2462,3 +2462,21 @@ WAVE 1 FILED 2026-08-31: T-1281 check (four remaining gates), T-1282 validate, T
|
||||
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.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 15:48:10.137', NULL, '400f022495449b2a3a4136f8aa5ceb71', 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 ('06G5GWCTS89VSST07DFVBVG064', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the check domain — the four remaining gates', 'Finish what T-1262 started. domains/check/ currently has one verb (client-version); the other four gates join it: check-canvas-version, check-systems-db-stamp, check-fact-ids and check-dataflow-graph.py. Per the domain map, canvas_sources.py and generator_sources.py move alongside as module-level REGISTRIES, not verbs — nothing types them, and putting them in reach --help would answer a question nobody has. schema_version.py goes to the db domain where it is defined; check imports it, which is what a service layer is for. STANDARD PORT ACCEPTANCE, applying to every per-domain ticket under this epic: (a) service.py holds the logic and is transport-agnostic — no printing, no sys.exit, no typer; (b) router.py holds no logic and every command wears @command; (c) a parity test per gate, copying the pattern in tooling/test_check_parity.py, which builds a throwaway fixture repo and runs BOTH implementations — exit codes must match exactly and no fact the old message carried may be lost; (d) the old scripts are RETIRED once parity passes, not left alongside; (e) that domain''s tooling make targets are retired too rather than left as wrappers, per the D-263 make/reach split — build orchestration stays with make. Note check-dataflow-graph.py and check-canvas-version both parse git output, so their fixtures need a git repo rather than a bare directory; that is the one thing here harder than client-version was.
|
||||
|
||||
PROGRESS 2026-08-31 — fact-ids ported, the first bash-to-Python rewrite under the D-263 decision. It was 89 lines of grep/sed pipeline; it is now service.fact_ids() returning a FactIdCheck, with a router that renders it. PARITY IS EXACT ON THE LIVE TREE: both implementations print ''check-fact-ids: OK — 6 references validated against 61 canonical facts'' and exit 0. The matching COUNTS are the real evidence — if the line-matching regex differed from the grep chain even slightly, 6 and 61 would diverge. Kept line-matched rather than YAML-parsed on purpose: parsing properly would change which lines count (anchors, merge keys, multi-document files would start contributing ids the old check never saw), which is a different check wearing the same name. Three parity cases added to test_check_parity.py — ok, unknown fact_id, and the advisory mode where catalogs are unpopulated. Proven to fail: removing the entity-attributes.yaml exclusion was caught, and interestingly by the ADVISORY case rather than the exclusion assertion — including that file made the catalog non-empty, so the new implementation went to enforcing mode while the old stayed advisory. A real behavioural divergence, caught by exit code. RETIREMENT DEFERRED to the end of this ticket, and there is a reason worth recording. The rule is retire per DOMAIN, and three gates remain. It also resolves a tension: the parity test COPIES the old script into its fixture, so deleting the script early would delete the test''s own subject. A parity test is scaffolding with a defined lifetime — when the old script goes, its parity case goes with it and is replaced by a unit test of the new behaviour. Same will apply to every port under T-1250. NOTE the pre-commit hook calls tooling/check-fact-ids directly, so retirement means editing that hook — and a mistake there is felt on every commit rather than every push. Do it together with validate-content (T-1282), which is in the same hook, so it is edited once.
|
||||
|
||||
PROGRESS 2026-08-31, second increment — dataflow-graph and systems-db-stamp ported. Both were already Python, so these are moves rather than rewrites, and both produce BYTE-IDENTICAL output to their originals on the live tree: "check-dataflow-graph: OK — 1 diagram(s)" and "check-systems-db-stamp: OK — 1 generator(s) up to date", same exit codes.
|
||||
|
||||
CORRECTION TO THIS TICKET''S OWN NOTE: it says check-dataflow-graph.py parses git output. It does not — it reads .d2 files and globs the filesystem. Only check-canvas-version parses git, so only that one needs a git repo in its fixture.
|
||||
|
||||
THE E402 DEBT EVAPORATED, exactly as T-1274 predicted. check-systems-db-stamp reached generator_sources through sys.path.insert plus a noqa: E402, because tooling/ was not a package. It now imports as `from tooling import generator_sources` with no hack and no suppression. That is the first concrete evidence for T-1274''s premise; the map''s plan to move generator_sources into the db domain still stands, but it is already importable where it sits.
|
||||
|
||||
SIX FAILURE MODES PRESERVED in the stamp port, modelled as a StampState enum rather than collapsed to pass/fail, because they carry different remedies and one carries a different EXIT CODE: UNSTAMPED exits 2 while every other failure exits 1, and the pre-push hook has relied on that distinction since T-857. The states are ok / absent / unstamped / bad_version / conflict / unknown / broken / stale, each with its own remedy string.
|
||||
|
||||
ONE DELIBERATE BEHAVIOURAL DIFFERENCE, flagged rather than hidden: the old stamp script was SILENT on success unless given --verbose; the new one always prints its verdict. That is not a lost fact, so it does not violate the parity definition, and it makes the gate consistent with client-version and dataflow-graph, which both always print. The old script was the odd one out. Its per-command --verbose also disappears in favour of the global one, which is the consolidation the whole initiative is for.
|
||||
|
||||
STILL OPEN on this ticket, and honest about it:
|
||||
- check-canvas-version is not ported. It is the one that genuinely parses git, so its fixture needs a real git repo — the hardest of the four.
|
||||
- NO PARITY TESTS yet for dataflow-graph or systems-db-stamp. Both were verified by running the two implementations side by side on the live tree, which proves the happy path only. The stamp gate''s six failure modes are entirely unexercised, and its fixture is awkward because the old script imports generator_sources from its own directory, so a fixture must copy both files and build a sqlite DB with a meta table.
|
||||
- Retirement (scripts, parity cases, make targets, the pre-commit hook entry for fact-ids) waits for the whole domain, per the per-domain rule.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-08-31 15:30:35.338', '2026-08-31 15:55:15.374', NULL, '86fc272377f0603e44b7b5af742a5f90', 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;
|
||||
|
||||
Reference in New Issue
Block a user