feat(config): T-1282 — the validate domain, and a move that broke a root

reach validate content / checklist / ron / name-collisions. The three old
scripts are retired, their make targets with them.

Print statements go through the logging sink rather than a collector. The
validators emit their findings as console events as they run, so a long content
validation streams instead of going quiet and dumping at the end — the message
strings and their order are unchanged, only the destination. That also
satisfies the conformance rule forbidding print() in the package, which is what
forced the question.

validate-ron was three languages deep: bash dispatching on a flag, a Python
heredoc doing collision detection, cargo run for schema validation. Logic
embedded in a shell string cannot be imported, tested, or found by anything
that indexes Python, so it became Python; the cargo call became a guarded exec.
It also split into two verbs, because --check-name-collisions answered a
different question from the default path: whether the SET of cultures is
coherent, versus whether ONE file is well-formed.

The move broke something, quietly, which is the point of doing these one at a
time. validate-checklist computed ROOT as Path(__file__).parent.parent — the
repo root while it lived at tooling/validate-checklist, and tooling/domains
once moved. Both its schema and gauntlet paths silently repointed at nothing,
the gauntlet directory "did not exist", and it reported success having checked
zero files. Caught by running it beside the original: old exit 1, new exit 0.
Now config.repo_root(), and load_schema raises ReachError instead of calling
sys.exit, which a service must not do.

Parity on the live tree: content reproduces the original byte for byte
including its counts, name-collisions likewise. Tests pin what those runs
cannot reach — the detection path, since the repo currently has no collisions,
and the argument errors.

Two things found and left alone: validate-content FAILS on the live tree with
13 missing schemas, pre-existing and unrelated to this port; and the ticket's
claim that validate-content sits in the pre-commit hook is wrong — that hook
runs only check-fact-ids and pql decisions validate, so there was no shared
edit to coordinate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 12:48:56 +02:00
co-authored by Claude Opus 5
parent cb5d3f1335
commit 7f20bd303b
15 changed files with 798 additions and 252 deletions
+128
View File
@@ -2292,3 +2292,131 @@ PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report bo
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.', NULL, '2026-08-31 18:07:14', '2026-08-31 18:07:14.123', '2026-08-31 18:07:14.123', NULL, 'eacbb029d58e1ea83aae262aed0c8e89', 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.
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.
PROGRESS 2026-08-31, third increment canvas-version ported. ALL FOUR GATES ARE NOW IN THE DOMAIN: client-version, canvas-version, systems-db-stamp, dataflow-graph, fact-ids. Byte-identical output to the original on the live tree.
FIRST REAL CONSUMER OF core/process.run. The git calls go through the guarded exec with check=False, deliberately: a git failure here is not an error to report but a signal that there is nothing to compare a fresh clone with no remote is a legitimate state, not a broken one. The argv-list and missing-binary guards still apply.
TWO SKIPS KEPT DISTINCT FROM A PASS. NO_BASE (base ref absent) and DIFF_FAILED both exit 0, as does CLEAN (nothing relevant changed) but only CLEAN means the gate actually looked at something. Collapsing them would hide a gate that had silently stopped running, which for this particular check is the exact failure mode it exists to prevent.
A SECOND RICH PATH FOUND, and it is worth knowing about. typer''s PRETTY-EXCEPTION handler renders unhandled errors as box-art with syntax highlighting, and it is a DIFFERENT mechanism from rich_markup_mode setting one does nothing about the other. It surfaced when a NameError produced a full-width box-drawn traceback. That is the same log pollution rich_markup_mode=None was set to prevent (T-1259), arriving through another door, and landing in the worst possible place: a hook log at the moment something has already gone wrong. pretty_exceptions_enable=False now set on both the root app and every domain built by cli.domain().
THE EXISTING TEST MOVED WITH THE CODE. tooling/test_canvas_version_check.py loaded the extensionless script through a SourceFileLoader and reached canvas_sources via sys.path.insert both only because tooling/ was not an importable package. It now does `from tooling.domains.check import service as check` and `from tooling.canvas_sources import relative_paths`. Second instance of the E402/sys.path debt evaporating on contact (T-1274). The test passes unchanged in what it ASSERTS, which is the point: diff_has_version_bump was kept pure in the port precisely so those six properties still hold without constructing git history.
A BUG CAUGHT BY RUNNING IT: the check router had dropped its `import typer` in T-1267 when it moved to cli.domain(), and canvas-version needs typer.Option for --base/--head. Restored.
STILL OPEN on this ticket:
- No parity tests for canvas-version, dataflow-graph or systems-db-stamp. All three were verified side by side on the live tree, which proves the happy path only. canvas-version''s failure path is the valuable one and needs a git fixture a temp repo with a commit touching a registry file and no version bump.
- Retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
PROGRESS 2026-08-31, fourth increment canvas-version parity, five cases, including its failure path.
A FIXTURE REPO DOES NOT WORK FOR THIS GATE, and finding out why exposed a real inconsistency in the port. canvas_sources.relative_paths() globs from a __file__-derived REPO_ROOT, so under SR_REPO_ROOT the new service would DIFF the fixture while GLOBBING the real tree. The service resolves git through config.repo_root() but the registry does not the two disagree about which repo is being checked. Harmless in production (they are the same repo) but it makes fixture-based parity impossible, and it is the same asymmetry the domain map already noted about old scripts having no root override, showing up inside the new code.
NOT FIXED HERE: making it dynamic means restructuring six module-level path constants in a module the old script also imports, which is more churn than this ticket should carry. Filed as a follow-up consideration for when canvas_sources moves into the domain per the map.
SO PARITY USES REAL HISTORY INSTEAD, which is better anyway: both implementations see byte-identical input, nothing is mutated, and no fixture can drift from the thing it models. Five cases:
- canvas-needs-bump: 4e503c356 (T-1237 one-course-per-river) changed canvas generation, did NOT bump. Both exit 1.
- canvas-needs-bump-2: 566b56651 (T-1194 relief/vegetation texture) same. Both exit 1.
- canvas-bumped: 9b146f9e1 changed canvas AND bumped. Both exit 0.
- canvas-clean: 6b31111cd governance only. Both exit 0.
- canvas-no-base: a nonexistent base ref a SKIP, not a failure. Both exit 0.
The two failure cases are GENUINE historical instances of the regression this gate exists to catch, both bumped after the fact. That is the history that produced the check, used as its own test.
THE TEST ALSO ASSERTS NO CHANGED FILE IS DROPPED from the failure message. The list of touched files is the actionable half a failure saying "something changed" without saying what leaves the reader to re-derive the intersection by hand.
PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report both the exit-code divergence AND all three omitted filenames by name.
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.', '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.
PROGRESS 2026-08-31, third increment canvas-version ported. ALL FOUR GATES ARE NOW IN THE DOMAIN: client-version, canvas-version, systems-db-stamp, dataflow-graph, fact-ids. Byte-identical output to the original on the live tree.
FIRST REAL CONSUMER OF core/process.run. The git calls go through the guarded exec with check=False, deliberately: a git failure here is not an error to report but a signal that there is nothing to compare a fresh clone with no remote is a legitimate state, not a broken one. The argv-list and missing-binary guards still apply.
TWO SKIPS KEPT DISTINCT FROM A PASS. NO_BASE (base ref absent) and DIFF_FAILED both exit 0, as does CLEAN (nothing relevant changed) but only CLEAN means the gate actually looked at something. Collapsing them would hide a gate that had silently stopped running, which for this particular check is the exact failure mode it exists to prevent.
A SECOND RICH PATH FOUND, and it is worth knowing about. typer''s PRETTY-EXCEPTION handler renders unhandled errors as box-art with syntax highlighting, and it is a DIFFERENT mechanism from rich_markup_mode setting one does nothing about the other. It surfaced when a NameError produced a full-width box-drawn traceback. That is the same log pollution rich_markup_mode=None was set to prevent (T-1259), arriving through another door, and landing in the worst possible place: a hook log at the moment something has already gone wrong. pretty_exceptions_enable=False now set on both the root app and every domain built by cli.domain().
THE EXISTING TEST MOVED WITH THE CODE. tooling/test_canvas_version_check.py loaded the extensionless script through a SourceFileLoader and reached canvas_sources via sys.path.insert both only because tooling/ was not an importable package. It now does `from tooling.domains.check import service as check` and `from tooling.canvas_sources import relative_paths`. Second instance of the E402/sys.path debt evaporating on contact (T-1274). The test passes unchanged in what it ASSERTS, which is the point: diff_has_version_bump was kept pure in the port precisely so those six properties still hold without constructing git history.
A BUG CAUGHT BY RUNNING IT: the check router had dropped its `import typer` in T-1267 when it moved to cli.domain(), and canvas-version needs typer.Option for --base/--head. Restored.
STILL OPEN on this ticket:
- No parity tests for canvas-version, dataflow-graph or systems-db-stamp. All three were verified side by side on the live tree, which proves the happy path only. canvas-version''s failure path is the valuable one and needs a git fixture a temp repo with a commit touching a registry file and no version bump.
- Retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
PROGRESS 2026-08-31, fourth increment canvas-version parity, five cases, including its failure path.
A FIXTURE REPO DOES NOT WORK FOR THIS GATE, and finding out why exposed a real inconsistency in the port. canvas_sources.relative_paths() globs from a __file__-derived REPO_ROOT, so under SR_REPO_ROOT the new service would DIFF the fixture while GLOBBING the real tree. The service resolves git through config.repo_root() but the registry does not the two disagree about which repo is being checked. Harmless in production (they are the same repo) but it makes fixture-based parity impossible, and it is the same asymmetry the domain map already noted about old scripts having no root override, showing up inside the new code.
NOT FIXED HERE: making it dynamic means restructuring six module-level path constants in a module the old script also imports, which is more churn than this ticket should carry. Filed as a follow-up consideration for when canvas_sources moves into the domain per the map.
SO PARITY USES REAL HISTORY INSTEAD, which is better anyway: both implementations see byte-identical input, nothing is mutated, and no fixture can drift from the thing it models. Five cases:
- canvas-needs-bump: 4e503c356 (T-1237 one-course-per-river) changed canvas generation, did NOT bump. Both exit 1.
- canvas-needs-bump-2: 566b56651 (T-1194 relief/vegetation texture) same. Both exit 1.
- canvas-bumped: 9b146f9e1 changed canvas AND bumped. Both exit 0.
- canvas-clean: 6b31111cd governance only. Both exit 0.
- canvas-no-base: a nonexistent base ref a SKIP, not a failure. Both exit 0.
The two failure cases are GENUINE historical instances of the regression this gate exists to catch, both bumped after the fact. That is the history that produced the check, used as its own test.
THE TEST ALSO ASSERTS NO CHANGED FILE IS DROPPED from the failure message. The list of touched files is the actionable half a failure saying "something changed" without saying what leaves the reader to re-derive the intersection by hand.
PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report both the exit-code divergence AND all three omitted filenames by name.
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
DONE 2026-08-31. All five gates ported, tested against their failure paths, old scripts retired, hooks and make targets rewired, and both hooks exercised for real.
FINAL STATE: reach check client-version / canvas-version / systems-db-stamp / dataflow-graph / fact-ids. tooling/check-* no longer exists.
VERIFIED BY THE HOOKS THEMSELVES, not by reading them. The retirement commit ran the rewired PRE-COMMIT hook, which executed reach check fact-ids and emitted its verdict. The push ran the rewired PRE-PUSH hook, which executed client-version, canvas-version and dataflow-graph through reach and passed. That is the only evidence that matters here a hook edit verified by inspection is a hook edit that has not been tested.
TESTS: tooling/test_check.py, 19 cases across five gates, every gate exercising at least one failure. Renamed from test_check_parity.py because its subjects no longer exist. Every case began as a parity case running both implementations; that evidence is recorded above and in the commits. With the scripts gone there is nothing to compare against, so the assertions become the spec. A parity test is scaffolding with a defined lifetime keeping one after its subject is deleted would mean keeping the subject alive to be compared with, which is the opposite of a migration.
TWO GATES COULD NOT BE FIXTURE-TESTED AGAINST THEIR ORIGINALS, and both reasons are findings rather than obstacles:
- canvas-version: canvas_sources globs from a __file__-derived root while the service resolves git through config.repo_root(), so a fixture would DIFF one tree and GLOB another. Real history used instead five ranges including two GENUINE instances of the regression this gate exists to catch (T-1237 and T-1194, both bumped after the fact). The history that produced the check, used as its own fixture.
- systems-db-stamp: generator_sources raises at IMPORT time when the economy-db tree is absent, so the old script died at line 39 in every fixture, before any logic. Its failure modes were never testable in isolation. The ported service imports it lazily and only after the absent/unstamped checks, which is precisely why absent/unstamped/empty/unknown/bad-version are reachable now and were not before. STALE still needs the full registered source set and is covered by the live repo on every push.
MAKE TARGETS RETIRED, NOT WRAPPED, per the D-263 split, with the mapping left as a comment where they used to be so the next reader finds the answer rather than an absence. Wrapping would leave two ways to invoke each gate and reach --help would stop being the answer to "what tooling exists" while the Makefile stayed a competing index. pre-pr-validate and pre-pr-content keep their orchestration role and drop the individual target dependency.
DOCS: CLAUDE.md and docs/DEVOPS.md updated to name the reach verbs. Sprint archives and workshop notes still reference the old paths and are deliberately left alone they record what was true when written, and rewriting history to match the present is how a record stops being evidence.
NET: 1040 lines deleted, 399 added.', NULL, '2026-08-31 18:42:59', '2026-08-31 18:42:59.700', '2026-08-31 18:42:59.700', NULL, '43a455cdc2ed0f006229bf85b97cff0b', 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', 'status', 'in_progress', 'done', NULL, '2026-08-31 18:43:00', '2026-08-31 18:43:00.154', '2026-08-31 18:43:00.154', NULL, 'a1e28007b5eaeed5eb27e74a98927937', 2) ON CONFLICT(hash) DO NOTHING;
@@ -0,0 +1,2 @@
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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'status', 'backlog', 'in_progress', NULL, '2026-09-01 15:22:13', '2026-09-01 15:22:13.360', '2026-09-01 15:22:13.360', NULL, '524f353821f4dc123ec999ca0e84f25e', 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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'status', 'in_progress', 'in_progress', NULL, '2026-09-01 15:22:27', '2026-09-01 15:22:27.856', '2026-09-01 15:22:27.856', NULL, '3c5c1770ada99b9f3c0822537836737e', 2) ON CONFLICT(hash) DO NOTHING;
+146
View File
@@ -2569,3 +2569,149 @@ PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report bo
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-08-31 15:30:35.338', '2026-08-31 18:07:14.123', NULL, 'b6d293132ca2394ec8e11441243d174f', 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.
PROGRESS 2026-08-31, third increment canvas-version ported. ALL FOUR GATES ARE NOW IN THE DOMAIN: client-version, canvas-version, systems-db-stamp, dataflow-graph, fact-ids. Byte-identical output to the original on the live tree.
FIRST REAL CONSUMER OF core/process.run. The git calls go through the guarded exec with check=False, deliberately: a git failure here is not an error to report but a signal that there is nothing to compare a fresh clone with no remote is a legitimate state, not a broken one. The argv-list and missing-binary guards still apply.
TWO SKIPS KEPT DISTINCT FROM A PASS. NO_BASE (base ref absent) and DIFF_FAILED both exit 0, as does CLEAN (nothing relevant changed) but only CLEAN means the gate actually looked at something. Collapsing them would hide a gate that had silently stopped running, which for this particular check is the exact failure mode it exists to prevent.
A SECOND RICH PATH FOUND, and it is worth knowing about. typer''s PRETTY-EXCEPTION handler renders unhandled errors as box-art with syntax highlighting, and it is a DIFFERENT mechanism from rich_markup_mode setting one does nothing about the other. It surfaced when a NameError produced a full-width box-drawn traceback. That is the same log pollution rich_markup_mode=None was set to prevent (T-1259), arriving through another door, and landing in the worst possible place: a hook log at the moment something has already gone wrong. pretty_exceptions_enable=False now set on both the root app and every domain built by cli.domain().
THE EXISTING TEST MOVED WITH THE CODE. tooling/test_canvas_version_check.py loaded the extensionless script through a SourceFileLoader and reached canvas_sources via sys.path.insert both only because tooling/ was not an importable package. It now does `from tooling.domains.check import service as check` and `from tooling.canvas_sources import relative_paths`. Second instance of the E402/sys.path debt evaporating on contact (T-1274). The test passes unchanged in what it ASSERTS, which is the point: diff_has_version_bump was kept pure in the port precisely so those six properties still hold without constructing git history.
A BUG CAUGHT BY RUNNING IT: the check router had dropped its `import typer` in T-1267 when it moved to cli.domain(), and canvas-version needs typer.Option for --base/--head. Restored.
STILL OPEN on this ticket:
- No parity tests for canvas-version, dataflow-graph or systems-db-stamp. All three were verified side by side on the live tree, which proves the happy path only. canvas-version''s failure path is the valuable one and needs a git fixture a temp repo with a commit touching a registry file and no version bump.
- Retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
PROGRESS 2026-08-31, fourth increment canvas-version parity, five cases, including its failure path.
A FIXTURE REPO DOES NOT WORK FOR THIS GATE, and finding out why exposed a real inconsistency in the port. canvas_sources.relative_paths() globs from a __file__-derived REPO_ROOT, so under SR_REPO_ROOT the new service would DIFF the fixture while GLOBBING the real tree. The service resolves git through config.repo_root() but the registry does not the two disagree about which repo is being checked. Harmless in production (they are the same repo) but it makes fixture-based parity impossible, and it is the same asymmetry the domain map already noted about old scripts having no root override, showing up inside the new code.
NOT FIXED HERE: making it dynamic means restructuring six module-level path constants in a module the old script also imports, which is more churn than this ticket should carry. Filed as a follow-up consideration for when canvas_sources moves into the domain per the map.
SO PARITY USES REAL HISTORY INSTEAD, which is better anyway: both implementations see byte-identical input, nothing is mutated, and no fixture can drift from the thing it models. Five cases:
- canvas-needs-bump: 4e503c356 (T-1237 one-course-per-river) changed canvas generation, did NOT bump. Both exit 1.
- canvas-needs-bump-2: 566b56651 (T-1194 relief/vegetation texture) same. Both exit 1.
- canvas-bumped: 9b146f9e1 changed canvas AND bumped. Both exit 0.
- canvas-clean: 6b31111cd governance only. Both exit 0.
- canvas-no-base: a nonexistent base ref a SKIP, not a failure. Both exit 0.
The two failure cases are GENUINE historical instances of the regression this gate exists to catch, both bumped after the fact. That is the history that produced the check, used as its own test.
THE TEST ALSO ASSERTS NO CHANGED FILE IS DROPPED from the failure message. The list of touched files is the actionable half a failure saying "something changed" without saying what leaves the reader to re-derive the intersection by hand.
PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report both the exit-code divergence AND all three omitted filenames by name.
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
DONE 2026-08-31. All five gates ported, tested against their failure paths, old scripts retired, hooks and make targets rewired, and both hooks exercised for real.
FINAL STATE: reach check client-version / canvas-version / systems-db-stamp / dataflow-graph / fact-ids. tooling/check-* no longer exists.
VERIFIED BY THE HOOKS THEMSELVES, not by reading them. The retirement commit ran the rewired PRE-COMMIT hook, which executed reach check fact-ids and emitted its verdict. The push ran the rewired PRE-PUSH hook, which executed client-version, canvas-version and dataflow-graph through reach and passed. That is the only evidence that matters here a hook edit verified by inspection is a hook edit that has not been tested.
TESTS: tooling/test_check.py, 19 cases across five gates, every gate exercising at least one failure. Renamed from test_check_parity.py because its subjects no longer exist. Every case began as a parity case running both implementations; that evidence is recorded above and in the commits. With the scripts gone there is nothing to compare against, so the assertions become the spec. A parity test is scaffolding with a defined lifetime keeping one after its subject is deleted would mean keeping the subject alive to be compared with, which is the opposite of a migration.
TWO GATES COULD NOT BE FIXTURE-TESTED AGAINST THEIR ORIGINALS, and both reasons are findings rather than obstacles:
- canvas-version: canvas_sources globs from a __file__-derived root while the service resolves git through config.repo_root(), so a fixture would DIFF one tree and GLOB another. Real history used instead five ranges including two GENUINE instances of the regression this gate exists to catch (T-1237 and T-1194, both bumped after the fact). The history that produced the check, used as its own fixture.
- systems-db-stamp: generator_sources raises at IMPORT time when the economy-db tree is absent, so the old script died at line 39 in every fixture, before any logic. Its failure modes were never testable in isolation. The ported service imports it lazily and only after the absent/unstamped checks, which is precisely why absent/unstamped/empty/unknown/bad-version are reachable now and were not before. STALE still needs the full registered source set and is covered by the live repo on every push.
MAKE TARGETS RETIRED, NOT WRAPPED, per the D-263 split, with the mapping left as a comment where they used to be so the next reader finds the answer rather than an absence. Wrapping would leave two ways to invoke each gate and reach --help would stop being the answer to "what tooling exists" while the Makefile stayed a competing index. pre-pr-validate and pre-pr-content keep their orchestration role and drop the individual target dependency.
DOCS: CLAUDE.md and docs/DEVOPS.md updated to name the reach verbs. Sprint archives and workshop notes still reference the old paths and are deliberately left alone they record what was true when written, and rewriting history to match the present is how a record stops being evidence.
NET: 1040 lines deleted, 399 added.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-08-31 15:30:35.338', '2026-08-31 18:42:59.700', NULL, '0f6e6001ef4474934dd9da781def12e4', 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.
PROGRESS 2026-08-31, third increment canvas-version ported. ALL FOUR GATES ARE NOW IN THE DOMAIN: client-version, canvas-version, systems-db-stamp, dataflow-graph, fact-ids. Byte-identical output to the original on the live tree.
FIRST REAL CONSUMER OF core/process.run. The git calls go through the guarded exec with check=False, deliberately: a git failure here is not an error to report but a signal that there is nothing to compare a fresh clone with no remote is a legitimate state, not a broken one. The argv-list and missing-binary guards still apply.
TWO SKIPS KEPT DISTINCT FROM A PASS. NO_BASE (base ref absent) and DIFF_FAILED both exit 0, as does CLEAN (nothing relevant changed) but only CLEAN means the gate actually looked at something. Collapsing them would hide a gate that had silently stopped running, which for this particular check is the exact failure mode it exists to prevent.
A SECOND RICH PATH FOUND, and it is worth knowing about. typer''s PRETTY-EXCEPTION handler renders unhandled errors as box-art with syntax highlighting, and it is a DIFFERENT mechanism from rich_markup_mode setting one does nothing about the other. It surfaced when a NameError produced a full-width box-drawn traceback. That is the same log pollution rich_markup_mode=None was set to prevent (T-1259), arriving through another door, and landing in the worst possible place: a hook log at the moment something has already gone wrong. pretty_exceptions_enable=False now set on both the root app and every domain built by cli.domain().
THE EXISTING TEST MOVED WITH THE CODE. tooling/test_canvas_version_check.py loaded the extensionless script through a SourceFileLoader and reached canvas_sources via sys.path.insert both only because tooling/ was not an importable package. It now does `from tooling.domains.check import service as check` and `from tooling.canvas_sources import relative_paths`. Second instance of the E402/sys.path debt evaporating on contact (T-1274). The test passes unchanged in what it ASSERTS, which is the point: diff_has_version_bump was kept pure in the port precisely so those six properties still hold without constructing git history.
A BUG CAUGHT BY RUNNING IT: the check router had dropped its `import typer` in T-1267 when it moved to cli.domain(), and canvas-version needs typer.Option for --base/--head. Restored.
STILL OPEN on this ticket:
- No parity tests for canvas-version, dataflow-graph or systems-db-stamp. All three were verified side by side on the live tree, which proves the happy path only. canvas-version''s failure path is the valuable one and needs a git fixture a temp repo with a commit touching a registry file and no version bump.
- Retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
PROGRESS 2026-08-31, fourth increment canvas-version parity, five cases, including its failure path.
A FIXTURE REPO DOES NOT WORK FOR THIS GATE, and finding out why exposed a real inconsistency in the port. canvas_sources.relative_paths() globs from a __file__-derived REPO_ROOT, so under SR_REPO_ROOT the new service would DIFF the fixture while GLOBBING the real tree. The service resolves git through config.repo_root() but the registry does not the two disagree about which repo is being checked. Harmless in production (they are the same repo) but it makes fixture-based parity impossible, and it is the same asymmetry the domain map already noted about old scripts having no root override, showing up inside the new code.
NOT FIXED HERE: making it dynamic means restructuring six module-level path constants in a module the old script also imports, which is more churn than this ticket should carry. Filed as a follow-up consideration for when canvas_sources moves into the domain per the map.
SO PARITY USES REAL HISTORY INSTEAD, which is better anyway: both implementations see byte-identical input, nothing is mutated, and no fixture can drift from the thing it models. Five cases:
- canvas-needs-bump: 4e503c356 (T-1237 one-course-per-river) changed canvas generation, did NOT bump. Both exit 1.
- canvas-needs-bump-2: 566b56651 (T-1194 relief/vegetation texture) same. Both exit 1.
- canvas-bumped: 9b146f9e1 changed canvas AND bumped. Both exit 0.
- canvas-clean: 6b31111cd governance only. Both exit 0.
- canvas-no-base: a nonexistent base ref a SKIP, not a failure. Both exit 0.
The two failure cases are GENUINE historical instances of the regression this gate exists to catch, both bumped after the fact. That is the history that produced the check, used as its own test.
THE TEST ALSO ASSERTS NO CHANGED FILE IS DROPPED from the failure message. The list of touched files is the actionable half a failure saying "something changed" without saying what leaves the reader to re-derive the intersection by hand.
PROVEN TO FAIL: truncating the touched-file set made canvas-needs-bump report both the exit-code divergence AND all three omitted filenames by name.
SHAs are hardcoded because history is immutable, and each case asserts the OLD script still behaves as the case claims so if a rebase ever invalidated a range, the test says so by name rather than silently checking nothing.
STILL OPEN on this ticket: parity tests for dataflow-graph and systems-db-stamp (happy path verified side by side only), and retirement of all five old scripts, their make targets, and the pre-commit hook entry for fact-ids.
DONE 2026-08-31. All five gates ported, tested against their failure paths, old scripts retired, hooks and make targets rewired, and both hooks exercised for real.
FINAL STATE: reach check client-version / canvas-version / systems-db-stamp / dataflow-graph / fact-ids. tooling/check-* no longer exists.
VERIFIED BY THE HOOKS THEMSELVES, not by reading them. The retirement commit ran the rewired PRE-COMMIT hook, which executed reach check fact-ids and emitted its verdict. The push ran the rewired PRE-PUSH hook, which executed client-version, canvas-version and dataflow-graph through reach and passed. That is the only evidence that matters here a hook edit verified by inspection is a hook edit that has not been tested.
TESTS: tooling/test_check.py, 19 cases across five gates, every gate exercising at least one failure. Renamed from test_check_parity.py because its subjects no longer exist. Every case began as a parity case running both implementations; that evidence is recorded above and in the commits. With the scripts gone there is nothing to compare against, so the assertions become the spec. A parity test is scaffolding with a defined lifetime keeping one after its subject is deleted would mean keeping the subject alive to be compared with, which is the opposite of a migration.
TWO GATES COULD NOT BE FIXTURE-TESTED AGAINST THEIR ORIGINALS, and both reasons are findings rather than obstacles:
- canvas-version: canvas_sources globs from a __file__-derived root while the service resolves git through config.repo_root(), so a fixture would DIFF one tree and GLOB another. Real history used instead five ranges including two GENUINE instances of the regression this gate exists to catch (T-1237 and T-1194, both bumped after the fact). The history that produced the check, used as its own fixture.
- systems-db-stamp: generator_sources raises at IMPORT time when the economy-db tree is absent, so the old script died at line 39 in every fixture, before any logic. Its failure modes were never testable in isolation. The ported service imports it lazily and only after the absent/unstamped checks, which is precisely why absent/unstamped/empty/unknown/bad-version are reachable now and were not before. STALE still needs the full registered source set and is covered by the live repo on every push.
MAKE TARGETS RETIRED, NOT WRAPPED, per the D-263 split, with the mapping left as a comment where they used to be so the next reader finds the answer rather than an absence. Wrapping would leave two ways to invoke each gate and reach --help would stop being the answer to "what tooling exists" while the Makefile stayed a competing index. pre-pr-validate and pre-pr-content keep their orchestration role and drop the individual target dependency.
DOCS: CLAUDE.md and docs/DEVOPS.md updated to name the reach verbs. Sprint archives and workshop notes still reference the old paths and are deliberately left alone they record what was true when written, and rewriting history to match the present is how a record stops being evidence.
NET: 1040 lines deleted, 399 added.', 'done', 'high', NULL, NULL, 'D-263', '2026-08-31 15:30:35.338', '2026-08-31 18:43:00.154', NULL, '3b6d263bb556f6eee249ffc29d3d52dc', 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;
+2
View File
@@ -0,0 +1,2 @@
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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:13.360', NULL, 'd8cc608ee1be87634289397ebae7fd61', 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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:27.856', NULL, '89c48e98147081cb5465e7373234ec9f', 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;