Skip to content

test(docs): check the evidence anchors against source - #802

Merged
jeremi merged 25 commits into
mainfrom
agent/evidence-anchor-check
Aug 22, 2026
Merged

test(docs): check the evidence anchors against source#802
jeremi merged 25 commits into
mainfrom
agent/evidence-anchor-check

Conversation

@jeremi

@jeremi jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member

Do not merge until v0.24.0 publishes

This changes docs content, so it moves the built v0.24.0 docset and would
fail archive:snapshot v0.24.0 --verify-lock exactly as #799 did before
d1a6c7b reverted it. v0.24.0 has a recorded lock entry but no tag and no
release yet. Draft until it publishes.

The checker cannot be split out and landed first: on current main it
fails on the phantom test name and on PROBLEM_BASE, so the gate and the
corrections have to land in the same commit range or main goes red.

What this adds

Every factual claim the docs site makes about a source repository is
anchored in a {/* Evidence: ... */} comment naming the code, test, or
contract behind it. Nothing verified those anchors. Across three recent
pull requests, 24 stale path:line-range citations and one citation
naming a test that does not exist were found only by reading them by hand,
and each one turned out to be one instance of a multi-instance defect.

docs/site/scripts/check-evidence-anchors.mjs resolves the paths an
anchor cites, including the continuation forms the prose uses (a bare
src/foo.rs, a bare sibling filename, and a bare :N-M all read against
the path before them), then requires:

  1. every cited path exists,
  2. every line reference falls inside its file,
  3. every symbol-shaped token the anchor names occurs in at least one path
    that same anchor cites.

A token that resolves to no repository path is treated as prose naming a
file the repo does not own and is skipped, so the check reports drift
rather than vocabulary. Path tokens are stripped before symbol
extraction, so a filename stem is never read as a symbol.

Current tree: 121 anchors, 298 cited paths, and 204 cited symbols checked; 81 line-range citations found. Line-range citations are counted
but not yet refused; --strict-line-refs turns them into failures once
the remaining 81 (49 distinct targets across 11 pages) are converted to
symbol citations. That conversion is deliberately not in this PR.

Wired into check:source immediately after check:evidence-links.
19 tests in check-evidence-anchors.test.mjs, node:test, temp-repo
fixtures, no dependencies.

What it found

The file secret provider accepts 0400 or 0600, not 0600 alone.
validate_file_metadata tests matches!(mode, 0o400 | 0o600), and
evidencectl itself writes a 0400 runtime file, so an operator reading
"the file provider rejects anything else" would expect a working
deployment to be refused. Four pages carried the stricter claim:
explanation/threat-model.mdx, security/index.mdx,
security/hardening-checklist.mdx, security/evidence.mdx.

Prescriptive guidance that tells an operator to write 0600 is left
alone; that is sound advice, not a false claim about the validator.

file_secret_uses_open_file_owner_and_exact_mode_checks does not
exist
anywhere in the workspace. Two pages cited it. The real test is
file_secret_accepts_only_owner_read_and_optional_owner_write_modes.

Relay's problem catalog was attributed to the wrong crate.
explanation/dpi-safeguards-alignment.mdx cited
crates/registry-relay-v2/src/problem.rs:14-45, which holds only the
ProblemCodeResponseExt trait, and credited it with a PROBLEM_BASE
constant that exists only in Evidence. The 26 entries are macro-generated
in registry-relay-http-contract and each carries its own literal
type_uri. The count was right; the crate and the constant were not.

Two contract.rs line ranges pointed about 160 lines off target,
naming PurposeConstraint and AuthorityRowBinding but landing on
DatePrecision and DisclosureProfile. Both anchors already name the
symbols, so the line numbers are dropped rather than repaired.

last_reviewed bumped on the six edited pages.

Review note (security-sensitive surface)

Documentation only. No runtime, contract, schema, or generated artifact
changes. The corrections touch how secret-file permission validation and
Relay's problem catalog are described, both security-sensitive surfaces,
so every corrected claim was re-verified against the runtime source its
anchor names rather than against another doc page.

Verification

From docs/site/:

  • npm test: 383 pass, 0 fail
  • npm run check: green, including check-llms 561 passed / 0 failed,
    SEO over 396 pages, and 18439 internal links and assets
  • npm run check:evidence-anchors: green

The checker was mutation-tested rather than taken on trust: a phantom
symbol, an out-of-range line reference, and a missing path were each
injected and each diagnostic confirmed to fire.

Not folded in

Re-landing #799 is left as its own act, so the revert in d1a6c7b keeps
a clean audit trail. Say the word if you would rather both ride together
once v0.24.0 publishes.

@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

CI on the first push confirms the hold is the only thing wrong with this
branch. Run 32558028530:

  • Immutable docs archives fails with bootstrapped archive v0.24.0 does not match its immutable lock entry.
  • Docs fails only because it aggregates that job. Its own docs gate
    reported RESULT: success; the failing step is Report path-gated result with ARCHIVES_RESULT: failure.
  • Everything else passes, including Docs checks, Rust workspace,
    Release source proof, Release tooling, Changed paths, Check commit sign-offs, CodeQL, Secret scan, and Evidence tutorials from source.

So check:evidence-anchors and all six corrected pages are green in CI.
Rebase and un-draft once v0.24.0 publishes; no other change is expected
to be needed.

@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

The stated blocker is cleared: v0.24.0 published on 2026-08-22, tag v0.24.0 on 4bf7c1cde.

Worth knowing before you rebase, because the reasoning in the description is close but not quite the mechanism. What protects the archive is not the tag existing. currentSourceGeneratedArtifacts binds only docs/site/src/content/docs/reference/cli and docs/site/src/data/generated/cli-reference.json to the pinned source ref; prose is still built from the checked-out tree. What protects it is publication: assemble-archives.mjs resolves a local bundle, then the published bundle, and only rebuilds under --bootstrap. registry-docs-v0.24.0.tar.gz is now a release asset whose sha256 equals the lock entry (7ff1ae9d...), so the docset is restored rather than rebuilt.

#799 was re-landed on that basis in #806, with npm run check:archives green locally (3 restored, 0 bootstrapped).

Two things to carry over when you take this off draft:

  1. CI will not run check:archives for a prose-only PR. The docs-archives job is path-gated in .github/scripts/ci_changes.py to an infrastructure allow-list, so it shows as skipping. Run it by hand.
  2. If it fails with archive bundle <id> digest ... does not match lock ..., check docs/site/.archive-bundles/ before believing it. A stale gitignored bundle from an earlier --bootstrap run shadows the published one and produces that exact message. See A stale local .archive-bundles entry shadows the published bundle and reports it as a lock violation #809.

Ordering note: this and #806 both touch docs content, so whichever lands second wants a rebase.

@jeremi
jeremi force-pushed the agent/evidence-anchor-check branch from 74decb7 to debfe23 Compare August 22, 2026 09:56
@jeremi
jeremi marked this pull request as ready for review August 22, 2026 09:56
@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Unblocked. v0.24.0 published 2026-08-22T09:35:12Z, the release carries
registry-docs-v0.24.0.tar.gz, and assemble-archives.mjs downloads a
published bundle instead of bootstrapping one from the tree, so docs
content no longer moves that archive. Rebased onto 4bf7c1cde (clean,
both sign-offs intact) and marked ready.

Merge #806 first. It re-lands the reverted #799 and shares two files
with this branch, explanation/threat-model.mdx and
explanation/trusted-context-constraints.mdx. git merge-tree reports a
content conflict in both: each PR bumps last_reviewed on the same
frontmatter line, and the prose edits are disjoint (#806 changes the
source-transport wording, this branch changes the secret-file mode claim
and drops a drifted contract.rs line range). I will rebase and resolve
once #806 lands rather than resolve against a head that is still under
review.

@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Green on the rebase. All 15 non-skipped checks pass, including Docs,
Docs checks, Immutable docs archives, Rust workspace, Release source proof, and Release tooling. mergeStateStatus: CLEAN.

Immutable docs archives runs here only because this branch touches
docs/site/package.json, which is in the docs_archives allow-list in
.github/scripts/ci_changes.py. A content-only docs change gets
skipping, which is #808. I also ran the check by hand:

Assembled 3 immutable archive(s): 3 restored, 0 bootstrapped, 0 supplied separately, 24 outside the publication window.
SEO check passed: 0 canonical release HTML files, 396 unreleased Main HTML files, 1181 immutable archive HTML files, and 0 legacy redirects checked.
Built link check passed: 70248 internal links and assets checked.

0 bootstrapped is the point: v0.24.0 restores from its published bundle,
so this branch no longer touches it.

Two things a reviewer should not read as oversights, both deliberate:

  • --strict-line-refs is off. 81 line-range citations remain, across
    49 distinct targets on 11 pages. The flag exists and works; turning it
    on requires converting those to symbol citations first, which is
    follow-up rather than part of this change.
  • Prescriptive 0600 guidance is untouched. Only claims about what
    the validator accepts were corrected. Telling an operator to set
    0600 is still sound advice, so those lines stay.

Still blocked on #806 for merge order, per the conflict note above.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: debfe23dd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Added docs/site/AGENTS.md to the checker commit, so the gate ships with
the rule an author reads before writing a citation. It sits under the
existing "Every factual claim about a source repo must be anchored"
paragraph, which is the rule this check enforces.

It states what the check rejects, that a drifted citation is now a merge
blocker rather than a wart, that a token resolving to no repository path
is read as prose and skipped, and both deliberate allowances:
--strict-line-refs off while 81 line-range citations remain, and
prescriptive guidance untouched because the check reasons about claims
describing what code does, not about advice. It also asks for symbol
citations over line ranges in new writing, since a symbol survives an
edit above it.

Documenting this in release notes instead would have reached the wrong
reader: the gate changes what a contributor can merge, not anything an
adopter runs, and the author who trips it will be reading AGENTS.md.
Shipping a gate and documenting it a release apart is how the
archive-window rule ended up unwritten, which is now #808.

@jeremi
jeremi force-pushed the agent/evidence-anchor-check branch from debfe23 to 57f6cb7 Compare August 22, 2026 10:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57f6cb76f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Four Codex findings all confirmed against the source and fixed, with a test
each written before the fix. They were one defect in four places: the check
reported success where it should have reported drift, which is worse than
no check because it manufactures confidence.

  • A continuation with nothing before it (src/data/projects.yaml) resolved
    under docs/site but was optional, so deleting the file removed the
    citation from validation. Now required. No anchor relied on the leniency;
    the cited-path count was unchanged by the fix.
  • Line refs were checked only against EOF, so :0, a descending :5-3, and
    a range starting past EOF all passed.
  • extractSymbols never read lowerCamelCase, so every wire and
    configuration key the docs quote went unchecked.
  • Citations could leave the repository: the path pattern matches .. and
    the resolved path was never tested for containment, so
    crates/../../../etc/passwd counted as valid. This is the security-
    relevant one, since the check stats and reads what it resolves.

The lowerCamelCase fix immediately found two real anchors, both quoting a
wire key while citing only the Rust file that carries the snake_case
spelling:

  • records-stay-home.mdx quotes authorityRowBinding, cites contract.rs
    (AuthorityRowBinding / authority_row_binding). The key is in the
    relayctl authoring schema.
  • verify/index.mdx quotes recordsEquivalentTo and etagSameAs, cites
    fixtures.rs (records_equivalent_to / etag_same_as). Both keys are in
    the business-registry acceptance expectations. recordsEquivalentTo
    passed only because a test literal in fixtures.rs happens to contain it,
    so the pair was inconsistent for an incidental reason.

Both anchors gained the path carrying the key rather than losing the claim.
The prose describes what an adopter writes, so the spelling it quotes is the
right one.

That same records-stay-home anchor cited contract.rs:947-976 for
AccessRule and AuthorityRowBinding, which are at 1107 and 1134; 947-976
now holds PartialStringReveal, DateInputType, DatePrecision, and
DisclosureProfile. Inside the file, so no line check can catch it, only
--strict-line-refs would. Dropped, since the anchor already names the
symbols. It is the clearest argument in this PR for converting the
remaining 80.

Local: npm test 390 pass, npm run check green, anchor check now
121 anchors, 300 cited paths, 224 cited symbols, 80 line-range citations.

Note for #806: this adds explanation/records-stay-home.mdx to the overlap,
so the rebase after #806 lands now touches three files rather than two.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2caf7b991e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c1b728863

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6daf04756

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebbfefeee0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs
@jeremi
jeremi force-pushed the agent/evidence-anchor-check branch from ebbfefe to e3a36b6 Compare August 22, 2026 11:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3a36b68cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/package.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74a6d5b945

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50329626a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
@jeremi
jeremi force-pushed the agent/evidence-anchor-check branch from 5032962 to 6802078 Compare August 22, 2026 13:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6802078110

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
jeremi added 5 commits August 22, 2026 16:14
Every factual claim the docs site makes about a source repository is
anchored in a {/* Evidence: ... */} comment naming the code, test, or
contract that backs it, and nothing verified those anchors. Across three
recent pull requests, 24 stale path:line-range citations and one citation
naming a test that does not exist were found only by reading them.

The checker resolves the paths an anchor cites, including the
continuation forms (src/foo.rs and a bare sibling filename read against
the previous path, and a bare :N-M read against the file before it), then
requires that every cited path exists, that every line reference is
inside its file, and that every symbol-shaped token the anchor names
occurs in at least one path that anchor cites.

Line-range citations are counted but not yet refused. 81 remain in the
tree; --strict-line-refs turns them into failures once they are converted
to symbol citations.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
What the anchor checker found, plus one claim the same reading exposed.

The file secret provider accepts mode 0400 or 0600, not 0600 alone:
validate_file_metadata() tests matches!(mode, 0o400 | 0o600), and
evidencectl writes a 0400 runtime, so an operator reading "rejects
anything else" would expect a working deployment to be refused. Three
security pages claimed the stricter rule. The security landing page also
cited file_secret_uses_open_file_owner_and_exact_mode_checks, which
exists nowhere in the workspace; the real test is
file_secret_accepts_only_owner_read_and_optional_owner_write_modes.
The threat model carried both errors and was corrected in #806.

The DPI safeguards page attributed Relay's problem catalog to
crates/registry-relay-v2/src/problem.rs, which holds only the
ProblemCodeResponseExt trait, and credited it with a PROBLEM_BASE
constant that exists only in Evidence. The 26 entries live in
registry-relay-http-contract and each carries its own literal type_uri.
The count was right; the crate and the constant were not. The same
anchor's contract.rs range named PurposeConstraint and
AuthorityRowBinding but pointed at DatePrecision and DisclosureProfile,
about 160 lines away, so that range and the stale auth.rs range beside
it are dropped rather than repaired; the anchor already names both
symbols.

Security-sensitive review note: documentation only, no runtime or
contract change. Every corrected claim was re-verified against the
runtime source named in its anchor.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Review found four ways the check reported success where it should have
reported drift, which is worse than no check because it manufactures
confidence.

A continuation path with nothing before it, such as src/data/projects.yaml,
resolved under docs/site but was optional, so deleting the file removed the
citation from validation instead of failing. It is a definite path and is
now required. No real anchor relied on the leniency: the cited-path count is
unchanged.

Line references were only checked against the end of the file, so :0, a
descending range such as :5-3, and a range starting past EOF all passed. A
range now has to start at line 1 or later, end at or after its start, and
lie inside the file.

extractSymbols read SCREAMING_SNAKE_CASE, snake_case, and UpperCamelCase but
not lowerCamelCase, so the wire and configuration keys the docs quote were
never checked at all. Misspelling packageRevision left the gate green.

Citations could leave the repository. The path pattern matches .. segments
and the resolved path was never tested for containment, so an anchor citing
crates/../../../etc/passwd counted as a valid existing path. Traversal is
now an error rather than a skip, since skipping is how it would hide.

Security-sensitive review note: the traversal fix is the reason this is not
purely a correctness change. The check reads and stats every path it
resolves, so before this it would follow a citation to an arbitrary file
outside the tree.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The lower-camel-case check finds two anchors naming a wire key while citing
only the Rust file, which carries the snake_case spelling.

records-stay-home.mdx quotes authorityRowBinding and cites contract.rs,
where the enum is AuthorityRowBinding and the field is
authority_row_binding; the key itself is in the relayctl authoring schema.
verify/index.mdx quotes recordsEquivalentTo and etagSameAs and cites
fixtures.rs, which spells them records_equivalent_to and etag_same_as; both
keys are in the business-registry acceptance expectations.
recordsEquivalentTo passed only because a test literal in fixtures.rs
happens to contain it, so the pair was inconsistent for an incidental
reason.

Both anchors gain the path carrying the key rather than losing the claim:
the prose is describing what an adopter writes, so the spelling it quotes
is the right one.

The same records-stay-home anchor cited contract.rs:947-976 for AccessRule
and AuthorityRowBinding, which are at 1107 and 1134; that range now holds
PartialStringReveal, DateInputType, DatePrecision, and DisclosureProfile.
The range is inside the file, so no line check can catch it. Dropped, since
the anchor already names the symbols.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Six cases the checker claimed to check and silently did not:

- a qualified symbol was read from its last segment only, so a typo in the
  module, type, or enum that qualified it stayed green; every segment that
  carries a symbol shape is now checked on its own.
- an identifier spelled with empty parentheses, router() or prepare(), matched
  none of the four case shapes and was dropped; the anchors already spell a
  function reference that way, so it is read as a name whatever its case.
- a bare filename the repository keeps at its root resolved against no
  candidate at all, so its symbols were never looked for. The root is tried
  after the search inside the cited unit rather than before it, so a crate's
  own README wins over the workspace one instead of losing to it.
- a sibling filename read against the last full path rather than the last
  cited one, so a continuation between them was ignored. Only a citation that
  claims a repository path moves that anchor: a sibling is a reading of the
  prose and leaves it where it is.
- a bare .rhai script was not a sibling filename, though the scripts are cited
  as bounded request preparation and source extraction.
- a line suffix cut short, :5-, parsed as the single line 5 and is now
  reported as the malformed reference it is.

The docs need no correction under any of them: 25 more symbols are checked,
and every one resolves in a path its anchor already cites. The cited-path and
line-range counts are unchanged, which is what holds the sibling-anchor change
honest: moving that anchor too eagerly drops a path from the count in silence.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
jeremi added 18 commits August 22, 2026 16:14
The lexical containment check refused a `..` segment and resolved the
citation against the repository root, but never consulted a real path. A
tracked symlink pointing outside the checkout passed it, and both the
stat and the read then followed the link, so content the repository does
not hold could satisfy an anchor.

Resolve both sides before deciding: the root as well, since a macOS
temporary directory is itself reached through a symlink. A path with no
real path stays the missing citation it already was.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
An exact wire value such as ES256 or RS256 matched none of the four case
shapes, so an anchor could name one that no cited path holds and stay
green. Add a fifth shape for it: uppercase letters and digits only, with
at least two letters and at least one digit.

Both exclusions are measured, not guessed. Requiring a digit keeps out
the acronyms the prose spells in capitals, JSON, HTTP, SQL, and the
second letter keeps out the product version words V1 and V2; without
them the rule reports correct anchors today. Mixed-case values such as
EdDSA stay uncaught: the only shape that admits them also admits
OpenAPI, OpenID, OpenCRVS and 25 more prose names, one of which already
resolves only through an unrelated file.

Cited symbols checked go from 286 to 288, with no change to the cited
paths or line ranges.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The guarantee the page advertises has to match what the check does, or it
converts an unchecked claim into a checked-looking one. Name the shapes a
symbol is read by, and name the one gap: an all-capital wire value with no
digit is prose to the check. Reaching EdDSA needs a shape that also admits
OpenAPI, OpenCRVS, and every acronym in the prose, so it stays uncaught by
choice rather than by oversight.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
An anchor names several files in one directory in a compact brace form,
`src/{contract,compiler,api}.rs`. The citation pattern held no brace, so the
match stopped at the directory before it and the check validated that
directory alone: renaming or deleting any file the list names left the gate
green.

Expand the list instead of refusing it. A brace segment is read where a path
segment can start, so every entry becomes its own full or continuation
citation with the suffix the entries share, resolved and counted on its own. A
brace group the prose itself writes, `{ claim, allowed }`, sits after no slash
and stays prose.

The one brace list in the content names six files, so the cited-path figure
moves from 333 to 338: six citations in place of the single directory the
match used to stop at. Anchors, symbols, and line ranges are unchanged.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
An anchor names configuration and wire keys as dotted paths, such as
evidence_data_request.transport_absences.credentials. The word pass splits on
the dot and then drops any segment that carries no symbol shape, so the leaf
of every such key went unchecked: misspelling it left the gate green.

Read a dotted token as one key path and check every segment of it, once one
segment already carries a symbol shape. That mark is what was measured, not
guessed. Checking every segment of every dotted token adds 73 checks over 30
distinct tokens and reports five of them, all from the version string v0.9.0,
and pulls in org, registrystack, and a bare digit besides. Requiring one shaped
segment adds 15 checks over 11 distinct tokens with nothing reported, and none
of the 15 passes by coincidence: each resolves in a file that also holds every
shaped segment of the same token, which for request.fields_invalid and
classification.context.selector_more_restrictive_than_disclosure is the file
holding that exact literal. A `*` is skipped rather than looked up, since it
stands for any key rather than naming one.

One gap stays open by choice. A key path no segment of which carries a shape,
sources.*.authentication.kind, is still unread. Treating the `*` itself as the
mark would reach it and reports nothing today, but the three segments it would
check are among the commonest words in the tree, with no shaped segment to tie
the match to the file the sentence is about, so it would look checked while
biting on almost nothing.

Cited symbols move from 288 to 303, the 15 the measurement predicted. Anchors,
paths, and line ranges are unchanged.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The sibling form needed a path before it, so a filename in first position
produced no citation at all and the file it named never reached the symbol
check. Read it against the repository instead: the file kept at the root,
then a single unambiguous file of that name in the tree, the two fallbacks
the sibling form already had.

It stays a reading of the prose rather than a path claim, like every other
bare filename: with no path before it, nothing says the repository owns the
name, so an adopter's metadata.yaml is still left alone.

The one anchor that opened on a filename, contracts.yaml in the boundaries
map, names docs/site/src/data/contracts.yaml, whose
registry-manifest.metadata-yaml entry carries the consumer note it cites.
Spell that path out: an explicit path is reported when it goes missing,
where a bare name would quietly stop resolving.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The sibling form asked for a filename extension, so a bare directory that
continued a cited directory carried no citation and renaming it left the
gate green. Read a trailing-slash name against the directory cited before
it and report it when it is gone, which is what the one real case wanted:
protected-read-evidence/ under the reference deployment projects.

A trailing-slash name after a filename stays prose. governed/ and
generated/ beside package.rs name directories relayctl writes into a
package, not directories the repository holds, so checking them would
report correct anchors.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
UpperCamelCase asked for an [A-Z][a-z]+ boundary at every word, so a name
whose capitals run together went unchecked while the guidance said the
shape was read. Seven declared types in crates/ carry it, all on the OAuth
token surface, and the anchors that would cite them are the ones a rename
there would drift.

The discriminator is two lower-case runs and one capital run of two or
more. Measured over all 134 anchors it adds no symbol check and no
distinct token: OpenAPI, SQLite, OpenCRVS, and EdDSA carry one lower-case
run, SDMX and JWKS carry none, so the acronyms the prose is full of stay
prose. A name carrying one run, SDMXProfile, stays unchecked with them,
and the guidance now names that boundary with the other gaps.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The seven readings build object literals of different shapes, so the field a
later reading looks for reads as absent on the ones that do not carry it. A
typedef states which fields every citation has and which belong to one reading.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The anchors cite source across the whole workspace, but the only job that
ran them sat behind the changed-path classifier's docs allow-list, which
names five crate files. A rename of any other cited source left the docs
job skipped and the drift merged green.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
UpperCamelCase asks for two capitalized chunks, so a one-word name such as
Public, Snapshot, or Visibility was read as prose and left unchecked. The
qualified spelling is already checked segment by segment, so naming the
owning type brings each variant under the existing rule.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A sibling stays optional so an anchor may name a file the repository does
not own, but that also let a repository-owned file be deleted with the
check still green. Only this repository writes Rust into the stack, so a
bare .rs name is a claim: measured over every anchor, the rule requires six
of the seven siblings that resolve today and fails none of the three that
do not.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The escape check reads a citation's candidate list, and the file kept at the
repository root is no member of it: a bare filename carries it separately as
its last resort. A root file that is a symlink pointing outside the checkout
therefore passed the check, resolved the citation, and had its outside
contents read to satisfy the anchor's symbols.

Unwind the resolution chain into explicit steps so the root candidate can be
refused where it is consulted, and report the refusal rather than skipping the
citation: it is the only thing that would have resolved, so falling through
silently would leave the anchor unchecked.

The search for a single unambiguous file of the same name needs no such check.
It walks the tree with readdirSync(..., { withFileTypes: true }), whose
Dirent.isFile() and isDirectory() read the entry rather than its target, so a
symlink is neither and never enters the walk.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The citation roots named six directories and the repository keeps ten, so a
path into editors/, docker/, or .cargo/ matched no citation pattern, parsed as
prose, and left its anchor checked against nothing. A rename under any of them
was invisible to the gate.

Name all ten, and add a test that reads the tracked directories out of the
committed tree so the next one the repository grows fails the gate rather than
slipping through it. Git is what says which directories are tracked: a listing
of the checkout also carries build output and local tooling, and which of
those are present differs between a clean CI checkout and a working machine.

schemas/ is on both root lists, because a crate or product keeps one and so
does the repository. Reading it as a repository path outright would take the
continuation reading away from the two dozen crate and product schemas
directories in exchange for the one file the repository keeps at the top, so a
citation that starts there keeps the continuation candidates and gains the
repository reading as its last one. The nearer directory still wins, as it
does for a bare filename that may name a file kept at the root.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
An anchor whose paths all failed to be read as citations, by a typo such as
crate/ for crates/ or by naming a directory the roots do not cover, reached
the symbol check with nothing to read against and was skipped. It counted as
an anchor and contributed no checked path and no checked symbol, so the gate
reported a guarantee it had not made for it.

The two ways an anchor arrives there are different mistakes and are told
apart. One that parsed no citation at all cites no path in this repository:
the writer named nothing the check could open, and pairing an upstream
standard with the file that implements it is the fix. One that parsed
citations none of which resolved is reported as resolving none of them: every
path it named was read as prose, which is what a bare filename the repository
does not own is, and the anchor needs one path the repository does hold.

An anchor whose citations were already reported keeps its own message, because
it fails on those and a second line names no further drift.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The citation pattern reads a valid line suffix and stops, so whatever the
anchor wrote past it was thrown away. `:abc` left the citation checked as the
bare file, and `:1foo` left it checked as line 1, neither of them a line the
anchor meant, and neither reported. Only the range cut short, `:5-`, was
caught, and only because a trailing hyphen was looked for by name.

Read the leftover instead: word characters or a hyphen, which a well-formed
reference would have carried itself, optionally behind the colon that opens
one. That is what a mis-spelled reference leaves and what a correct one never
does, since prose puts a space after a colon it writes and puts its
punctuation after a reference it ends on. The existing report carries the
leftover rather than a hardcoded hyphen, so the three shapes read alike.

The rule fires on none of the 134 anchors in the tree, which keeps the counts
and the zero errors exactly where they were.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Whether a trailing-slash name continues a directory or is prose was decided by
looking for a dot in the last segment of the path before it. An extensionless
executable such as release/scripts/registry-release has no dot, so it read as a
directory and the name after it was resolved beneath a file, which can never
exist. A correct anchor for any of the repository's extensionless scripts was
reported as drift.

The parse stays free of the filesystem, which is what makes it directly
testable: it records the path the child continues, and the resolution, which
already opens the tree, reads that path's kind and drops the citation where it
is not a directory. A name whose parent is a file, or is nothing the repository
holds, is prose and is left alone rather than reported.

This is the same discrimination that already made governed/ and generated/
correct beside package.rs in explanation/publishing-pipeline.mdx. The dot in
that filename is what saved them; the parent's real kind saves them for the
reason they are actually prose, and that anchor is the regression test in the
tree.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The five fixes changed what an anchor may say and what the gate promises, so
the guidance says it: an anchor has to cite at least one path the repository
holds, a citation may start at any top-level directory, `schemas/` reads
nearest first because a crate and the repository both keep one, a path a
symlink leads out of the checkout is refused, a line reference is spelled
`:12` or `:12-14` and nothing else, and a trailing-slash name continues the
path before it only where the repository holds that path as a directory.

The bare filename read as prose keeps its exception and gains its condition:
it is fine wherever the anchor resolves some other path.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi
jeremi force-pushed the agent/evidence-anchor-check branch from 6802078 to 73f6c95 Compare August 22, 2026 14:14
A bare child continued the parse's first candidate rather than the path the
citation actually resolved to. Where a shared root resolved through a fallback,
the repository's own schemas/ rather than the cited crate's, that first
candidate named nothing, so the guard read it as prose and dropped the child
citation without checking or counting it: deleting the directory it named left
the gate green.

The parse now records the child's bare name and the resolution reads it against
the directory the anchor reached, dropping that position wherever a citation
resolves nothing so a child after a reported miss stays prose instead of being
read against a stale directory.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73f6c959a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs
@jeremi

jeremi commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Confirmed and fixed in 0003231.

The defect is exactly as described. parseAnchor advanced its position with
lastCitedPath = citation.candidates[0], the first candidate, while resolution
took the first candidate that exists. A shared-root continuation carries three
candidates (cited unit, parent of the last path, repository root), so when it
resolved at the repository the recorded position still named a path under the
crate. The following child then failed the entryKind(...) !== 'directory'
guard and hit a bare continue: no error, no paths increment, nothing checked.
A gate that silently drops a citation is worse than one that never had it.

What changed:

  • The parse records childName, the bare name, instead of parentDirectory,
    the guess. The guessed parent is gone rather than left unread.
  • Resolution tracks lastResolvedPath, the path the previous non-sibling
    citation actually resolved to, and builds the child's candidate from it.
  • That position is dropped wherever a citation resolves nothing (escaping
    candidate, malformed line reference, escaping root file, reported miss), so a
    child after a reported miss stays prose rather than being read against a stale
    directory two citations back. Without this the fix would have traded a silent
    miss for a spurious error.

Test, written before the fix and failing on the parent commit
(paths 2 rather than 3): reads a bare child against the directory the anchor resolved, not the first guess. It resolves schemas/registry/ at the repository
root after a crate path, then asserts both that a held child counts and that a
renamed one is reported.

Suite is 78/78, up from 77. Worth stating plainly: running the gate over the
real tree gives 134 anchors / 340 paths / 318 symbols both before and after, so
no anchor in the repository exercises this today. The fix closes the hole rather
than correcting a present miscount.

One related thing left alone, since it is a different shape of change: the parse
chain still advances on candidates[0], so a continuation or sibling that
follows a fallback-resolved citation is still built from the guess. Neither is
silent (a continuation reports a miss; a sibling falls back to a unique-name
search), so it is a clarity problem rather than a hole. Rebasing the whole
candidate construction onto resolved paths means moving it out of the parser,
which is a larger change than this thread.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 000323199b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/site/scripts/check-evidence-anchors.mjs
Comment thread docs/site/scripts/check-evidence-anchors.mjs Outdated
A line reference the anchor carried on past the line the pattern read, the `.5`
of `:1.5`, was thrown away and the citation checked against line 1. It is read
now, and only where the token carried a reference, so the full stop that ends a
sentence on a path stays punctuation.

A bare JavaScript or TypeScript filename was no sibling the grammar knew, so a
name that resolves was never counted and `projectRoot.ts` was read as a dotted
key path instead, demanding a symbol spelled `ts`. Both are siblings now.
Whether one has to resolve is unchanged: only a Rust name does, because only
this repository writes Rust into the stack.

A file with no extension is a script the repository keeps, so a symbol an anchor
cites from `release/scripts/registry-release` was reported absent wherever the
anchor cited the directory holding it. The directory search reads it now.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi
jeremi merged commit 50ec005 into main Aug 22, 2026
45 checks passed
@jeremi
jeremi deleted the agent/evidence-anchor-check branch August 22, 2026 14:45
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