Skip to content

Gate the two-file agent-notes contract, with the matcher derived by counting this tree - #42

Merged
NSchatz merged 5 commits into
mainfrom
agent-notes-contract-gate
Aug 10, 2026
Merged

Gate the two-file agent-notes contract, with the matcher derived by counting this tree#42
NSchatz merged 5 commits into
mainfrom
agent-notes-contract-gate

Conversation

@NSchatz

@NSchatz NSchatz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What

The 2026-08-04 split made every anchor between CLAUDE.md and documentation/agent-notes.md
load-bearing, and nothing checked them. scripts/check-agent-notes.ts now verifies that the
narrative file is tracked, that no section is emptied down to its heading (a container whose body is
its subsections is exempt and counted), and that every pointer at it resolves.

It runs from test/scripts/agent-notes.test.ts, so it rides the required ci / verify contexts and
prepublishOnly rather than adding a fourth workflow. In this package prepublishOnly is a real
pre-publish gate, so the check blocks a publish too.

The matcher was derived by counting this tree, not ported

Porting a sibling's matcher without re-counting has produced a false green twice in this ecosystem: a
qualified-only matcher would have covered 3 of ncpdp's 38 pointers, and would have matched nothing
at all in terminology (42 bare, zero qualified) while still exiting 0.

Measured here before the matcher was written: 11 qualified pointers, all in CLAUDE.md, and zero
bare pointers.
The bare-shaped spans in the pair are 5 digits-only pull-request references plus
spans that are not anchor-shaped (a quoted shebang, quoted headings). So this gate matches the
qualified form only, and a bare-form census refuses the run at exit 2 if a genuine bare pointer
ever appears, because the evidence the scope rests on would have gone stale. No count is written into
the gate's prose as a promise; the OK line prints them all on every run.

Proven non-vacuous before it was believed

Every control below was run against a clone of the real tree, not a fixture:

what was broken result
one real anchor in CLAUDE.md misspelled exit 1, naming file and line
a real section emptied down to its heading exit 1, naming section and slug
the narrative file deleted exit 1, the pair plus every pointer
a genuine bare pointer added (one that RESOLVES) exit 2, "re-derive the matcher"
the matcher neutered so it matches nothing exit 2, self-test refusal
every qualified pointer rewritten out of range exit 2, "ZERO qualified pointers"

The last two are the terminology scenario. A gate that went green on either would be worthless
while looking healthy.

Re-derived for this repository rather than inherited

  • Exit codes come from this repo's own scripts/phi-scan.ts: 0, 1 a finding, 2 a refusal.
  • An unmerged path REFUSES here, even though phi-scan leaves that status unenumerated. Its
    reasoning (git commit refuses an unmerged index at exit 128) covers a staged route this gate
    does not have.
  • The NUL skip is a disclosed miss and is required, not tidy: the tree tracks vendored @cosyte/*
    tarballs and a synthetic DICOM fixture. A draft claimed this partition differs from
    check-no-emdash.sh's; that was false and was corrected by reading that gate's own OK line.
    Both key on an actual NUL byte. Git's binary set is the wider one, which is why neither gate may be
    reduced to grep -I.
  • The slug transcription was verified by running [email protected] over this tree's 20 real
    headings, not by copying a sibling's table.

Budget

CLAUDE.md was at its ceiling, so the branch-protection, PHI-residual and em-dash blocks were
compressed to their imperatives; the reasoning each compresses was already in the narrative file, and
the new gate's own narrative was added there. No trap was deleted and no ceiling was raised.

Gate status, stated plainly

scripts/verify.sh cli is red on exactly two steps and both reproduce on the base 0a1b322:
pnpm audit --prod (advisories in @modelcontextprotocol/sdk transitive deps) and the licenses gate
(a local pnpm store index error for the vendored fhir tarball). This slice adds no dependency and
touches neither. Every other rung is green: typecheck, lint, format:check, phi-scan, no-emdash,
no-internal-refs, test:coverage, build, attw, smoke.

verify.sh's ladder does not yet know the name check:agent-notes, and says so itself, so a green
verify.sh means less than a green CI here.
The checks on this PR are the authority.

…ounting this tree

The 2026-08-04 split made every anchor between CLAUDE.md and
documentation/agent-notes.md load-bearing, and nothing checked them.
scripts/check-agent-notes.ts verifies the narrative file is tracked, that no
section is emptied down to its heading (a container whose body is its
subsections is exempt and counted), and that every pointer at it resolves. It
runs from test/scripts/agent-notes.test.ts, so it rides the required ci / verify
contexts and prepublishOnly rather than adding a fourth workflow.

THE MATCHER WAS DERIVED BY COUNTING BOTH SPELLINGS HERE, NOT PORTED. Porting
without re-counting has produced a false green twice: a qualified-only matcher
would have covered 3 of ncpdp's 38 pointers, and would have matched nothing at
all in terminology (42 bare, zero qualified) while still exiting 0. Measured on
this tree: 11 qualified pointers, all in CLAUDE.md, and zero bare pointers. So
this gate matches the qualified form only, and a bare-form census refuses the
run at exit 2 if that spelling ever appears, because the evidence the scope
rests on would have gone stale. No count is written into the gate's prose as a
promise; the OK line prints them all on every run.

PROVEN NON-VACUOUS AGAINST A CLONE OF THE REAL TREE before it was believed: a
misspelled real anchor and an emptied real section each exit 1; a deleted
narrative file exits 1 with the pair and every pointer; a bare pointer that
RESOLVES exits 2; a neutered matcher and a tree with every pointer rewritten out
of range both exit 2 rather than going green.

Re-derived for this repository rather than inherited: the exit codes come from
scripts/phi-scan.ts, and an unmerged path REFUSES here even though phi-scan
leaves that status unenumerated, because its reasoning (git commit refuses an
unmerged index) covers a staged route this gate does not have. The NUL skip is a
disclosed miss and is required rather than tidy: the tree tracks vendored
tarballs and a synthetic DICOM fixture. A draft claimed that skip differs from
check-no-emdash.sh's partition; that was false and was corrected by reading that
gate's own OK line. Both key on an actual NUL byte.

CLAUDE.md sat at its byte budget, so the branch-protection, PHI-residual and
em-dash blocks were compressed to their imperatives and the reasoning each one
compresses (already present in the narrative file) now carries it. No trap was
deleted and no ceiling was raised.

Two verify.sh steps are red and BOTH REPRODUCE ON THE BASE 0a1b322: pnpm audit
(advisories in @modelcontextprotocol/sdk transitive deps) and the licenses gate
(a local pnpm store index error for the vendored fhir tarball). Neither is
touched by this slice, which adds no dependency.
Pass 1 returned REFUTED with three INTRODUCED major findings. None needed a
bigger matcher; all three were a claim that was wrong or a hole left open.

1. THE BARE CENSUS WAS SCOPED TO THE PAIR ON A FALSE JUSTIFICATION. The prose
   claimed a tree-wide census would refuse on CHANGELOG.md's pull-request
   references, on quoted shebangs in tsup.config.ts and docs-content/, and on a
   quoted comment marker in scripts/phi-allow-list.txt. Running it tree-wide
   proves none of them can: a shebang, a `# synthetic` marker and a lone hash are
   all outside the anchor class, and the CHANGELOG spans are digits-only, which
   the census classifies as references by design. The only files that could
   refuse were this gate's own source and its own test, which wrote bare spans
   out literally while illustrating the rule. So the narrow scope was a
   self-exemption for the two files where a broken pointer would hide, which the
   gate's own CONTRACT_BASENAME note already forbids. The samples are now
   assembled from parts, as the qualified ones always were, and the census covers
   every opened file. That also closes the real hole: a bare pointer in a THIRD
   file was covered by neither the matcher nor a pair-scoped census.

2. "A RENAME" WAS IN THE OPENING PROMISE AND IS NOT CAUGHT. Both halves match on
   basename, which is deliberate and is what lets a qualified, a ./-relative and
   a bare-path pointer reach one target, but the directory is never compared:
   moving the file to docs/agent-notes.md while pointers keep their prefix exits
   0 while every rendered link 404s. The promise is narrowed to "stops being
   tracked at all" and the miss is disclosed and pinned. The guard was not grown.

3. A HEADING INSIDE AN HTML COMMENT MINTS A PHANTOM ANCHOR, and the disclosed-miss
   list omitted it. Every sibling copy of this gate discloses it; this one forgot,
   and the omission was the defect. Now disclosed and pinned in the false-green
   direction.

Also from that pass: the written-down count of sibling repos lacking the file was
already wrong (eight, not seven) and the set moves whenever a submodule is added,
so the number is dropped in favour of the class, including in the stderr a human
reads. Two [PINNED] misses had no case in the block whose own invariant promises
one; both now do, alongside cases for the two misses added here. The stale
"twenty real headings" figure is gone for the same reason.

CLAUDE.md gained the basename trap as a one-line imperative and stays under its
ceiling; the attw block was compressed to its imperatives, with the reasoning it
compresses already in the archive. 53 tests pass. Every local gate green.
…'s cost

Pass 2 returned REFUTED: the fix for an overclaim had shipped a new one.

1. "EVERY SIBLING COPY DISCLOSES THE HTML-COMMENT MISS" IS FALSE, and I took the
   phrasing from the review rather than from the tree. Re-measured on this
   checkout: ncpdp, terminology and astm disclose it; mllp, ccda, transform and
   docs do not, anywhere. mllp decides it, because this gate's disclosed-miss
   block is transcribed from mllp's shape and that list stops at (ix) as well, so
   the shared ancestor never carried the entry and "this one forgot" is backwards.
   The claim is withdrawn in the script, the archive and the changelog, and what
   replaces it is the enumeration rather than a count, so it can self-correct.
   Recorded with it: the umbrella's conventions assert the universal ("in every
   copy including ccda's") and the repos do not bear that out. Four sibling repos
   are owed this disclosure. That is the umbrella's to reconcile.

2. THE WIDENED CENSUS REFUSES ON AN ANCHOR-SHAPED SPAN THAT WAS NEVER A POINTER,
   and nothing disclosed it. A hex colour or a CSS id in an inline code span reds
   at exit 2 with advice to re-derive the matcher, which is wrong for a colour.
   Now disclosed as miss (xii) and pinned. DISCLOSED, NOT NARROWED: the direction
   is conservative (refuse, never a false green), and the pair-scoped census this
   replaced avoided the cost only by leaving every third file uncovered by both
   the matcher and the census.

3. The list read (viii), (ix), (vi-b), (xi), (x). Reordered so (vi-b) follows the
   (vi) it cross-references and (xi), (xii) follow (x).

A draft of (xii) wrote its two examples out literally and the gate refused on its
own source at exit 2, which is the widened census working exactly as intended.
Both examples are now described rather than written, for the same reason every
sample in these two files is assembled from parts. Swept: no non-digit literal
bare span survives in any of the five files this slice touches.

54 tests pass. Every local gate green. CLAUDE.md unchanged and under its ceiling.
Third generation of one defect, and the last: a correction carrying its own
unmeasured claim about other repositories.

1. "FOUR SIBLING REPOS ARE OWED THIS DISCLOSURE" IS WRONG. Only three are.
   mllp, ccda and transform derive an anchor by SLUGGING a heading, so a
   commented-out heading mints a phantom anchor there and the HTML-comment
   disclosure applies to each. docs does not slug at all: it resolves pointers
   against explicit <a id="..."></a> anchors, so the failure mode cannot arise
   there, and writing the disclosure into it would name a failure that repo
   cannot have. That is exactly the trap mllp's own miss (vi) states: a
   disclosure that names the wrong failure mode sends the next reader hunting
   something that cannot happen.

   The durable lesson is recorded with it, because "count more carefully" is not
   it: A CLAIM ABOUT ANOTHER REPOSITORY IS NOT CHECKABLE FROM INSIDE THIS ONE, so
   it is measured against that repository's source or it is not made. Three
   review passes were spent on three generations of that one mistake.

2. Miss (xii) overstated its own cost. A hex colour refuses only when it carries
   a LETTER; one whose digits are all decimal is classified as a pull-request
   reference and exits 0. Corrected, and the direction is noted: an entry that
   overstates its own cost is the safe way round to be wrong.

3. The changelog carried a bare count of a moving set and a dangling comparison
   ("whose list stops at the same entry", with no numeral anywhere in the entry).
   Both replaced by the enumeration, which is the rule this slice already adopted
   twice: keep the list or drop the number.

No gate behaviour changed. scripts/check-agent-notes.ts is comment-only in this
diff. 54 tests pass, every local gate green, CLAUDE.md untouched and under its
ceiling.
Both are wording, neither changes gate behaviour, and pass 4 returned PASS with
them filed rather than blocking. Fixed anyway, because leaving a known-false
sentence inside a slice whose whole subject is overclaiming would be perverse.

1. The archive said a bullet is "three levels deep". Measured, it is two, and a
   sibling of the correction above it rather than its child. Clause deleted; a
   pure deletion cannot introduce a new claim.

2. Miss (xii) said a span refuses "only when it carries a letter". Exact for the
   hex-colour half, over-narrow for the CSS-id half: an id of digits with an
   underscore or a hyphen carries no letter and still refuses. The boundary is
   exactly DIGITS_ONLY, so it is now stated as "not all decimal digits", with
   both wrong versions named so neither is written again. Every version of this
   boundary was fail-closed, so none was ever a false green.

484 tests pass. Every local gate green.
@NSchatz
NSchatz merged commit 905868e into main Aug 10, 2026
8 checks passed
@NSchatz
NSchatz deleted the agent-notes-contract-gate branch August 10, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant