Skip to content

fix(docs-audit): admit only leaf symbols into the route bridge — a container name in a handler is not that route's implementation - #9431

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9294-anchor-line-scope
Aug 18, 2026
Merged

fix(docs-audit): admit only leaf symbols into the route bridge — a container name in a handler is not that route's implementation#9431
os-steve merged 1 commit into
mainfrom
claude/issue-9294-anchor-line-scope

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9294

Mechanism — measured, and it is neither candidate the card named

The card offered two hypotheses with different fixes and asked for a measurement first. Both are wrong, and the real chain is a third thing:

  • (a) "the ledger hop is file-scoped" — FALSE. The ledger hop is a faithful lookup: it only maps routes that are already in routeAnchors to their declared client method. And the literal-route derivation (literalAnchorsFromLines) is already strictly line-scoped — it reads route literals off changed lines only. /book/:name/tree never entered through either.
  • (b) "the enclosing-declaration walk falls back to the file" — FALSE. There is no file fallback. What is true is narrower: a changed line inside a doc comment has no declaration of its own (declarationOn refuses comment bodies), and the indent walk then climbs past every sibling member to the class. So a doc-comment edit above a private method attributes to RestServer — correctly, per the card's own ruling on that row.

The defect is one hop later, in PHASE 2 of the bridge.parseRegistrarSource tests the premise "this symbol IS some route's implementation" by scanning a registrar handler window for the bare identifier. A container name satisfies that scan without satisfying the premise. Measured on 9e2e68206 (the squash-merge of PR #9291):

RestServer -> rest-server.ts:4211 RestServer.metaTypeSingular(...) -> /:type/:name/layers
RestServer -> rest-server.ts:4286 RestServer.anyPermissionSetAudience(...) -> /book/:name/tree
ledger -> meta.getBookTree / getBookTree

Three wrong rows off a static-call qualifier ~1350 lines from any changed line. Two routes is under MAX_ROUTES_PER_SYMBOL (3), so the cross-cutting cap never fired.

Fix

A symbol derived from a container declaration (class, interface, type, enum, namespace, or a const object that owns its keys) is admitted as a doc anchor exactly as before, but no longer enters the route bridge. It is the same shape of carve-out the file already applies to SCREAMING_SNAKE constants one line above, and it generalises the reason: a container is the scope a route's implementation lives in, never the implementation.

documentableDeclarationsAt now reports container alongside the name, and symbolAnchorsFromSource returns { names, bridgeable }bridgeable positive rather than subtractive, so a name that reached the set through a leaf derivation anywhere stays bridgeable.

Nothing else moves: most-specific-wins already anchors a changed method body on the method, and a method is a leaf, so auditMetaItem to /:type/:name/audit to meta.getAudit still bridges.

Measured, before and after — verified at f05810a75

Tool generations: coarse = the pre-#9192 package-mention predicate · main = origin/main at a4331227b · fixed = this branch.

commitcoarsemainfixeddelta
9e2e68206 (the card's specimen)135 docs / 6 anchors3 docs / 2 anchorsremoved api/client-sdk.mdx, releases/v14.mdx
30b1c636a (#9192 headline)11344none
3851f87f0 (#9192 headline)1161918removed ui/public-data-collection.mdx
07ad42463 (#9192 headline)2211none

On the specimen, all three wrong rows are gone and so is a fourth wrong anchor nobody had noticed — /:type/:name/layers (route), which matched no page and so never showed up in the advisory. RestServer (symbol) survives on all three release pages, which is the row the card ruled correct.

07ad42463 reads 1 rather than the card's 0 because #9230 later added the command anchor kind; that is #9230's recall win, unchanged here.

The one other row that moved is a SECOND instance of the same defect, not a regression

3851f87f0 lost content/docs/ui/public-data-collection.mdx, whose only anchor was /forms/:slug/submit (route). Tracing it: SecurityPlugin reached the anchor set from 17 changed doc-comment lines (the JSDoc at security-plugin.ts:5514, the identical doc-comment-attributes-to-the-class path), and then bridged because two lines inside that route's handler window are English prose comments

rest-server.ts:7870 // ... The SecurityPlugin's publicFormGrant
rest-server.ts:7886 // ... The SecurityPlugin honors `publicFormGrant` (create + the

The page has zero occurrences of maskingRule, FieldMasker, keepHead, keepTail or mask — it documents nothing that diff touched. Measured independently and months apart from the card's specimen, so the blast radius is wider than one PR.

Self-test — 197 to 212, pinned in both directions

The removal direction alone would pass just as happily on an over-correction that also drops the correct row, so the new fixture pins four things at once: the class stays a symbol anchor, the class is not bridgeable, the method stays bridgeable, and the identifier scan still sees the qualifier — that last one is what stops the block going green because the fixture drifted into deriving no route at all. Plus a counterfactual holding the pre-fix behaviour: the raw anchor set still selects /book/:name/tree, so the test cannot pass for the wrong reason.

Verification

Run after the final commit, at f05810a75:

node scripts/docs-audit/affected-docs.mjs --self-test -> 212 cases pass
pnpm check:docs-audit-scope -> 212 + 22 cases, scope in sync (178 docs), 9 release pages read-only
node scripts/docs-audit/check-affected-docs.mjs -> 212 cases pass
pnpm check:nul-bytes -> 75 assertions; 6122 files, no raw control bytes

Gate family re-derived from the actual changed path with node scripts/pm/dispatch-gates.mjs scripts/docs-audit/affected-docs.mjs; it named exactly the two above and nothing new.

Scope

One file. skip-changeset: scripts/docs-audit/ is repo tooling, not a published package, so this PR releases nothing.

#9331 is not addressed here and nothing in this diff goes near it — that card is about validating an anchor against its target page at report time; this one is about which anchors a diff is entitled to produce on the source side. No overlap in the code paths touched.

Two things deliberately left alone, both worth a separate look rather than a rider on this PR:

  1. The bridge-eligibility exclusions are silent — neither the pre-existing SCREAMING_SNAKE rule nor this container rule publishes what it withheld, while the cross-cutting cap publishes crossCuttingSymbols and the drift comment renders it. Adding a field would need docs-drift-check.yml to render it, which is outside this card's file surface.
  2. parseRegistrarSource scans handler windows for identifiers including inside comments, which is what let an English sentence carry SecurityPlugin into a route. This fix removes the class of symbol that made that reachable; it does not make the scan comment-aware.

Generated by Claude Code

A symbol derived from a CONTAINER declaration (class / interface / type / enum /
namespace / key-owning const) is a doc anchor but not a route's implementation, so
it no longer feeds the symbol -> route -> sdk bridge.
Measured on 9e2e682: a 27-line edit confined to `RestServer.probeMcpServeable`
(17 of them its doc comment, which attributes to the enclosing class) put
`RestServer` in the anchor set; two handlers ~1350 lines away call `RestServer.`
statics, and the bridge's bare-identifier handler scan read that qualifier as
"this handler implements the changed symbol". Result: `/book/:name/tree`,
`/:type/:name/layers`, `getBookTree` and `meta.getBookTree`. Two routes is under
MAX_ROUTES_PER_SYMBOL, so the cross-cutting cap never fired.
The `RestServer (symbol)` row is correct and survives untouched — only the bridge
hop is cut. Self-test 197 -> 212, pinned in both directions including a
counterfactual that the raw anchor set still selects the book route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@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.

@os-steveos-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026 — with Claude
@os-steve
os-steve marked this pull request as ready for review August 18, 2026 01:16
@os-steve
os-steve added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 5f5c586Aug 18, 2026
25 checks passed
@os-steve
os-steve deleted the claude/issue-9294-anchor-line-scope branch August 18, 2026 01:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve