Five cases, including the failure path that matters — this is the gate with
five shipped regressions behind it.
A fixture repo does not work here, and finding out why exposed a real
inconsistency in the port: canvas_sources globs from a __file__-derived root,
so under SR_REPO_ROOT the service would diff the fixture while globbing the
real tree. Git goes through config.repo_root(); the registry does not. Harmless
in production since they are the same repo, but it is the same
no-root-override asymmetry the domain map noted about the old scripts, now
inside the new code. Not fixed here — making it dynamic means restructuring six
module-level constants in a module the old script still imports.
Real history is the better fixture anyway: both implementations see identical
input, nothing is mutated, and nothing can drift from the thing it models. Two
of the cases are genuine historical instances of the regression this gate
exists to catch — T-1237 and T-1194 both changed canvas generation and were
bumped only after the fact. The history that produced the check, used as its
own test.
The test also asserts no changed file is dropped from the failure message. That
list is the actionable half; "something changed" without saying what leaves the
reader to re-derive the intersection by hand.
Proven to fail by truncating the touched-file set, which reported both the
exit-code divergence and all three omitted filenames by name. Each case also
asserts the OLD script still behaves as the case claims, so a rewritten history
would say so rather than silently checking nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines of grep/sed pipeline become a service returning a FactIdCheck and a
router that renders it. Parity on the live tree is exact: both implementations
print "check-fact-ids: OK — 6 references validated against 61 canonical facts"
and exit 0. The matching counts are the real evidence — a line-matching regex
that differed from the grep chain even slightly would move 6 or 61.
Kept line-matched rather than YAML-parsed on purpose. Parsing properly would
change which lines count: anchors, merge keys and multi-document files would
start contributing ids the old check never saw. That is a different check
wearing the same name, and a port is not the place to make it.
Three parity cases: ok, unknown fact_id, and the advisory mode where the
catalogs hold no definitions and the gate deliberately exits 0 — failing every
commit until they are populated would teach people to bypass the hook, and a
gate people route around protects nothing.
Proven to fail by removing the entity-attributes.yaml exclusion, and caught in
a way worth noting: not by the assertion aimed at it, but by the advisory case,
where including that file made the catalog non-empty so the new implementation
enforced while the old stayed advisory. A real behavioural divergence, surfaced
by exit code.
Retirement waits for the whole domain, per the per-domain rule — 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.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
schemas.py becomes pydantic, so the reference domain is the normal pattern
rather than an exception carrying a footnote. Frozen: a result is a statement
about what was found, and nothing downstream should edit the finding on its way
to being reported. pydantic stays off the --help path — test_lazy_domains still
passes, which is precisely the assertion that it loads with the domain and not
with the CLI.
The acceptance criterion could not be met as written, and that is the finding
worth keeping. It asked for byte-for-byte parity with the old script; D-263 was
amended after this ticket to give reach a streaming model that puts the verdict
on stderr, while the old script writes its success line to stdout. Measured:
the text is byte-identical in text mode, only the stream differs. Matching both
would mean abandoning streaming or special-casing every ported gate.
So parity is redefined, and it is stronger than bytes where it counts: exit
codes match exactly, no fact the old message carried is lost, and failures name
a remedy as a structured field. That governs every port in T-1251, not just
this one, so it is in D-263 rather than only here.
test_check_parity.py runs three paths — ok, drift, missing file — through both
implementations and compares. It builds a throwaway fixture repo and copies the
OLD script into it, because that script resolves its root from __file__ and has
no override; the new command just takes SR_REPO_ROOT. That asymmetry is part of
why the port earns its keep. It also asserts the failing paths actually exit
non-zero, without which "the exit codes matched" would be vacuous for two
checks that both silently pass.
Proven to fail twice before being trusted. Once by accident: the first version
asserted the yaml version appears on every failing path, which the old script
does not report when the client file is missing — the test was wrong, not the
code, and it now derives expected facts from what the old output actually
contains. Once on purpose: mutating the router to drop a version made it fail
and name the missing fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>