From 7f200bcd6edbf328c9c155d9b509d78f9f2b8d37 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 07:55:31 +0000 Subject: [PATCH 1/4] refactor(docs-audit): a route SOURCE of two kinds, and admit contract declarations Rename the "registrar" concept in affected-docs.mjs to a route SOURCE with two kinds -- a registration call site (the existing filename convention) and a spec contract declaration (admitted by evidence) -- and implement the evidence route behind a runtime-registration guard. The guard is the HTTP method the declaration answers on, read beside the path, rather than a hand-kept exclusion list: it keeps out the whole class of data payloads that merely carry a `path:` key, of which the connector-action input in packages/spec/src/conversions/registry.ts is today's only instance. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/docs-audit/affected-docs.mjs | 328 +++++++++++++++++++-------- 1 file changed, 237 insertions(+), 91 deletions(-) diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index 965c95f05f..adf56b1f5d 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -588,19 +588,51 @@ function qualifyDataProperty(inner, outer, surface) { } /** - * Where route REGISTRARS live. Deliberately a filename convention rather than a hand-kept - * file list — the same choice the package-root derivation made for the same reason (#4162: - * a hardcoded list fails again on container number eight). A registrar this misses costs - * recall on the `sdk` anchor kind only. + * A ROUTE SOURCE — a file whose source DECLARES a route, so a change to it may nominate + * docs. There are TWO KINDS, and keeping them named apart is the point (#11857, maintainer + * ruling 2026-09-04 batch #31): + * + * (a) a registration CALL SITE — the filename convention in `CALL_SITE_FILE_RE` below. + * The file that MOUNTS the route on a server at runtime. + * (b) a spec contract DECLARATION — a non-test `packages/**` file whose masked source + * declares a route beside the HTTP method it answers (`ROUTE_METHOD_RE`). The file + * that DECLARES the contract a registration serves. + * + * ⛔ "REGISTRAR" MUST NOT MEAN BOTH, which is why nothing here is called one any more. + * Until this card the word meant kind (a) alone — the recognizer, its docblock and its + * `--self-test` all said "the file that registers the route" — and the measured widening + * that motivated this change admits five `packages/spec` Zod contract declarations, which + * register nothing with anything. Stretching the old word over them would have left one + * term denoting two constructs in a tool whose whole job is telling declared surfaces + * apart; the ruling made the rename a CONDITION of admitting them, not a tidy-up after. + * + * WHY KIND (b) IS A SOURCE AND NOT A WIDENING OF KIND (a). The drift check exists to say + * "the contract changed, re-verify the manual". A Zod API declaration in `packages/spec` + * IS the contract — arguably the better source of truth than the mount that serves it — + * so admitting it narrows the class of rows this tool STRUCTURALLY cannot see. It does not + * make those files registration call sites, and no convention widening would reach them: + * `storage.zod.ts` is not going to be renamed `storage-routes.ts`. + * + * ⛔ THE TWO KINDS ARE NOT INTERCHANGEABLE AT THE ADMISSION TEST. Kind (a) is admitted by + * its NAME and contributes every tail it declares; kind (b) is admitted by EVIDENCE and + * contributes only the tails carrying a runtime-registration signal — see + * `ROUTE_METHOD_RE` for what that buys and what it keeps out. + */ + +/** + * Kind (a): where route registration CALL SITES live. Deliberately a filename convention + * rather than a hand-kept file list — the same choice the package-root derivation made for + * the same reason (#4162: a hardcoded list fails again on container number eight). A call + * site this misses costs recall on the `sdk` anchor kind only. * * ⛔ THE SECOND HALF OF THAT SENTENCE USED TO READ "and `anchorlessChanges` reports the * silence". It does not, and #9572 measured why: `anchorlessChanges` fires per changed - * FILE that yielded ZERO anchors, while a handler change in a missed registrar yields its + * FILE that yielded ZERO anchors, while a handler change in a missed call site yields its * own symbol anchors, so the run is never anchorless and prints nothing. The silence was * reported by no field at all until `bridgeCoverage` existed. Keep that distinction in * mind before leaning on any other field to "report" a gap: the populations differ. */ -const REGISTRAR_FILE_RE = /(?:^|\/)(?:[\w.-]*route[\w.-]*|[\w.-]*-server)\.ts$/; +const CALL_SITE_FILE_RE = /(?:^|\/)(?:[\w.-]*route[\w.-]*|[\w.-]*-server)\.ts$/; /** Route LEDGERS — the declared `route` ⟷ `client` tables the `sdk` anchor rides on. */ const LEDGER_FILE_RE = /(?:^|\/)[\w.-]*route-ledger\.ts$/; @@ -622,8 +654,46 @@ const selectsFrom = (tails) => { return (route) => tailList.some((t) => route.replace(/^[A-Z*]+\s+/, '').endsWith(t)); }; -/** How far past a `path:` line a registrar's handler body is scanned for identifiers. */ -const REGISTRAR_HANDLER_WINDOW = 150; +/** How far past a `path:` line a route source's handler body is scanned for identifiers. */ +const ROUTE_SOURCE_HANDLER_WINDOW = 150; + +/** + * THE RUNTIME-REGISTRATION SIGNAL that admits a kind (b) route source: the declaration + * names the HTTP METHOD it answers, beside the path it answers on. + * + * A route declaration says WHAT VERB it serves; a data payload that happens to carry a + * `path:` key does not. That is the whole separation, and it is EVIDENCE READ OFF THE + * DECLARATION rather than a hand-kept exclusion list — the same choice, for the same + * reason, as `CALL_SITE_FILE_RE` and `packageRootOf` (#4162: a list fails again on the + * eighth one). + * + * ⛔ THE EXCLUSION THIS REPLACES WOULD HAVE BEEN A FILE NAME. The one non-route the + * evidence route admits on today's tree is `packages/spec/src/conversions/registry.ts`, + * whose `/api/v1/health` is a connector-action INPUT inside an automation fixture — + * `config: { input: { path: '/api/v1/health' } }`. Naming that file would have kept it out + * and taught the recognizer nothing; the signal keeps out its whole CLASS. Measured on + * `460134af8`: of that file's 61 literal `path:` sites, ZERO carry an HTTP method, while + * the five spec contract declarations carry one at 61 of their 62 sites — and the 62nd is + * a site whose `method:` sits five lines up behind a JSDoc block, which the blank-skipping + * below reaches. The separation is total at every lookaround from 1 to 6, so the number is + * not load-bearing; it is a margin, not a threshold. + * + * ⚠️ THIS IS AN ADMISSION TEST, NOT A REACH TEST. It runs per SITE and only for kind (b). + * Kind (a) is untouched — a call site admitted by name still contributes every tail it + * declares, exactly as before this card, so no reach this tool had can be lost here. + */ +const ROUTE_METHOD_RE = /(?:^|[\s{,(])method\s*:\s*(['"`])(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\1/i; + +/** + * How many NON-BLANK lines either side of a `path:` site are read for the signal above. + * + * Non-blank is what makes this a property-list test rather than a line-distance one: + * `maskComments` blanks a comment to whitespace WITHOUT moving any line, so a JSDoc + * between `method:` and `path:` reads as blank here and the two properties stay adjacent + * — which is exactly the shape of the one site in `plugin-rest-api.zod.ts` that a naive + * line-distance window misses. + */ +const ROUTE_METHOD_LOOKAROUND = 4; /** * Above this many routes, a changed symbol is a CROSS-CUTTING helper rather than one @@ -689,7 +759,7 @@ const OCLIF_COMMANDS_DIR = 'src/commands'; * inside the prose it describes and is deleted with it. That is the whole difference from * the hand-kept registry of canonical-rule sites #9282 considered and rejected — a second * source of truth beside the rule drifts from it silently (the same reason `packageRootOf`, - * `REGISTRAR_FILE_RE` and `commandIdFor` are all derived rather than listed). + * `CALL_SITE_FILE_RE` and `commandIdFor` are all derived rather than listed). * * The opt-in itself is deliberate and is the only authored bit. Deriving from EVERY doc * comment in `packages/**` would hand the corpus-share guard a flood to filter rather than @@ -852,14 +922,14 @@ if (args.includes('--self-test')) { // a complete read) is broken rather than clean, and no such verdict can fire on a tree // where the scan works at all. if (args.includes('--bridge-coverage')) { - const { registrarFiles, sourceFiles, ledgers, registrarByTail } = scanRouteSurface(); + const { routeSources, sourceFiles, ledgers, routeSourceByTail } = scanRouteSurface(); // THE CEILING (#11178) — read lazily off the walk this scan already did, so the census // holds one file's source at a time rather than the tree's. Since #11867 the PHASE 2 // advisory run pays for it too, through this same `ceilingTailsFrom` — one derivation, // so the two paths cannot report the same three buckets from two populations. const ceiling = ceilingTailsFrom(sourceFiles); - const coverage = bridgeCoverageFrom(ledgers, registrarByTail.keys(), ceiling.keys()); - const selects = selectsFrom(registrarByTail.keys()); + const coverage = bridgeCoverageFrom(ledgers, routeSourceByTail.keys(), ceiling.keys()); + const selects = selectsFrom(routeSourceByTail.keys()); const causeOf = new Map(coverage.ledgers.map((l) => [l.file, l.cause])); // Through the one definition, per tail, so naming a witness cannot drift from the rule // that decided the row was remediable in the first place (⛔ never restate the suffix test). @@ -867,7 +937,7 @@ if (args.includes('--bridge-coverage')) { if (asJson) { process.stdout.write(JSON.stringify({ ...coverage, - registrarFiles: registrarFiles.filter((f) => !LEDGER_FILE_RE.test(f)), + routeSources, unreachableRows: ledgers.flatMap(({ file, rows }) => rows.filter((r) => r.client && !selects(r.route)).map((r) => { const witnesses = witnessesFor(r.route); @@ -880,7 +950,14 @@ if (args.includes('--bridge-coverage')) { }, null, 2) + '\n'); } else { console.log(`sdk route bridge — reach over the declared client-bound surface`); - console.log(` registrar files scanned .... ${registrarFiles.filter((f) => !LEDGER_FILE_RE.test(f)).length}`); + // BOTH KINDS, NAMED, NEVER ONE OPAQUE TOTAL (#11857). The two are admitted by + // different tests — a filename for one, evidence for the other — so a count that + // merged them would hide exactly the movement this card exists to make legible: a + // contract declaration appearing or disappearing reads as "the convention changed". + const byKind = (k) => routeSources.filter((r) => r.kind === k).length; + console.log(` route sources scanned ...... ${routeSources.length}`); + console.log(` registration call sites .${String(byKind('call-site')).padStart(4)} admitted by the filename convention`); + console.log(` contract declarations ...${String(byKind('contract')).padStart(4)} admitted by evidence: a route declared beside its HTTP method`); console.log(` route tails produced ....... ${coverage.tails}`); console.log(` ledger files ............... ${coverage.ledgers.length}`); // BOTH HALVES OF THE FRACTION, ALWAYS. A bare "221" cannot be told apart from a 221 @@ -1203,7 +1280,7 @@ const liveManifestIo = { * from paths — topic = directory, command = filename — so the machine-readable registry * this anchor kind needs ALREADY EXISTS as the convention, and a hand-kept ledger beside * it would be a second source of truth that drifts silently (the same reasoning that made - * `packageRootOf` and `REGISTRAR_FILE_RE` filesystem-derived rather than listed). + * `packageRootOf` and `CALL_SITE_FILE_RE` filesystem-derived rather than listed). * * Shapes it handles, all mechanically: * - `build.ts` → `build` (a top-level command) @@ -1596,6 +1673,28 @@ function symbolAnchorsFromSource(text, changed, surface = liveContainerSurface() return { names, bridgeable, from }; } +/** + * Does the `path:` site at `i` carry the runtime-registration signal — an HTTP `method:` + * inside its own property list? See `ROUTE_METHOD_RE` for why the verb is the evidence. + * + * Reads OUTWARD over NON-BLANK lines in both directions, plus the site's own line (a + * one-line `{ method: 'GET', path: '/api/x' }` is one property list, not two). `lines` + * must already be MASKED — the blank-skipping is what steps over a JSDoc sitting between + * the two properties, and on unmasked source those lines are not blank. + */ +function hasRouteMethodSignal(lines, i) { + if (ROUTE_METHOD_RE.test(lines[i])) return true; + for (const step of [-1, 1]) { + let seen = 0; + for (let j = i + step; j >= 0 && j < lines.length && seen < ROUTE_METHOD_LOOKAROUND; j += step) { + if (!lines[j].trim()) continue; + seen++; + if (ROUTE_METHOD_RE.test(lines[j])) return true; + } + } + return false; +} + /** * `path:` literals in a route registrar, each mapped to the identifiers its handler body * mentions. This is the mechanical half of the SDK bridge: a changed protocol method @@ -1633,7 +1732,7 @@ function symbolAnchorsFromSource(text, changed, surface = liveContainerSurface() * anchor there are DOC-COMMENT-only, so `publishItem`'s behaviour never moved, and the page * names none of the three symbols that did. */ -function parseRegistrarSource(text) { +function parseRouteSource(text, { requireMethodSignal = false } = {}) { const lines = maskComments(text).split('\n'); const sites = []; for (let i = 0; i < lines.length; i++) { @@ -1646,14 +1745,18 @@ function parseRegistrarSource(text) { // NON-LITERAL `path:` line behave the same way, which is the whole change. if (!/(?:^|[\s{,(])path\s*:/.test(lines[i])) continue; const m = lines[i].match(/(?:^|[\s{,(])path\s*:\s*([`'"])(.*?)\1/); - sites.push({ line: i, tail: m ? routeTailOf(m[2]) : null }); + sites.push({ line: i, tail: m ? routeTailOf(m[2]) : null, signalled: hasRouteMethodSignal(lines, i) }); } const byTail = new Map(); for (let k = 0; k < sites.length; k++) { - const { line, tail } = sites[k]; - if (!tail) continue; + const { line, tail, signalled } = sites[k]; + // ⚠️ A SITE DROPPED FOR THE TAIL STILL BOUNDS THE PREVIOUS WINDOW. `next` is read off + // the SITES array, never off the tails, so requiring the signal narrows what a file + // CONTRIBUTES without moving where any other site's handler window ends (#9503 — + // the same reason a `path:` with no literal was always kept in this array). + if (!tail || (requireMethodSignal && !signalled)) continue; const next = k + 1 < sites.length ? sites[k + 1].line : lines.length; - const end = Math.min(next, line + REGISTRAR_HANDLER_WINDOW, lines.length); + const end = Math.min(next, line + ROUTE_SOURCE_HANDLER_WINDOW, lines.length); let ids = byTail.get(tail); if (!ids) byTail.set(tail, (ids = new Set())); for (let j = line; j < end; j++) { @@ -1668,7 +1771,7 @@ function parseRegistrarSource(text) { * ignored entirely — the CEILING on what a widened discovery could ever reach (#11178). * * ⛔ THIS IS NOT A SECOND DISCOVERY ROUTE, and nothing downstream of it selects a row. - * `REGISTRAR_FILE_RE` is untouched, the bridge still rides on `registrarByTail` alone, and + * `CALL_SITE_FILE_RE` is untouched, the bridge still rides on `routeSourceByTail` alone, and * the published `reachable` figure is computed exactly where it was. This function exists * only so the REPORT can name WHY a row is unreachable, which the report could not do * while it had one number for two causes: @@ -1684,11 +1787,11 @@ function parseRegistrarSource(text) { * discovery, whose own remedy was then measured to move the auth ledger by ZERO rows. * * SUPERSET BY CONSTRUCTION, which is what makes the comparison legitimate: the same - * `parseRegistrarSource`, over the same walk's files, minus the convention test. So every + * `parseRouteSource`, over the same walk's files, minus the convention test. So every * tail discovery yields appears here too, and `reachable` can never exceed this ceiling — * an invariant `bridgeCoverageFrom` turns into a broken-scan verdict rather than trusting. * - * The `includes('path')` prefilter is a SOUND superset, not a heuristic: `parseRegistrarSource` + * The `includes('path')` prefilter is a SOUND superset, not a heuristic: `parseRouteSource` * yields a tail only from a line matching `path\s*:` in the MASKED source, and masking * replaces bytes with spaces rather than inserting any, so those four bytes must be present * in the raw text for any tail to exist. Positive control on `589758d22`: masking all 1930 @@ -1723,7 +1826,7 @@ function maximalTailsFrom(sources) { const byTail = new Map(); for (const { file, text } of sources) { if (!text.includes('path')) continue; - for (const [tail] of parseRegistrarSource(text)) { + for (const [tail] of parseRouteSource(text)) { let owners = byTail.get(tail); if (!owners) byTail.set(tail, (owners = [])); owners.push(file); @@ -1739,7 +1842,7 @@ function maximalTailsFrom(sources) { * WHY THIS IS REPORTED AND NOT INFERRED. The bridge's one hop from a changed symbol to * `api/client-sdk.mdx` is `registrar tail` ⟶ `ledger row`, and a ledger row no registrar * tail can select is STRUCTURALLY unreachable: no symbol change bridges to it, ever. The - * file's own note on `REGISTRAR_FILE_RE` says a missed registrar "costs recall on the + * file's own note on `CALL_SITE_FILE_RE` says a missed registrar "costs recall on the * `sdk` anchor kind only, and `anchorlessChanges` reports the silence" — measured on * `9ff11921a`, neither half of that holds. `anchorlessChanges` fires per changed FILE * with ZERO anchors, and a handler change yields its own symbol anchors, so the run is @@ -1922,7 +2025,7 @@ function bridgeCoverageFrom(ledgers, tails, maximalTails) { * * ⭐ AND THE EXCLUSION IS RIGHT FOR THE CEILING TOO, which is the question this walk's * two consumers make live. `sourceFiles` is the #11178 ceiling population and - * `registrarFiles` is the bridge's own discovery — one walk, both. It is tempting to + * `conventionFiles` is the bridge's own discovery — one walk, both. It is tempting to * argue the ceiling wants the WIDEST possible population and so should keep test * directories; it does not, because of what the ceiling's verdict MEANS. A row counted * `remediable by discovery` is a claim that widening the FILENAME CONVENTION would reach @@ -1938,10 +2041,14 @@ function bridgeCoverageFrom(ledgers, tails, maximalTails) { * @param {string} root the tree to walk (`packages/**` under it); paths come back * relative to it, which is what `isTestFile` and both file regexes are defined over. * @param {(dir: string, opts: {withFileTypes: true}) => Array<{name: string, isFile(): boolean, isDirectory(): boolean}>} [readDir] - * @returns {{registrarFiles: string[], sourceFiles: string[]}} + * @returns {{conventionFiles: string[], sourceFiles: string[]}} */ function walkSourceFiles(root, readDir = readdirSync) { - const registrarFiles = []; + // The files the FILENAME CONVENTION picks out — route ledgers and kind (a) route + // sources, in one list because one walk fills both (#4851). ⛔ NOT the route-source + // population: kind (b) is admitted by EVIDENCE in `scanRouteSurface`, off `sourceFiles`, + // and no filename convention can see it. + const conventionFiles = []; // EVERY candidate the convention CHOSE FROM, collected in the same walk (#11178). This // is not a second discovery route and nothing downstream of the bridge reads it: it is // the population `maximalTailsFrom` measures the convention against, so that "the @@ -1961,11 +2068,11 @@ function walkSourceFiles(root, readDir = readdirSync) { const rel = relative(root, p); if (isTestFile(rel) || isMigrationLedgerEntry(rel)) continue; sourceFiles.push(rel); - if (LEDGER_FILE_RE.test(rel) || REGISTRAR_FILE_RE.test(rel)) registrarFiles.push(rel); + if (LEDGER_FILE_RE.test(rel) || CALL_SITE_FILE_RE.test(rel)) conventionFiles.push(rel); } }; walkSrc(join(root, 'packages')); - return { registrarFiles, sourceFiles }; + return { conventionFiles, sourceFiles }; } /** @@ -1974,12 +2081,24 @@ function walkSourceFiles(root, readDir = readdirSync) { * same walk — a second walk here is exactly the drift #4851 billed us for. */ function scanRouteSurface() { - const { registrarFiles, sourceFiles } = walkSourceFiles(repoRoot); + const { conventionFiles, sourceFiles } = walkSourceFiles(repoRoot); const ledgers = []; const ledgerRows = []; - const registrarByTail = new Map(); - for (const rel of registrarFiles) { + const routeSourceByTail = new Map(); + // Every admitted route source WITH THE KIND THAT ADMITTED IT, so the report can say + // which of the two definitions reached a row rather than printing one opaque count. + const routeSources = []; + const absorb = (byTail) => { + for (const [tail, ids] of byTail) { + let acc = routeSourceByTail.get(tail); + if (!acc) routeSourceByTail.set(tail, (acc = new Set())); + for (const id of ids) acc.add(id); + } + }; + + // KIND (a) — the filename convention, unchanged. Ledgers ride the same list. + for (const rel of conventionFiles) { let text; try { text = readFileSync(join(repoRoot, rel), 'utf8'); } catch { continue; } if (LEDGER_FILE_RE.test(rel)) { @@ -1987,15 +2106,42 @@ function scanRouteSurface() { ledgers.push({ file: rel, rows, declined, routesDeclared, clientsDeclared, outsideCode }); ledgerRows.push(...rows); } - if (REGISTRAR_FILE_RE.test(rel)) { - for (const [tail, ids] of parseRegistrarSource(text)) { - let acc = registrarByTail.get(tail); - if (!acc) registrarByTail.set(tail, (acc = new Set())); - for (const id of ids) acc.add(id); - } + // ⛔ A ROUTE LEDGER IS NOT A ROUTE SOURCE, even though most ledger names match the + // call-site convention (`route-ledger.ts` carries the word `route`). A ledger is the + // declared TABLE tails are matched INTO; a route source is a file that declares a + // route. Before this card the combined convention list meant every ledger was also + // parsed as a registrar — harmless, because ledgers declare `route:` rows and not + // `path:` sites, but it is the sort of overlap that makes one word mean two things. + // + // MEASURED, NOT ASSUMED: on `460134af8` all 11 live route ledgers yield ZERO route + // tails, so narrowing this predicate moves no tail at all. `--self-test` pins that + // against the LIVE tree rather than trusting the sentence, so the day a ledger starts + // declaring a `path:` route it reds here instead of losing the tail in silence. + if (CALL_SITE_FILE_RE.test(rel) && !LEDGER_FILE_RE.test(rel)) { + routeSources.push({ file: rel, kind: 'call-site' }); + absorb(parseRouteSource(text)); } } - return { registrarFiles, sourceFiles, ledgers, ledgerRows, registrarByTail }; + + // KIND (b) — the CONTRACT DECLARATION, admitted by evidence (#11857). Same walk, same + // parser, same `includes('path')` prefilter that makes `maximalTailsFrom`'s superset + // sound: masking replaces bytes with spaces and inserts none, so those four bytes must + // be present in the raw text for any tail to exist. + // + // ⛔ A LEDGER IS NOT A ROUTE SOURCE and a call site is not counted twice — both are + // skipped here, so `routeSources` partitions cleanly by kind. + for (const rel of sourceFiles) { + if (LEDGER_FILE_RE.test(rel) || CALL_SITE_FILE_RE.test(rel)) continue; + let text; + try { text = readFileSync(join(repoRoot, rel), 'utf8'); } catch { continue; } + if (!text.includes('path')) continue; + const byTail = parseRouteSource(text, { requireMethodSignal: true }); + if (!byTail.size) continue; + routeSources.push({ file: rel, kind: 'contract' }); + absorb(byTail); + } + + return { conventionFiles, routeSources, sourceFiles, ledgers, ledgerRows, routeSourceByTail }; } /** @@ -2745,13 +2891,13 @@ function selfTest() { // // ⛔ NON-VACUITY IS THE ENTIRE POINT OF THESE FIXTURES, and it is why none of them is a // real repo path. The live population is ZERO — measured on `d63b01436`, the three files - // under a test directory that the old walk admitted match neither `REGISTRAR_FILE_RE` + // under a test directory that the old walk admitted match neither `CALL_SITE_FILE_RE` // nor `LEDGER_FILE_RE` and declare no `path:`, so `--bridge-coverage` is byte-identical // across this fix. A pin built from real paths would therefore have passed just as // green with the bug in place and pinned NOTHING. Every fixture below is instead a file // the BASENAME test admits and the PATH test excludes: `x-route.ts` carries no `.test.` // / `.spec.` infix, so under the old call site it was walked, matched - // `REGISTRAR_FILE_RE`, and a test double contributed production route tails — the exact + // `CALL_SITE_FILE_RE`, and a test double contributed production route tails — the exact // failure this is here to keep out. Verified RED against the pre-fix line: 4 registrar // files and 6 source files, against the 1 and 2 asserted here. battery('the WALK\'s admission decision, against a fake tree (#11866)'); @@ -2770,7 +2916,7 @@ function selfTest() { 'packages/foo/README.md', // not a .ts file // #12966. NON-VACUOUS BY CONSTRUCTION, like every fixture above it: neither entry // carries a test infix or lives under a test directory, so BOTH are walked under the - // previous line and BOTH matched `REGISTRAR_FILE_RE` — an ADR-0049 tombstone + // previous line and BOTH matched `CALL_SITE_FILE_RE` — an ADR-0049 tombstone // recording that `contributes.routes` was DELETED was being counted as a file that // registers routes. Reverting the exclusion turns the two equality checks below red. 'packages/foo/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.routes.ts', @@ -2803,40 +2949,40 @@ function selfTest() { const walked = walkSourceFiles(fakeRoot, fakeReadDir); const sorted = (a) => [...a].sort().join(' | '); check('walkSourceFiles', 'a registrar-NAMED file under __tests__/ is NOT a registrar — the walk tests the path', - 'registrarFiles', 'packages/foo/src/migrations/runner-route.ts | packages/foo/src/real-route.ts', sorted(walked.registrarFiles)); + 'conventionFiles', 'packages/foo/src/migrations/runner-route.ts | packages/foo/src/real-route.ts', sorted(walked.conventionFiles)); check('walkSourceFiles', 'and the three test directories contribute nothing to the CEILING population either', 'sourceFiles', 'packages/foo/src/engine.ts | packages/foo/src/migrations/runner-route.ts | packages/foo/src/real-route.ts', sorted(walked.sourceFiles)); // Per-fixture, so a regression NAMES the arm that came back rather than only the totals. const excludedFixtures = [ - ['packages/foo/src/__tests__/x-route.ts', 'a __tests__/ file matching REGISTRAR_FILE_RE'], + ['packages/foo/src/__tests__/x-route.ts', 'a __tests__/ file matching CALL_SITE_FILE_RE'], ['packages/foo/src/__mocks__/fake-server.ts', 'a __mocks__/ file matching the `-server` alternative'], ['packages/foo/src/__fixtures__/stub-route-ledger.ts', 'a __fixtures__/ file matching LEDGER_FILE_RE'], ['packages/foo/src/__tests__/helper.ts', 'a __tests__/ helper with no registrar name'], // #11857. NON-VACUOUS BY CONSTRUCTION, exactly as the four above are: neither // carries a .test. / .spec. infix, so under the PREVIOUS predicate both were - // walked - `stub-route.ts` then matched `REGISTRAR_FILE_RE` and a test fixture + // walked - `stub-route.ts` then matched `CALL_SITE_FILE_RE` and a test fixture // contributed production route tails, and `engine.bench.ts` entered the ceiling // population. Re-admitting either arm breaks the two equality checks above, and // these two rows name WHICH arm came back. - ['packages/foo/test/fixtures/stub-route.ts', 'a non-underscore test/fixtures/ file matching REGISTRAR_FILE_RE'], + ['packages/foo/test/fixtures/stub-route.ts', 'a non-underscore test/fixtures/ file matching CALL_SITE_FILE_RE'], ['packages/foo/src/engine.bench.ts', 'a .bench.ts benchmark'], // #12966, one row per entry so a regression NAMES which one came back. ['packages/foo/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.routes.ts', - 'an ADR-0049 retired-keys tombstone matching REGISTRAR_FILE_RE'], + 'an ADR-0049 retired-keys tombstone matching CALL_SITE_FILE_RE'], ['packages/foo/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts', - 'an ADR-0049 semantic entry matching REGISTRAR_FILE_RE'], + 'an ADR-0049 semantic entry matching CALL_SITE_FILE_RE'], ['packages/foo/src/migrations/entries/semantic/18.plain-rename.ts', 'a ledger entry with no registrar name — excluded from the CEILING too'], ]; for (const [rel, label] of excludedFixtures) { check('walkSourceFiles', `${label} is walked at all`, rel, false, walked.sourceFiles.includes(rel)); - check('walkSourceFiles', `${label} reaches the registrar list`, rel, false, walked.registrarFiles.includes(rel)); + check('walkSourceFiles', `${label} reaches the registrar list`, rel, false, walked.conventionFiles.includes(rel)); } check('walkSourceFiles', 'a genuine registrar still survives the walk', 'packages/foo/src/real-route.ts', - true, walked.registrarFiles.includes('packages/foo/src/real-route.ts')); + true, walked.conventionFiles.includes('packages/foo/src/real-route.ts')); check('walkSourceFiles', 'a migrations/ registrar OUTSIDE entries/ survives — the exclusion is the DIRECTORY class, not the word', 'packages/foo/src/migrations/runner-route.ts', true, - walked.registrarFiles.includes('packages/foo/src/migrations/runner-route.ts')); + walked.conventionFiles.includes('packages/foo/src/migrations/runner-route.ts')); check('walkSourceFiles', 'a .test.ts file is still excluded by the FILE arm', 'packages/foo/src/engine.test.ts', false, walked.sourceFiles.includes('packages/foo/src/engine.test.ts')); check('walkSourceFiles', 'node_modules/ and dist/ are still pruned', 'packages/foo/{node_modules,dist}/route.ts', @@ -3353,7 +3499,7 @@ function selfTest() { check('changedLineNumbers', 'old-side lines (a REMOVED export still anchors)', 'hunks', JSON.stringify([6376, 13396, 13397]), JSON.stringify(parsed.oldLines)); // The two declared tables the SDK bridge rides on. - const registrarSource = [ + const routeSourceFixture = [ 'this.routeManager.register({', " method: 'GET',", ' path: `${metaPath}/:type/:name/audit`,', @@ -3370,11 +3516,11 @@ function selfTest() { ' },', '});', ].join('\n'); - const registrar = parseRegistrarSource(registrarSource); - check('parseRegistrarSource', 'the audit route is indexed by its tail', 'tail', true, registrar.has('/:type/:name/audit')); - check('parseRegistrarSource', 'its handler symbols are captured', 'auditMetaItem', true, !!registrar.get('/:type/:name/audit')?.has('auditMetaItem')); - check('parseRegistrarSource', 'a handler does NOT absorb the NEXT route\'s symbols', 'historyMetaItem', false, !!registrar.get('/:type/:name/audit')?.has('historyMetaItem')); - check('parseRegistrarSource', 'the second route is indexed too', 'historyMetaItem', true, !!registrar.get('/:type/:name/history')?.has('historyMetaItem')); + const registrar = parseRouteSource(routeSourceFixture); + check('parseRouteSource', 'the audit route is indexed by its tail', 'tail', true, registrar.has('/:type/:name/audit')); + check('parseRouteSource', 'its handler symbols are captured', 'auditMetaItem', true, !!registrar.get('/:type/:name/audit')?.has('auditMetaItem')); + check('parseRouteSource', 'a handler does NOT absorb the NEXT route\'s symbols', 'historyMetaItem', false, !!registrar.get('/:type/:name/audit')?.has('historyMetaItem')); + check('parseRouteSource', 'the second route is indexed too', 'historyMetaItem', true, !!registrar.get('/:type/:name/history')?.has('historyMetaItem')); const ledgerSource = [ 'export const REST_ROUTE_LEDGER = [', @@ -4372,7 +4518,7 @@ function selfTest() { // THE CHAIN, each link real and each one but the last correct: a doc-comment line has no // declaration of its own and its indent walk climbs past every sibling member to the // CLASS, so `RestServer` enters the anchor set (a correct row — three release pages name - // that class) → the route bridge accepts it as a bridge symbol → `parseRegistrarSource` + // that class) → the route bridge accepts it as a bridge symbol → `parseRouteSource` // scans handler windows for the BARE IDENTIFIER and two unrelated handlers call // `RestServer.` statics → two route anchors → the ledger maps one to `meta.getBookTree`. // Two routes is UNDER `MAX_ROUTES_PER_SYMBOL`, so the cross-cutting cap never saw it. @@ -4405,10 +4551,10 @@ function selfTest() { ' }', '}', ].join('\n'); - const bookRegistrar = parseRegistrarSource(serverSource); - const bookIds = bookRegistrar.get('/book/:name/tree'); - check('parseRegistrarSource', 'the mechanism is real: a static-call QUALIFIER lands in the handler window', 'RestServer', true, !!bookIds?.has('RestServer')); - check('parseRegistrarSource', 'and so does the handler\'s own implementation symbol', 'anyPermissionSetAudience', true, !!bookIds?.has('anyPermissionSetAudience')); + const bookRouteSource = parseRouteSource(serverSource); + const bookIds = bookRouteSource.get('/book/:name/tree'); + check('parseRouteSource', 'the mechanism is real: a static-call QUALIFIER lands in the handler window', 'RestServer', true, !!bookIds?.has('RestServer')); + check('parseRouteSource', 'and so does the handler\'s own implementation symbol', 'anyPermissionSetAudience', true, !!bookIds?.has('anyPermissionSetAudience')); const docCommentLine = 3; // `* [#9120] Resolve the environment …` — inside the JSDoc const methodBodyLine = 6; // `return this.resolveRequestEnvironmentId(req);` @@ -4426,8 +4572,8 @@ function selfTest() { for (const [tail, ids] of registrarMap) if ([...symbols].some((sym) => ids.has(sym))) tails.push(tail); return tails; }; - check('bridge', 'a doc-comment-only edit inside a class selects no route at all', 'tails', JSON.stringify([]), JSON.stringify(tailsSelectedBy(bridgeableAt(serverSource, docCommentLine), bookRegistrar))); - check('bridge', 'the pre-fix behaviour, held as the counterfactual: the raw anchor set DID select the book route', 'tails', JSON.stringify(['/book/:name/tree']), JSON.stringify(tailsSelectedBy(anchorsAt(serverSource, docCommentLine), bookRegistrar))); + check('bridge', 'a doc-comment-only edit inside a class selects no route at all', 'tails', JSON.stringify([]), JSON.stringify(tailsSelectedBy(bridgeableAt(serverSource, docCommentLine), bookRouteSource))); + check('bridge', 'the pre-fix behaviour, held as the counterfactual: the raw anchor set DID select the book route', 'tails', JSON.stringify(['/book/:name/tree']), JSON.stringify(tailsSelectedBy(anchorsAt(serverSource, docCommentLine), bookRouteSource))); check('bridge', 'a changed protocol METHOD still selects its own route', 'tails', JSON.stringify(['/:type/:name/audit']), JSON.stringify(tailsSelectedBy(bridgeableAt(protocolSource, 9), registrar))); // The container/leaf split on the two fixtures the derivation is already pinned against, @@ -4467,7 +4613,7 @@ function selfTest() { // name — that last one is the counterfactual, and it is what stops this block going green // because the fixture drifted into carrying no comment at all. battery('the handler-window scan reads CODE, never PROSE (#9432)'); - const commentaryRegistrar = [ + const commentaryRouteSource = [ 'export class RestServer {', ' private registerPublishRoutes() {', ' this.routeManager.register({', @@ -4494,12 +4640,12 @@ function selfTest() { ' private describeRegistration() {}', '}', ].join('\n'); - const commentary = parseRegistrarSource(commentaryRegistrar); + const commentary = parseRouteSource(commentaryRouteSource); const publishIds = commentary.get('/:type/:name/publish'); - check('parseRegistrarSource', 'a leaf the handler actually CALLS is still seen — the bridge is untouched where its premise holds', 'publishMetaItem', true, !!publishIds?.has('publishMetaItem')); - check('parseRegistrarSource', 'a leaf named only in an English comment is NOT this route\'s implementation', 'promoteDraftForPublish', false, !!publishIds?.has('promoteDraftForPublish')); - check('parseRegistrarSource', 'a `//` inside a STRING opens no comment — this scan rides the shared scanner, not a private regex', 'base + publishMetaItem', true, !!publishIds?.has('base') && !!publishIds?.has('publishMetaItem')); - check('parseRegistrarSource', 'a `path:` inside a JSDoc @example is an illustration, not a registration', 'tails', JSON.stringify(['/:type/:name/publish']), JSON.stringify([...commentary.keys()])); + check('parseRouteSource', 'a leaf the handler actually CALLS is still seen — the bridge is untouched where its premise holds', 'publishMetaItem', true, !!publishIds?.has('publishMetaItem')); + check('parseRouteSource', 'a leaf named only in an English comment is NOT this route\'s implementation', 'promoteDraftForPublish', false, !!publishIds?.has('promoteDraftForPublish')); + check('parseRouteSource', 'a `//` inside a STRING opens no comment — this scan rides the shared scanner, not a private regex', 'base + publishMetaItem', true, !!publishIds?.has('base') && !!publishIds?.has('publishMetaItem')); + check('parseRouteSource', 'a `path:` inside a JSDoc @example is an illustration, not a registration', 'tails', JSON.stringify(['/:type/:name/publish']), JSON.stringify([...commentary.keys()])); // The counterfactual, twice: the PRE-FIX scan, verbatim, over the same fixture. Both // halves must still be there to be excluded, or this block proves nothing. @@ -4507,13 +4653,13 @@ function selfTest() { const ls = src.split('\n'); const start = ls.findIndex((l) => l.includes(siteFragment)); const ids = new Set(); - for (let j = start; j >= 0 && j < Math.min(ls.length, start + REGISTRAR_HANDLER_WINDOW); j++) { + for (let j = start; j >= 0 && j < Math.min(ls.length, start + ROUTE_SOURCE_HANDLER_WINDOW); j++) { for (const m of ls[j].matchAll(/[A-Za-z_$][\w$]*/g)) ids.add(m[0]); } return ids; }; - check('parseRegistrarSource', 'counterfactual: the bare-token scan DID read the prose name out of that window', 'promoteDraftForPublish', true, rawWindowIds(commentaryRegistrar, ':type/:name/publish').has('promoteDraftForPublish')); - check('parseRegistrarSource', 'counterfactual: that @example line DID match the registration-site regex', '/api/users/:id', '/api/users/:id', routeTailOf(commentaryRegistrar.split('\n').find((l) => l.includes('/api/users/:id')).match(/(?:^|[\s{,(])path\s*:\s*([`'"])(.*?)\1/)?.[2] ?? '')); + check('parseRouteSource', 'counterfactual: the bare-token scan DID read the prose name out of that window', 'promoteDraftForPublish', true, rawWindowIds(commentaryRouteSource, ':type/:name/publish').has('promoteDraftForPublish')); + check('parseRouteSource', 'counterfactual: that @example line DID match the registration-site regex', '/api/users/:id', '/api/users/:id', routeTailOf(commentaryRouteSource.split('\n').find((l) => l.includes('/api/users/:id')).match(/(?:^|[\s{,(])path\s*:\s*([`'"])(.*?)\1/)?.[2] ?? '')); // End to end through the same selection step the bridge runs: a diff confined to the // commented-about symbol selects no route, and one on the called symbol still selects its @@ -4573,7 +4719,7 @@ function selfTest() { // which is still the shape the scan cannot see wherever it survives. Kept verbatim // for that reason — ⛔ do not "refresh" a hermetic fixture to match today's tree. battery('a registration BOUNDS the previous window even when its path is a variable (#9503)'); - const variablePathRegistrar = [ + const variablePathRouteSource = [ 'export class RestServer {', ' private registerStateRoutes() {', " for (const objectsSegment of ['objects', 'object']) {", @@ -4602,14 +4748,14 @@ function selfTest() { ' }', '}', ].join('\n'); - const variablePath = parseRegistrarSource(variablePathRegistrar); + const variablePath = parseRouteSource(variablePathRouteSource); const stateIds = variablePath.get('/:name/state/:field'); - check('parseRegistrarSource', 'the site\'s OWN implementation symbol still lands in its window', 'legalNextStates', true, !!stateIds?.has('legalNextStates')); - check('parseRegistrarSource', 'a `path:` written in a COMMENT bounds nothing — the boundary rides the same mask the tail does', 'legalNextStates after a commented `path:`', true, !!stateIds?.has('legalNextStates')); - check('parseRegistrarSource', 'the NEXT route\'s handler symbol is not this route\'s implementation', 'getMetaItemLayered', false, !!stateIds?.has('getMetaItemLayered')); - check('parseRegistrarSource', 'a variable `path:` bounds a window without claiming a tail — the recall half is untouched, not silently faked', 'tails', JSON.stringify(['/:name/state/:field']), JSON.stringify([...variablePath.keys()])); + check('parseRouteSource', 'the site\'s OWN implementation symbol still lands in its window', 'legalNextStates', true, !!stateIds?.has('legalNextStates')); + check('parseRouteSource', 'a `path:` written in a COMMENT bounds nothing — the boundary rides the same mask the tail does', 'legalNextStates after a commented `path:`', true, !!stateIds?.has('legalNextStates')); + check('parseRouteSource', 'the NEXT route\'s handler symbol is not this route\'s implementation', 'getMetaItemLayered', false, !!stateIds?.has('getMetaItemLayered')); + check('parseRouteSource', 'a variable `path:` bounds a window without claiming a tail — the recall half is untouched, not silently faked', 'tails', JSON.stringify(['/:name/state/:field']), JSON.stringify([...variablePath.keys()])); - // The counterfactual: `parseRegistrarSource` verbatim as it stood before this hop — + // The counterfactual: `parseRouteSource` verbatim as it stood before this hop — // literal `path:` lines are the only sites. Both halves have to be real for the block // above to prove anything: the defect must reach the foreign symbol, and it must do so // through the boundary and not through the 150-line cap. @@ -4625,16 +4771,16 @@ function selfTest() { const { line, tail } = sites[k]; if (!tail) continue; const next = k + 1 < sites.length ? sites[k + 1].line : ls.length; - const end = Math.min(next, line + REGISTRAR_HANDLER_WINDOW, ls.length); + const end = Math.min(next, line + ROUTE_SOURCE_HANDLER_WINDOW, ls.length); let ids = byTail.get(tail); if (!ids) byTail.set(tail, (ids = new Set())); for (let j = line; j < end; j++) for (const id of ls[j].matchAll(/[A-Za-z_$][\w$]*/g)) ids.add(id[0]); } return byTail; }; - const preFix = literalOnlySites(variablePathRegistrar); - check('parseRegistrarSource', 'counterfactual: the literal-only site scan DID swallow the next route\'s handler whole', 'getMetaItemLayered', true, !!preFix.get('/:name/state/:field')?.has('getMetaItemLayered')); - check('parseRegistrarSource', 'counterfactual: and the fixture is short enough that the 150-line cap is not what stops it', 'lines under the window', true, variablePathRegistrar.split('\n').length < REGISTRAR_HANDLER_WINDOW); + const preFix = literalOnlySites(variablePathRouteSource); + check('parseRouteSource', 'counterfactual: the literal-only site scan DID swallow the next route\'s handler whole', 'getMetaItemLayered', true, !!preFix.get('/:name/state/:field')?.has('getMetaItemLayered')); + check('parseRouteSource', 'counterfactual: and the fixture is short enough that the 150-line cap is not what stops it', 'lines under the window', true, variablePathRouteSource.split('\n').length < ROUTE_SOURCE_HANDLER_WINDOW); // End to end through the same selection step the bridge runs. check('bridge', 'a diff touching only the FOREIGN handler\'s symbol selects no route', 'tails', JSON.stringify([]), JSON.stringify(tailsSelectedBy(new Set(['getMetaItemLayered']), variablePath))); @@ -5113,7 +5259,7 @@ function selfTest() { // render branch with no ceiling prints `unmeasured` in a nicer shape. battery('`causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867)'); check('emit', 'the ADVISORY path measures causes — it passes a ceiling, not just tails', 'affected-docs.mjs', - true, /bridgeCoverageFrom\(ledgers, registrarByTail\.keys\(\), ceilingTailsFrom\(sourceFiles\)\.keys\(\)\)/.test(ownSource)); + true, /bridgeCoverageFrom\(ledgers, routeSourceByTail\.keys\(\), ceilingTailsFrom\(sourceFiles\)\.keys\(\)\)/.test(ownSource)); // ⚠️ READ THE CODE, NOT THE COMMENT THAT FORBIDS IT. These three pins are about what // the renderer DOES, and the block it lives in names `bridge.ledgers` in prose precisely // to forbid deriving from it — so a raw-text negative pin fails on its own rationale @@ -5148,7 +5294,7 @@ function selfTest() { // that count is by deriving selection a second time. Pinned at the source, because the // two agreeing today is what a re-statement costs nothing to break tomorrow. check('emit', 'the --json row list selects through `selectsFrom`, not a second copy of the rule', 'affected-docs.mjs', - true, /const selects = selectsFrom\(registrarByTail\.keys\(\)\);/.test(ownSource)); + true, /const selects = selectsFrom\(routeSourceByTail\.keys\(\)\);/.test(ownSource)); check('emit', 'and nothing else restates the suffix test inline', 'affected-docs.mjs', 1, (ownSource.match(/\.some\(\(t\) => route\.replace\(/g) || []).length); @@ -5460,7 +5606,7 @@ for (const name of [...symbolAnchors].sort()) { if (!admitAnchor('symbol', name, symbolRe(name))) continue; // TWO KINDS OF SYMBOL ARE DOC ANCHORS BUT NOT BRIDGE SYMBOLS. The bridge's premise is // "this name IS some route's implementation, so the handler that implements that route - // mentions it" — and `parseRegistrarSource` tests that premise by scanning a handler + // mentions it" — and `parseRouteSource` tests that premise by scanning a handler // window for the BARE IDENTIFIER (over comment-masked source since #9432, so a name a // handler only WRITES ABOUT never satisfies it). Any name a handler mentions for some // OTHER reason satisfies the scan without satisfying the premise, and mints a route (and, @@ -5521,7 +5667,7 @@ const crossCuttingSymbols = []; // same rule), and a fabricated `0 of 0` here would read as "nothing to reach". let bridgeCoverage = { measured: false, reason: 'no bridgeable symbol in this change — the sdk route bridge did not run' }; if (bridgeSymbols.length) { - const { ledgers, ledgerRows, registrarByTail, sourceFiles } = scanRouteSurface(); + const { ledgers, ledgerRows, routeSourceByTail, sourceFiles } = scanRouteSurface(); // WITH THE CEILING (#11867). Until now this call omitted the third argument, so every // ledger's cause came back `unmeasured` and the three counts `null` — honest, but it // meant the PR comment, which is the surface a human actually reads, rendered all 177 @@ -5543,7 +5689,7 @@ if (bridgeSymbols.length) { // ⛔ Through `ceilingTailsFrom`, never a second inline census: `--bridge-coverage` and // this path now publish the same three buckets, and two spellings of the population they // are computed over is how the two surfaces start disagreeing about one repo. - bridgeCoverage = bridgeCoverageFrom(ledgers, registrarByTail.keys(), ceilingTailsFrom(sourceFiles).keys()); + bridgeCoverage = bridgeCoverageFrom(ledgers, routeSourceByTail.keys(), ceilingTailsFrom(sourceFiles).keys()); // symbol → route, capped: the bridge answers "which routes mention this name", and for // a CROSS-CUTTING helper that is every route it is wired into. Measured on the REST @@ -5551,7 +5697,7 @@ if (bridgeSymbols.length) { // families whose pages document nothing that change touched. Above the cap a symbol // contributes no route anchor — and says so in `crossCuttingSymbols`. const routesBySymbol = new Map(); - for (const [tail, ids] of registrarByTail) { + for (const [tail, ids] of routeSourceByTail) { for (const s of bridgeSymbols) { if (!ids.has(s)) continue; let tails = routesBySymbol.get(s); From cbadba89ec105e4a130aecc7751fd59f6da81e7f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 08:00:26 +0000 Subject: [PATCH 2/4] test(docs-audit): pin the two route-source kinds and the guard between them Adds the #11857 self-test battery (21 cases): the guard admits a contract declaring its verb beside its path, declines a connector-action input, and the counterfactual proves the parser reads that payload's tail so the guard is what declines it. Live pins for the real registry.ts, the five admitted declarations, and the measured no-op of excluding route ledgers from kind (a). Roster 28 -> 29 batteries, floor raised with the cases. Renames the concept in scripts/docs-audit/README.md to match the script. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/docs-audit/README.md | 96 ++++++++++--- scripts/docs-audit/affected-docs.mjs | 196 +++++++++++++++++++++++++-- 2 files changed, 265 insertions(+), 27 deletions(-) diff --git a/scripts/docs-audit/README.md b/scripts/docs-audit/README.md index 7e8be71c79..e716a69b01 100644 --- a/scripts/docs-audit/README.md +++ b/scripts/docs-audit/README.md @@ -49,12 +49,12 @@ Three anchor kinds, each exact: | anchor | what it is | how it is derived | |:--|:--|:--| | `symbol` | a documentable declaration the diff touched | the top-level declaration, or a member of a top-level **container** (class / interface / type / enum / schema object), enclosing each changed line — on **both** sides of the diff, so a removed export still anchors the pages naming it. A member that is a **data property** is additionally qualified by its declaring container against the authorable surface (see below) | -| `route` | a wire path the change touched | a path literal on a changed line, plus every route whose **registrar handler** references a changed symbol | +| `route` | a wire path the change touched | a path literal on a changed line, plus every route whose **route-source handler** references a changed symbol | | `sdk` | the client method bound to an anchor route | the declared `route` ⟷ `client` rows in the repo's route ledgers | The `route` and `sdk` hops are what carry the derivation across the surface boundary the package graph cannot cross: `auditMetaItem` (changed) → `GET /api/v1/meta/:type/:name/audit` -(`rest-server.ts` registrar) → `meta.getAudit` (`rest-route-ledger.ts`) → the token +(`rest-server.ts`, a registration call site) → `meta.getAudit` (`rest-route-ledger.ts`) → the token `api/client-sdk.mdx` actually contains. **A local variable is not documentable surface.** That one rule is what drops the measured @@ -87,7 +87,7 @@ Each anchor kind names its own origin, from the same field the JSON publishes as | kind | the clause | |:--|:--| | `symbol` | `a field of interface MetaOverlayCacheKey` · `a method of class RestServer` · `a top-level function` | -| `route` | `a path literal in RestServer` · `bridged from symbol enforceEnvironmentOwnership — its registrar handler names it` | +| `route` | `a path literal in RestServer` · `bridged from symbol enforceEnvironmentOwnership — its route-source handler names it` | | `sdk` | `the route ledger binds it to GET /api/v1/ui/view/:object/:type` | | `literal` | `a string literal in cacheKeyOf` | | `command` | `read off packages/cli/src/commands/environments/bind.ts` | @@ -181,7 +181,7 @@ whole anchor set, in every kind, after it is derived.) The first build of this derivation was, on some PRs, *noisier* than the proxy it replaced (134 rows where the old tool gave 26). Two guards fixed that, and both run **before** the route bridge — a name left in the set does not merely add a noisy row, it mints noisy route -and SDK anchors from every registrar handler that mentions it: +and SDK anchors from every route-source handler that mentions it: 1. **Shape** — an anchor must be code-shaped (camelCase / PascalCase / snake_case / dotted). `label`, `object`, `start`, `locale` and `sections` all arrived as real @@ -200,7 +200,7 @@ entirely — a data table is consulted by handlers, it is not their implementati ### The `sdk` bridge reaches part of its own population, and says which part (#9572) -The `sdk` hop needs a registrar `path:` tail to select a route-ledger row. Measured on +The `sdk` hop needs a route source's `path:` tail to select a route-ledger row. Measured on `9ff11921a`: **45 of the 221 client-bound ledger rows are reachable, 176 are not.** An unreachable row is not "unlisted this time" — no symbol change bridges to it, ever. @@ -210,7 +210,7 @@ drift — each move was measured row by row: | `--bridge-coverage` | `a6eca9223` | `8f10a79f7a` | why | | --- | ---: | ---: | --- | -| registrar files | 12 | 12 | two ADR-0049 ledger entries were admitted in between and are excluded again here; they produced **0 tails and 0 reachable rows**, so they never moved the figures below | +| route sources | 12 | 12 | two ADR-0049 ledger entries were admitted in between and are excluded again here; they produced **0 tails and 0 reachable rows**, so they never moved the figures below | | route tails | 43 | 44 | `rest-server.ts` unrolled `for (const publishedPath of […])` into a literal `path:` — a variable path yields no tail, a literal one does | | client-bound rows | 222 | 219 | three `:type/:section/:name` rows deleted from `rest-route-ledger.ts`, all three already unreachable | | **reachable** | **45** | **47** | the one new tail `/:type/:name/published` selects `meta.getPublished` on the rest ledger *and* on the runtime ledger | @@ -218,7 +218,7 @@ drift — each move was measured row by row: ⭐ 45 → 47 is the bridge reaching **more** of its population, not losing track of it, so the figure stands at 47. ⛔ Do not "restore" 45: the only recognizer spelling that reproduces it drops ten of the fourteen matched files, including a tail-producing -registrar — the control appears to recover exactly when the recognizer stops working. +call site — the control appears to recover exactly when the recognizer stops working. That number now travels with the answer. `bridgeCoverage` is emitted on every run whose change carried a bridgeable symbol (`{ measured: false, reason }` when it did not — never a @@ -246,34 +246,96 @@ among them), so the silence is not an empty region. `56 of 56` and `46 of 87` used to print in the same words, and they are not the same finding. Every unreachable row is now attributed against a **ceiling** — every `path:` any -`packages/**` file declares, with `REGISTRAR_FILE_RE` ignored entirely, built by -`maximalTailsFrom` from the same `parseRegistrarSource` over the same walk. Measured on +`packages/**` file declares, with `CALL_SITE_FILE_RE` ignored entirely, built by +`maximalTailsFrom` from the same `parseRouteSource` over the same walk. Measured on `589758d22`, the 177 unreachable rows partition as: | cause | rows | what it means | | --- | --- | --- | | `discovery-gap` | 14 | an in-repo file declares this exact path; the filename convention did not scan that file. The JSON **names the witness**. | -| `no-in-repo-registrar` | 56 | on a ledger where **not one** row is declared in-repo — declared upstream and catch-all-mounted. No discovery change reaches it. | -| `undecided` | 107 | no in-repo declaration for the row, on a ledger that *has* in-repo registrars. Absence and an unreadable spelling are not distinguishable here, so neither is claimed. | +| `no-in-repo-declaration` | 56 | on a ledger where **not one** row is declared in-repo — declared upstream and catch-all-mounted. No discovery change reaches it. | +| `undecided` | 107 | no in-repo declaration for the row, on a ledger that *has* in-repo route sources. Absence and an unreadable spelling are not distinguishable here, so neither is claimed. | -Exactly **one** of the seven ledgers is `no-in-repo-registrar` today: `auth-route-ledger.ts`, +Exactly **one** of the seven ledgers is `no-in-repo-declaration` today: `auth-route-ledger.ts`, whose own header has said so since #3656 — better-auth declares those routes inside `node_modules` and the plugin mounts them with a single ``rawApp.all(`${basePath}/*`)``, which `routeTailOf` cannot and should not turn into a tail. That is why widening -`REGISTRAR_FILE_RE` to admit `auth-plugin.ts` was measured to move `registrar files +`CALL_SITE_FILE_RE` to admit `auth-plugin.ts` was measured to move `route sources scanned` 12 → 13 and **nothing else**. -⛔ **This changes no discovery and moves no reach.** `REGISTRAR_FILE_RE` is byte-identical, -the bridge still rides on `registrarByTail` alone, and `reachable` is 45 before and after — +⛔ **This changes no discovery and moves no reach.** `CALL_SITE_FILE_RE` is byte-identical, +the bridge still rides on `routeSourceByTail` alone, and `reachable` is 45 before and after — pinned in `--self-test`. The ceiling only explains the number; it never participates in it, and because it is a superset by construction a ceiling that misses a *reachable* row is a `brokenScan` verdict rather than a quieter result. The classification is **derived, never listed**. Control on `589758d22`: adding one in-repo file that declares one auth route — under a filename the convention does not match -— moves the auth ledger out of `no-in-repo-registrar` on its own (structural 56 → 0, +— moves the auth ledger out of `no-in-repo-declaration` on its own (structural 56 → 0, `reachable` still 45), and removing it restores 56. +### A route SOURCE is two kinds, and one of them is admitted by evidence (#11857) + +Maintainer ruling A, 2026-09-04 decision batch #31. Until this card the recognizer's name, +docblock and `--self-test` all meant *the file that registers the route* — while the +measured widening that motivated the card admits five `packages/spec` **Zod contract +declarations**, which register nothing. The ruling made the **rename** a condition of +admitting them: nothing here is called a "registrar" any more, because the word would +otherwise denote two constructs. + +A **route source** is a file whose source declares a route. Two kinds: + +| kind | admitted by | today | +| --- | --- | ---: | +| registration **call site** | the `CALL_SITE_FILE_RE` filename convention (unchanged) | 12 | +| spec contract **declaration** | **evidence**: a non-test `packages/**/*.ts` whose masked source declares a route beside the HTTP method it answers | 5 | + +Why a declaration counts: the drift check exists to say *"the contract changed, re-verify +the manual"*, and a Zod API declaration in `packages/spec` **is** the contract. No widening +of the filename convention would ever reach it — `storage.zod.ts` is not going to be +renamed `storage-routes.ts`. + +**The guard is the HTTP method, not a file list.** A route declaration names the verb it +serves; a data payload carrying a `path:` key does not. Measured on `460134af8`: of the 61 +literal `path:` sites in `packages/spec/src/conversions/registry.ts` — whose +`/api/v1/health` is a connector-action **input** inside an automation fixture — **zero** +carry an HTTP method, while the five contract declarations carry one at 61 of their 62 +sites. (The 62nd has its `method:` five *lines* but zero *properties* up, behind a JSDoc +that `maskComments` blanks; the lookaround skips blank lines for exactly that reason.) The +separation is total at every lookaround from 1 to 6, so the constant is a margin, not a +threshold. + +⛔ **The fixture and the benchmark are not re-excluded here.** The card also named +`test/fixtures/*.ts` and `*.bench.ts`; `isTestFile` grew those arms in #12965, so the walk +never offers them to either kind. `--self-test` **pins** that over the two real repo paths +rather than restating the exclusion — the day either arm is loosened, they come back as +contract declarations and the self-test says so. + +Measured on `460134af8`, before → after: + +| `--bridge-coverage` | before | after | +| --- | ---: | ---: | +| route sources scanned | 12 | **17** (12 call sites + 5 contract declarations) | +| route tails produced | 44 | **78** | +| client-bound rows reachable | 47 | **61** | +| `discovery-gap` rows | 14 | **0** | +| `storage` ledger unreachable | 7 of 7 | **0 of 7** | +| `i18n` ledger unreachable | 3 of 3 | **1 of 3** | +| `rest` ledger unreachable | 42 of 84 | **40 of 84** | +| `runtime` ledger unreachable | 64 of 69 | **61 of 69** | +| `plugin-auth` unreachable | 56 of 56 | 56 of 56 (unchanged — declared upstream, see above) | + +No reach regression is possible **by construction**: `selectsFrom` is a `some()` over the +tail list, so added tails can only add selections. `discovery-gap` reaching **0** is the +strong reading — every row the ceiling said a widening *could* reach is now actually +reached, and what remains unreachable is unreachable for a reason no discovery change +touches. + +⚠️ **The published `45 → 59` figure from the card is not what landed.** The card measured +on `a6eca9223`; both endpoints have since moved with the tree (see the #9572 table above), +and the guard admits five files rather than the card's eight. The figure this PR +establishes is **47 → 61 on `460134af8`**, re-measured rather than copied. + ### A PARTIAL ledger read is a verdict too (#9896) The row recognizer reads **single-quoted** values only, and the `rowsParsed === 0` guard @@ -429,7 +491,7 @@ How often it renders, re-derived over the 40 first-parent commits ending at `e43 is a rare notice rather than a per-PR banner, which is what keeps it readable. **Cost** (the card's open question): the anchor derivation reads the same 178-page corpus -the old one did, plus the 18 route-registrar/ledger sources (~875 KB) and one `git show` +the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show` per changed file per side. Measured end-to-end on the ten PRs above, `node affected-docs.mjs` went from 85-195 ms to 114-582 ms. The heaviest case is the widest diff; every case stays well under a second, against a job that already spends seconds checking out the repo and diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index adf56b1f5d..b74ef24595 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -248,11 +248,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the sdk bridge\'s REACH over the declared surface (#9572)': 11, '#11178: WHY a row is unreachable, and the two causes that printed as one': 28, '`causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867)': 16, + 'the ROUTE SOURCE concept: two kinds, and the runtime-registration guard (#11857)': 21, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 28; +const SELF_TEST_BATTERY_FLOOR = 29; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -982,7 +983,7 @@ if (args.includes('--bridge-coverage')) { // moves it by zero rows. That misreading is the reason this split exists — it aimed a // whole card at widening a recognizer that was never the constraint. const CAUSE_NOTE = { - 'no-in-repo-registrar': () => 'NO in-repo registrar for ANY row — discovery cannot reach this surface', + 'no-in-repo-declaration': () => 'NO in-repo declaration for ANY row — discovery cannot reach this surface', 'discovery-gap': (l) => `all ${l.remediable} remediable by discovery`, 'undecided': (l) => `${l.remediable} remediable by discovery, ${l.unwitnessed} undecided`, 'no-client-surface': () => 'no client-bound rows to reach', @@ -998,11 +999,11 @@ if (args.includes('--bridge-coverage')) { // fraction is: remediable + structural + undecided === UNREACHABLE, so a reader can see // it stay whole, and a bucket that starts absorbing another cannot do it quietly. if (coverage.causes.measured) { - console.log(` why — against every \`path:\` any packages/** file declares (${coverage.causes.ceilingTails}-tail ceiling vs the ${coverage.tails} the filename convention yields):`); + console.log(` why — against every \`path:\` any packages/** file declares (${coverage.causes.ceilingTails}-tail ceiling vs the ${coverage.tails} the two route-source kinds yield):`); const n = (v) => String(v).padStart(4); - console.log(` remediable by discovery ..${n(coverage.causes.remediable)} an in-repo file declares the row's path; the convention did not scan that file`); - console.log(` NO in-repo registrar .....${n(coverage.causes.structural)} on a ledger where not ONE row is declared in-repo — declared upstream and catch-all-mounted, so no discovery change reaches it`); - console.log(` undecided ................${n(coverage.causes.undecided)} no in-repo declaration, on a ledger that HAS in-repo registrars — absence and an unreadable spelling are not distinguishable here`); + console.log(` remediable by discovery ..${n(coverage.causes.remediable)} an in-repo file declares the row's path; NEITHER route-source kind admitted that file`); + console.log(` NO in-repo declaration ...${n(coverage.causes.structural)} on a ledger where not ONE row is declared in-repo — declared upstream and catch-all-mounted, so no discovery change reaches it`); + console.log(` undecided ................${n(coverage.causes.undecided)} no in-repo declaration, on a ledger that HAS in-repo route sources — absence and an unreadable spelling are not distinguishable here`); } else { console.log(` why ........................ ${coverage.causes.reason}`); } @@ -1933,14 +1934,14 @@ function bridgeCoverageFrom(ledgers, tails, maximalTails) { // and the plugin mounts them with a single catch-all — and it is derived here rather // than asserted, so a ledger that grows an in-repo registrar leaves this bucket by // itself. Measured on `589758d22`: exactly one ledger of the seven. - else if (hit.length === 0 && witnessed.length === 0) cause = 'no-in-repo-registrar'; + else if (hit.length === 0 && witnessed.length === 0) cause = 'no-in-repo-declaration'; else if (unwitnessed.length === 0) cause = 'discovery-gap'; // ⛔ NOT defaulted into either bucket. This ledger HAS in-repo registrars, so its // unwitnessed rows are not the auth shape — but nothing here can tell "no registration // site" apart from "a registration site whose path this recognizer cannot read", and // rendering that ignorance as either verdict is exactly the #9747 false green. else cause = 'undecided'; - if (cause === 'no-in-repo-registrar') structuralRows += unwitnessed.length; + if (cause === 'no-in-repo-declaration') structuralRows += unwitnessed.length; else undecidedRows += unwitnessed.length; remediableRows += witnessed.length; byLedger.push({ file, clientRows: bound.length, reachable: hit.length, unreachable: bound.length - hit.length, rowsParsed: rows.length, routesDeclared, clientsDeclared, declined, outsideCode, cause, remediable: censusMeasured ? witnessed.length : null, unwitnessed: censusMeasured ? unwitnessed.length : null }); @@ -1957,7 +1958,7 @@ function bridgeCoverageFrom(ledgers, tails, maximalTails) { // answer, and pinning it would be a false red on an accurate ledger. const brokenScan = []; if (!ledgers.length) brokenScan.push('no route-ledger file was found at all — the ledger walk selected nothing, so every `sdk` anchor is silently unavailable'); - if (!tailList.length) brokenScan.push('the registrar scan produced no route tail at all — the symbol → route → sdk bridge cannot fire for any change'); + if (!tailList.length) brokenScan.push('the route-source scan produced no route tail at all — the symbol → route → sdk bridge cannot fire for any change'); if (censusBroken) brokenScan.push(censusBroken); for (const l of byLedger) { if (l.rowsParsed === 0) brokenScan.push(`${l.file} matched the ledger convention but parsed 0 rows — the row recognizer no longer reads this file's shape`); @@ -5170,7 +5171,7 @@ function selfTest() { const causeOf = (f) => causeCov.ledgers.find((l) => l.file === f).cause; const causeCases = [ // THE ONE THIS CARD IS ABOUT: not one row of the surface is declared anywhere in-repo. - ['a ledger no in-repo file declares ANY row of is structural', 'no-in-repo-registrar', causeOf('structural-route-ledger.ts')], + ['a ledger no in-repo file declares ANY row of is structural', 'no-in-repo-declaration', causeOf('structural-route-ledger.ts')], ['a ledger whose every unreachable row has an in-repo witness is a discovery gap', 'discovery-gap', causeOf('gap-route-ledger.ts')], // ⛔ NOT defaulted into either bucket — the #9747 rule this whole split is an // application of: a recognizer narrower than the repo reports "unrecognised". @@ -5393,6 +5394,181 @@ function selfTest() { } + // ── The ROUTE SOURCE concept: two kinds, and the guard between them (#11857) ── + // + // Ruling A, 2026-09-04 batch #31. Everything here defends ONE sentence: a route source + // is a file that DECLARES a route, of two kinds — a registration CALL SITE admitted by + // its name, and a spec contract DECLARATION admitted by EVIDENCE — and the evidence is + // the HTTP method the declaration answers on. + // + // ⛔ THE GUARD IS THE POINT, NOT THE ADMISSION. Admitting contract declarations without + // it also admits every data payload carrying a `path:` key, and the card that measured + // this route named the instance: a connector-action INPUT inside an automation fixture. + // So the cases below pin the guard from BOTH sides — that it keeps the payload out, and + // that it does not cost the real declarations a single tail. + battery('the ROUTE SOURCE concept: two kinds, and the runtime-registration guard (#11857)'); + + // The shape kind (b) exists to admit: a contract declaring its verb beside its path. + const contractDecl = [ + 'export const StorageApiContracts = {', + ' getPresignedUrl: {', + " method: 'POST' as const,", + " path: '/api/v1/storage/upload/presigned',", + ' input: GetPresignedUrlRequestSchema,', + ' },', + '};', + ].join('\n'); + check('parseRouteSource', 'a contract declaring its VERB beside its path is a route source', + 'tails under the guard', JSON.stringify(['/api/v1/storage/upload/presigned']), + JSON.stringify([...parseRouteSource(contractDecl, { requireMethodSignal: true }).keys()])); + + // The shape the guard exists to keep out — `packages/spec/src/conversions/registry.ts`'s + // own, reduced to the two lines that matter. A path inside a connector-action `input:` + // is an ARGUMENT to an action, not a route anything serves. + const connectorInput = [ + ' {', + " id: 'n5',", + " type: 'connector_action',", + " connectorConfig: { connectorId: 'rest', actionId: 'get' },", + " config: { input: { path: '/api/v1/health' } },", + ' },', + ].join('\n'); + check('parseRouteSource', 'a connector-action INPUT is not a route source — the guard declines it', + 'tails under the guard', 0, parseRouteSource(connectorInput, { requireMethodSignal: true }).size); + // ⭐ THE COUNTERFACTUAL, and the reason the case above proves anything: the parser DOES + // read a tail out of that payload. What declines it is the guard, not a parse failure — + // without this pin the case above would keep passing if `path:` scanning broke entirely. + check('parseRouteSource', 'counterfactual: unguarded, that same payload DOES yield a tail', + 'tails without the guard', JSON.stringify(['/api/v1/health']), + JSON.stringify([...parseRouteSource(connectorInput).keys()])); + + // `actionId: 'get'` is one masked line from the path and reads like a verb to a human. + // It is not an HTTP `method:`, and the signal must not accept it — otherwise the guard + // admits the very file the ruling named. + check('hasRouteMethodSignal', 'an actionId is not an HTTP method — the signal keys on the KEY too', + "actionId: 'get'", false, hasRouteMethodSignal([" actionId: 'get',", " path: '/api/v1/health',"], 1)); + check('hasRouteMethodSignal', 'and a non-HTTP `method:` value is not the signal either', + "method: 'cron'", false, hasRouteMethodSignal([" method: 'cron',", " path: '/api/v1/health',"], 1)); + check('hasRouteMethodSignal', 'a one-line property list carries its own signal', + "{ method: 'GET', path: '/api/x' }", true, hasRouteMethodSignal([" { method: 'GET', path: '/api/x' },"], 0)); + check('hasRouteMethodSignal', 'and the verb is read case-insensitively — a lowercase `get` still registers', + "method: 'get'", true, hasRouteMethodSignal([" method: 'get',", " path: '/api/x',"], 1)); + + // THE MASKED-COMMENT GAP. `plugin-rest-api.zod.ts` has one site whose `method:` sits five + // LINES up but zero PROPERTIES up — a JSDoc block sits between them, which `maskComments` + // blanks to whitespace without moving a line. Blank-skipping is what makes the lookaround + // a property-list test rather than a line-distance one; a naive window misses this site. + const jsdocGap = [" method: 'POST',", ' ', ' ', ' ', ' ', " path: '/trigger/:name',"]; + check('hasRouteMethodSignal', 'a JSDoc between the two properties is blank after masking — the site still signals', + 'method five blank lines up', true, hasRouteMethodSignal(jsdocGap, 5)); + // …and the lookaround is still BOUNDED: five non-blank properties away is a different + // object, and reads as one. The blank-skipping widens what counts as adjacent, never + // how many PROPERTIES away the verb may sit. + const farMethod = [" method: 'POST',", ' a: 1,', ' b: 2,', ' c: 3,', ' d: 4,', " path: '/trigger/:name',"]; + check('hasRouteMethodSignal', 'but five NON-BLANK properties away is another object — not a signal', + 'method past the lookaround', false, hasRouteMethodSignal(farMethod, 5)); + + // A DROPPED SITE STILL BOUNDS THE PREVIOUS WINDOW (#9503, one card on). The guard changes + // what a file CONTRIBUTES, never where another site's handler window ends — so a payload + // sitting between two real declarations must not let the first one swallow the second's + // handler symbols. + const guardedBoundary = [ + " { method: 'GET', path: '/api/v1/a',", + ' handler: firstImpl },', + ' filler1: 1,', + ' filler2: 2,', + ' filler3: 3,', + ' filler4: 4,', + " { config: { input: { path: '/api/v1/payload' } } },", + ' filler5: 5,', + ' filler6: 6,', + ' filler7: 7,', + ' filler8: 8,', + " { method: 'GET', path: '/api/v1/b',", + ' handler: secondImpl },', + ].join('\n'); + const guarded = parseRouteSource(guardedBoundary, { requireMethodSignal: true }); + check('parseRouteSource', 'the payload between them contributes no tail of its own', + 'tails', JSON.stringify(['/api/v1/a', '/api/v1/b']), JSON.stringify([...guarded.keys()])); + check('parseRouteSource', 'and the first route does NOT absorb the second route\'s handler symbol', + 'secondImpl', false, !!guarded.get('/api/v1/a')?.has('secondImpl')); + check('parseRouteSource', 'while its own handler symbol still lands in its window', + 'firstImpl', true, !!guarded.get('/api/v1/a')?.has('firstImpl')); + + // KIND (a) IS UNTOUCHED, which is the no-reach-regression half. A call site admitted by + // NAME contributes every tail it declares, guard or no guard — this is the default. + check('parseRouteSource', 'kind (a) is unguarded by DEFAULT — the same source yields the payload tail too', + 'tails without the guard', JSON.stringify(['/api/v1/a', '/api/v1/payload', '/api/v1/b']), + JSON.stringify([...parseRouteSource(guardedBoundary).keys()])); + + // ⚠️ THE GUARD'S KNOWN LIMIT, PINNED RATHER THAN LEFT TO BE DISCOVERED. The signal is a + // PROPERTY-LIST test, so a payload written INSIDE a real declaration's property list + // inherits that declaration's verb and is admitted. That is a bounded over-admission and + // an acceptable one: it can only happen inside a file that is a route source already, so + // it widens which TAILS one source contributes — never which FILES are sources. It is + // also why the connector-action pin above is stated on the REAL file: `registry.ts` has + // 61 path sites and not one of them sits beside an HTTP method. + check('hasRouteMethodSignal', 'a payload nested INSIDE a declaration\'s property list does inherit its verb', + 'adjacent payload', true, hasRouteMethodSignal([ + " { method: 'GET', path: '/api/v1/a',", + " config: { input: { path: '/api/v1/payload' } } },", + ], 1)); + check('CALL_SITE_FILE_RE', 'and the call-site convention itself is unchanged by the rename', + 'packages/rest/src/rest-server.ts', true, CALL_SITE_FILE_RE.test('packages/rest/src/rest-server.ts')); + + // ── The isTestFile pin, stated over the ROUTE SOURCE population (#12965 / #11857) ── + // + // The card measured THREE non-registrars the evidence route admits. Two of them — a + // fixture under `test/fixtures/` and a `.bench.ts` benchmark — stopped being admitted + // when `isTestFile` grew those arms, so this card re-excludes NEITHER by name. The + // ruling's instruction was to PIN that rather than re-state it: these are the two real + // repo paths, asserted over the predicate the walk actually calls, so the day either arm + // is loosened the fixture and the benchmark come back as contract declarations and THIS + // says so — instead of two phantom route sources appearing in the census. + for (const [rel, why] of [ + ['packages/qa/dogfood/test/fixtures/endpoint-policy-fixture.ts', 'the #11857 fixture — kept out by isTestFile, NOT by name'], + ['packages/spec/src/benchmark.bench.ts', 'the #11857 benchmark — kept out by isTestFile, NOT by name'], + ]) { + check('isTestFile', `${why} — so the walk never offers it to EITHER kind`, rel, true, isTestFile(rel)); + } + + // ── LIVE pins: the three claims this card makes about the real tree ── + // + // Fixtures cannot carry these: each is a claim about what the REPO contains, and each is + // a number the PR body quotes. Read off the same scan the report prints from. + { + const live = scanRouteSurface(); + const liveKind = (k) => live.routeSources.filter((r) => r.kind === k).map((r) => r.file); + + // (1) The guard's target, on the real file rather than a reduced fixture. + check('scanRouteSurface', 'the connector-action input is NOT admitted as a route source', + 'packages/spec/src/conversions/registry.ts', false, + live.routeSources.some((r) => r.file === 'packages/spec/src/conversions/registry.ts')); + const registryText = (() => { + try { return readFileSync(join(repoRoot, 'packages/spec/src/conversions/registry.ts'), 'utf8'); } catch { return null; } + })(); + // The same counterfactual as the fixture pair above, on the real file: it is EVIDENCE + // that would admit it, and the guard is the only thing that does not. + check('scanRouteSurface', 'counterfactual: unguarded, that real file WOULD be admitted — the guard is what excludes it', + 'registry.ts tails, unguarded', true, registryText === null || parseRouteSource(registryText).size > 0); + + // (2) The five spec contract declarations the ruling admits, each carrying the signal. + check('scanRouteSurface', 'every contract declaration admitted is a packages/spec API declaration', + 'kind=contract', true, liveKind('contract').every((f) => f.startsWith('packages/spec/src/api/'))); + + // (3) THE LEDGER EXCLUSION IS A MEASURED NO-OP, not an assumption. Most ledger names + // match the call-site convention (`route-ledger.ts` carries `route`), so before this + // card every ledger was also parsed as a registrar. Narrowing kind (a) to exclude them + // moves a tail only if some ledger declares a `path:` route — none does today, and + // this is the assertion that reds the day one starts. + let ledgerTails = 0; + for (const rel of live.conventionFiles.filter((f) => LEDGER_FILE_RE.test(f))) { + try { ledgerTails += parseRouteSource(readFileSync(join(repoRoot, rel), 'utf8')).size; } catch { /* unreadable contributes none */ } + } + check('scanRouteSurface', 'no route LEDGER declares a route tail — so excluding ledgers from kind (a) moved nothing', + 'tails declared by the live route ledgers', 0, ledgerTails); + } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── // // Evaluated after every battery has had its chance and BEFORE the verdict, so From fa12687d1da829fad6d3b1d089af2ee33c4efb2a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 08:02:17 +0000 Subject: [PATCH 3/4] test(docs-audit): floor the contract-kind population so a shape pin cannot pass vacuously Ablation B (evidence route disabled) left the battery green: `every()` over an empty list is true, so the pin that says WHAT kind (b) admits said nothing about WHETHER it admits. Adds the size floor and names the declaration the storage ledger's closure rests on. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/docs-audit/affected-docs.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index b74ef24595..6f89c16365 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -248,7 +248,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the sdk bridge\'s REACH over the declared surface (#9572)': 11, '#11178: WHY a row is unreachable, and the two causes that printed as one': 28, '`causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867)': 16, - 'the ROUTE SOURCE concept: two kinds, and the runtime-registration guard (#11857)': 21, + 'the ROUTE SOURCE concept: two kinds, and the runtime-registration guard (#11857)': 23, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -5552,7 +5552,19 @@ function selfTest() { check('scanRouteSurface', 'counterfactual: unguarded, that real file WOULD be admitted — the guard is what excludes it', 'registry.ts tails, unguarded', true, registryText === null || parseRouteSource(registryText).size > 0); - // (2) The five spec contract declarations the ruling admits, each carrying the signal. + // (2) THE CONTRACT KIND ADMITS SOMETHING — the anti-vacuity floor, and the case that + // names kind (b) when it stops running. ⚠️ Without it the `every()` below passes on an + // EMPTY list: disabling the evidence route entirely left this whole battery green, + // measured, which is how this pin came to exist. A shape assertion over a population + // is only as strong as a floor under that population's size. + check('scanRouteSurface', 'the CONTRACT kind admits at least one route source — kind (b) is still running', + 'kind=contract count', true, liveKind('contract').length >= 1); + // …and the one the headline rests on by NAME: `storage.zod.ts` is what closes the + // storage ledger from 7-of-7 unreachable to 0-of-7. If it is legitimately renamed, + // move this pin with it — do not delete it, or the closure goes back to being asserted. + check('scanRouteSurface', 'and the declaration that closes the storage ledger is among them', + 'packages/spec/src/api/storage.zod.ts', true, + liveKind('contract').includes('packages/spec/src/api/storage.zod.ts')); check('scanRouteSurface', 'every contract declaration admitted is a packages/spec API declaration', 'kind=contract', true, liveKind('contract').every((f) => f.startsWith('packages/spec/src/api/'))); From f0dd5ab79afff63d1026e1d4d2e5dc8aafd442af Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 08:14:49 +0000 Subject: [PATCH 4/4] docs(docs-audit): finish the rename in the anchor provenance string The `route` anchor's own `from` note still read "its registrar handler names it" -- the one place the old word reached a user-visible string rather than a comment. Found by running the tool in normal mode on a real diff. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/docs-audit/affected-docs.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index 6f89c16365..a41faf9c27 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -5761,7 +5761,7 @@ for (const f of implementationChanges) { // Both guards run BEFORE the bridge, not after it, and that ordering is the fix rather // than a detail: the bridge answers "which routes mention this name", so a name left in // the set does not merely add a noisy row — it mints noisy ROUTE and SDK anchors from -// every registrar handler that happens to mention it. Measured both ways: `label` / +// every route-source handler that happens to mention it. Measured both ways: `label` / // `start` / `subject` (locals in the auth-email change 445ae4deb) pulled `/:object/import` // and `/forms/:slug` into an advisory about email templates, and `ObjectQL` did the same // to the objectql cascade fix 650cd3daa. @@ -5897,7 +5897,7 @@ if (bridgeSymbols.length) { if (tails.size > MAX_ROUTES_PER_SYMBOL) { crossCuttingSymbols.push(`${s} (${tails.size} routes)`); continue; } for (const t of tails) { routeAnchors.add(t); - noteAnchorFrom('route', t, `bridged from symbol ${s} — its registrar handler names it`); + noteAnchorFrom('route', t, `bridged from symbol ${s} — its route source's handler names it`); } } // route → client method (the ledger's declared binding), and the reverse direction for