Skip to content

fix(docs-audit): anchor the ledger route:/client: KEY in one place (#11542) - #11634

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11542-phantom-row-key-anchor
Aug 24, 2026
Merged

fix(docs-audit): anchor the ledger route:/client: KEY in one place (#11542)#11634
os-steve merged 1 commit into
mainfrom
claude/issue-11542-phantom-row-key-anchor

Conversation

@claude

@claude claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #11542

scripts/docs-audit/affected-docs.mjs has eight scans that ask "is a route: / client: declaration written here?". #11558 gave the run between the colon and the value one home (declLead) and deliberately left the key as each call site's own argument. Only declarationsIn anchored it with \b, so subroute: 'GET /api/v1/gone' was a declaration to seven of the eight scans and not to the eighth, and it minted a silent phantom row.

Silent for the reason #10683 and #10793 were: the partial-read verdict keys on the gap between rows and routesDeclared and both terms read the unanchored spelling, so both moved together and no verdict fired. outsideCode could not see it either — the lead genuinely is in code position. declarationsIn, the one scan that got it right, sat on the side of the ledger where being right shows up only as a shortfall, which is exactly what that arithmetic hides.

The anchor now lives in declLead, once. declarationsIn stops restating it.

-function declLead(keys) { return String.raw`${keys}\s*:\s*`; }
+function declLead(keys) { return String.raw`\b${keys}\s*:\s*`; }

-  const re = new RegExp(declLead(String.raw`\b(route|client)`), 'g');   // declarationsIn
+  const re = new RegExp(declLead('(route|client)'), 'g');

All eight scans read the same anchored spelling — 1 of 8 → 8 of 8

The #11558-equivalent measurement, taken by extracting the real new RegExp(declLead(…)) call sites out of the file and evaluating them through the file's own declLead body, on both trees:

tree anchored distinct lead source
origin/main 3637731e2 1 of 8 \b(route|client)\s*:\s* ×1 · (route|client)\s*:\s* ×2 · route\s*:\s* ×3 · client\s*:\s* ×1 · (?:route|client)\s*:\s* ×1
this branch 505ecc609 8 of 8 \b(route|client)\s*:\s* ×3 · \broute\s*:\s* ×3 · \bclient\s*:\s* ×1 · \b(?:route|client)\s*:\s* ×1

After the change the four remaining spellings differ only in the key alternation each scan's own question needs (capturing / bare / non-capturing); the anchor and the colon run are byte-identical across all eight. Pinned three ways in --self-test, so a ninth scan or a call site that restates the \b fails there rather than drifting green:

  • the anchor is spelled once in source (String.raw\b${keys}\s*:\s*``, exactly 1 occurrence);
  • no call-site argument carries a \b (exactly 0 occurrences);
  • behaviourally, the three key spellings the call sites pass all come back anchored.

Priced against the --bridge-coverage before/after standard

Anchoring removes rows from the measured population, which is why #11542 was filed rather than folded into #11558. Priced on the tree where the move is provably free, and proven at row granularity rather than counter granularity:

base 3637731e2 (unanchored) this branch 505ecc609 (anchored)
rowsParsed / routesDeclared 269 of 269 269 of 269
clientRows / clientsDeclared 222 of 222 222 of 222
reachable 45 45
unreachable 177 177
leadsOutsideCode 0 0
brokenScan 0 (exit 0) 0 (exit 0)
tails / ledger files / registrar files 43 / 7 / 12 43 / 7 / 12

Which rows move: none. --bridge-coverage --json — which carries all 177 unreachableRows by {file, route, client}, not just the counts — is byte-identical across the change: git hash-object gives d04a5cedfb613370e5b46ac4725db1d941e5dc88 for the base tree, for the ablated (unanchored) branch tree, and for the fixed tree. The human-rendered output diffs empty too. So no row — phantom or genuine — enters or leaves any population on today's seven ledgers, and the reach ratio is untouched.

The direct reason: across the seven live ledgers there are 0 divergent leads — every match of (route|client)\s*:\s* over raw text is also a match of \b(route|client)\s*:\s* at the same index. Same "provably free tree" argument #10683 and #10793 each made explicitly for their own population moves, made explicitly here for the same reason.

The pin #11584 left is moved, not deleted

--self-test pinned today's phantom behaviour as deliberately unmoved so that whoever closed this card would flip an existing pin. It is flipped, and the boundary comment above it is rewritten to record what moved.

Seven previously-unanchored scans, one fixture each — because "all eight agree now" is a claim about seven behaviours, and anchoring one more while leaving six is this defect with a smaller denominator. Every one of the seven is observed failing with the anchor reverted (see the ablation below); the eighth is kept as a control that was green both ways.

Not all seven are counting errors. The worst is the window delimiter: the row window is delimited by the same lead, so a subroute: written between a real route: and its client: closed the real row's window and handed the binding to the phantom —

2 row · GET /api/v1/meta → null | GET /api/v1/gone → meta.getTypes

— a wrong binding, on a path no registrar mounts, joining the UNREACHABLE population while the real row loses the binding it plainly declares. A count comparison is blind to it by construction (the same shape #10636 measured for the quote spellings). The in-window client: match had its own version: window.match() takes the first hit, so a myclient: ahead of the real client: became the binding (wrong.binding) and the real one was then named by #10636's unclaimed sweep as a value no row read. And declinedIn gave the loud direction — a double-quoted subroute: was billed as a route: the parse failed to read, firing a PARTIAL-read verdict with exit 1 on a wholly accurate ledger.

Reverse verification (ablation), mutation proven on disk both ways

Both lines the fix moved were reverted to their exact pre-fix spelling, under trap … EXIT INT TERM. The mutation was confirmed on disk by counting the injected and the removed text separately (not by a bare diff --stat, which a same-round edit can turn green), and the restore was confirmed byte-identical by hash:

CLEAN_HASH=dd3f379bbb7295e5b51ea362554ed4b9c9b6ee39
  ON DISK: injected=1 (want 1)  removed=0 (want 0)     # declLead anchor
  ON DISK: injected=1 (want 1)  removed=0 (want 0)     # declarationsIn call site
MUTATED_HASH=1aa3f9a747125c7e590319cfe88d3833084dcbba
RESTORED_HASH=dd3f379bbb7295e5b51ea362554ed4b9c9b6ee39

Direction predicted before running: turn red, on the new pins only. Observed: 12 checks fail, and each got value is the before state.

ABLATED_SELFTEST_EXIT=1
  ✗ "a `subroute:` mints NO row — the ROW RECOGNIZER anchors the key"      expected 1, got 2
  ✗ "and the row that survives is the REAL one, carrying its binding"      got "GET /api/v1/gone → null"
  ✗ "and the DENOMINATOR drops it too, so no phantom gap opens"            got "2 route / 1 client / 0 declined"
  ✗ "a `subroute:` does not CLOSE the real row window"                     got "2 row · GET /api/v1/meta → null | GET /api/v1/gone → meta.getTypes"
  ✗ "a `myclient:` does not become the row BINDING"                        got "wrong.binding"
  ✗ "and the real `client:` is bound, not swept up as unclaimed"           got "2 client / 1 declined"
  ✗ "a double-quoted `subroute:` is not billed as a DECLINED row"          got "1 row / 2 route / 1 declined"
  ✗ "so no PARTIAL-read verdict fires on an accurate ledger"               got brokenScan 1
  ✗ "a `subroute:` in PROSE is not reported as a prose-quoted lead"        got outsideCode 1
  ✗ "the run between a `route:`/`client:` colon and its value is spelled ONCE"
  ✗ "and the KEY anchor is spelled once too — no call site restates it"
  ✗ "every key spelling a call site passes comes back ANCHORED"            got "(route|client)\s*:\s* | route\s*:\s* | (?:route|client)\s*:\s*"

The $route: pins did not fail in the ablation, which is correct and deliberate — see below. Non-vacuity in the other direction is carried by the same fixtures: the genuine route: 'GET /api/v1/meta' is still read, still carries meta.getTypes, and the 269 live rows are unchanged.

The boundary this card does not cross — filed, and pinned

\b fails only against a preceding word character, so $route: — a legal JS identifier — is still read as a declaration. It is no longer a divergence, which is what this card was about: all eight scans agree on it now, and they agree by reading the spelling declarationsIn already had. It is still a phantom row. Closing it means widening the anchor from \b to a negative lookbehind that also excludes $ — the idiom rulePatternFor, symbolRe and dottedRe already use in this same file. (Spelled verbatim in declLead's docblock and in the --self-test boundary comment, not here: GitHub's body sanitizer strips the < + ! digraph out of an issue or PR body — inside a code span and inside a fenced block alike — and silently leaves a different, valid-looking regex behind. Measured twice on this PR.) That widening moves declarationsIn too — the one scan this card's before/after was priced to leave byte-identical — so it is a second population move with its own before/after to price.

Filed as #11630 and pinned in --self-test as deliberately unmoved, so that card flips a pin rather than finding none.

Verification

Base origin/main 3637731e2; measured and re-measured on final commit 505ecc609.

Derived gate union — node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; change set taken from the merge base by the script itself), 9 families, plus check:nul-bytes and the self-test directly. Exit codes captured before any pipe; each verdict below is the line the gate itself printed.

gate exit its own verdict line
pnpm check:cross-package-test-inputs 0 OK: 16 package(s) read outside themselves, all declared…
pnpm check:docs-audit-scope 0 ✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).
pnpm check:entry-guard 0 ✓ check:entry-guard: 142 scripts/ file(s) — every entry guard goes through invoked-as.mjs
pnpm check:parse-guard 0 ✓ check:parse-guard: 141 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
pnpm check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 129 assertions
pnpm check:pnpm-filter-targets 0 ✓ check:pnpm-filter-targets: 120/148 --filter occurrence(s)… resolve
node scripts/check-ci-filter-parity.mjs 0 OK: all 95 declared cross-package glob(s) (80 unique) are covered…
node scripts/check-cross-package-test-inputs.mjs 0 OK: 16 package(s) read outside themselves, all declared…
node scripts/docs-audit/check-affected-docs.mjs 0 ✓ affected-docs self-test: 395 cases pass.
pnpm check:nul-bytes 0 ✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
affected-docs.mjs --self-test 0 ✓ affected-docs self-test: 395 cases pass. (base: 381 — +14)

Lint — a declared, measured narrowing, not a skip. Repo-wide eslint . is CI's run; the narrowing is proven rather than assumed, with all three pieces of evidence:

  1. Population read from ESLint's own config, not guessed: ESLint#isPathIgnored says scripts/docs-audit/affected-docs.mjs is in the population (2 resolved rules) and scripts/docs-audit/README.md is not (ignoredByEslintConfig=true).
  2. File count from --format json: 2 entries, errors=0 warnings=1; the one warning is File ignored because no matching configuration was supplied. on the README — ESLint's notice about an out-of-population path, not a finding. Exit 0.
  3. Invariance for untouched files: this repo runs one eslint.config.mjs and it never enables type-aware linting for any file — no parserOptions.project, no projectService, no typed @typescript-eslint rules (0 occurrences; the config's own header documents the measurement with a positive control). A two-file diff therefore cannot move any untouched file's verdict.

No changeset: scripts/** publishes nothing (the root package is private and ships no files), matching every prior PR in this family (#11558, #11500, #10900, #10795). skip-changeset applied.


Generated by Claude Code


Generated by Claude Code

#11542)

Eight scans in `affected-docs.mjs` ask "is a `route:` / `client:` declaration
written here?". `declLead` has spelled the colon and the run after it once since
#11494, but the KEY stayed each call site's own argument: `declarationsIn`
anchored it with `\b` and the other seven did not. So `subroute: 'GET /x'` was a
declaration to SEVEN of the eight and not to the eighth, and it minted a silent
phantom ROW — silent because the partial-read verdict keys on the gap between
`rows` and `routesDeclared` and both terms read the unanchored spelling.

The anchor now lives in `declLead`, once, and `declarationsIn` stops restating
it. Priced against the `--bridge-coverage` before/after standard: 0 divergent
leads across the seven live ledgers, and the census is byte-identical row for
row (269 of 269 / 222 of 222 / 45 reachable / 177 UNREACHABLE / 0 prose-quoted
leads / brokenScan 0).

`--self-test` moves the pin #11584 left rather than deleting it, adds one
fixture per previously-unanchored scan, and pins the `$route:` residue (#11630)
where the next card will find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 10:16
@os-steve
os-steve added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit d744340 Aug 24, 2026
39 checks passed
@os-steve
os-steve deleted the claude/issue-11542-phantom-row-key-anchor branch August 24, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs-audit: subroute: / myclient: mints a silent PHANTOM ROW — only declarationsIn anchors the key with \b, the other seven scans do not

2 participants