From 451d171da240eb45bfeb724f015b958d0fa2ae24 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 16:22:30 +0000 Subject: [PATCH 1/2] fix(devx): read the ledger row recognizer through typeDeclRegions too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recognizer read through codeOnly (#10683) but not through typeDeclRegions, which is the second of #10500's two exact discriminators for 'is this declaration in code position'. Type declarations are code, so a literal-union `route: 'GET /a' | 'GET /b'` TYPE member still minted a row — silently, for the same two reasons as the comment case: rows and routesDeclared moved together so the partial-read guard saw no gap, and outsideCode could not see it because the lead genuinely is in code position. The row loop and the first term of the denominator now skip the same region list declarationsIn has read since #10500, so the two scans answer the question the same way instead of drifting while both look right. Measured across all seven live ledgers, before and after: identical — 259 of 259 rows read, 221 of 221 client-bound, 176 unreachable, and the full --bridge-coverage --json byte-identical. 0 quoted route:/client: leads sit inside a type declaration today. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- scripts/docs-audit/affected-docs.mjs | 155 +++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 7 deletions(-) diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index e11e41d7cd..0641bdc5a3 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -1477,6 +1477,15 @@ function declinedIn(s) { * all seven ledgers. `outsideCode` is what keeps the fix from trading a phantom row for a * new silence — it NAMES every lead the mask dropped, and carries no verdict. * + * …AND THROUGH BOTH OF #10500'S DISCRIMINATORS, NOT ONE (#10793). `codeOnly` was only half + * of the answer: type declarations are CODE, so a literal-union `route: 'GET /a' | 'GET /b'` + * TYPE member still minted a row — the identical silent shape arriving through the other + * discriminator, with `rows` and `routesDeclared` moving together again so no verdict fired. + * The recognizer and the first term of its denominator now skip `typeDeclRegions` the way + * `declarationsIn` has since #10500, so the two scans answer "is this in code position?" the + * same way instead of drifting while both look right. Delta 0 on all seven ledgers again: + * none of them declares a quoted `route:` inside a type declaration. + * * @returns {{rows: Array<{route: string, client: string|null}>, declined: Array<{key: string, line: number, text: string}>, routesDeclared: number, clientsDeclared: number, outsideCode: Array<{key: string, line: number, text: string}>}} */ function parseLedgerSource(text) { @@ -1509,13 +1518,39 @@ function parseLedgerSource(text) { // one tree where the move is provably free: no ledger quotes a `route:` in prose today, // and `rows`, `routesDeclared`, `clientsDeclared` and the 176 UNREACHABLE rows are // byte-identical across the change (all seven ledgers, delta 0). - // ⛔ ONE of #10500's two discriminators, not both. `typeDeclRegions` is still NOT applied - // here, so a literal-union `route: 'GET /a' | 'GET /b'` TYPE member is still read as a row - // — the same silent shape, through the other discriminator. Measured on this tree: 0 such - // members across all seven ledgers (every one declares `route: string;`). Applying it is a - // SECOND population move and wants its own before/after, which is why it is #10793 and not - // a rider here. + // BOTH OF #10500'S DISCRIMINATORS NOW, NOT ONE (#10793). `codeOnly` blanks comments and + // string CONTENTS; it does not blank TYPE DECLARATIONS, and has no reason to — they are + // code. So a literal-union `route: 'GET /a' | 'GET /b'` TYPE member was still read as a + // ROW: the identical silent shape the paragraph above describes, arriving through the + // other discriminator. It stayed silent for the same two reasons — `rows` and + // `routesDeclared` moved together, so the partial-read guard saw no gap, and `outsideCode` + // could not see it either, because the lead genuinely IS in code position. Measured on the + // tree this landed on, one file declaring ONE row: + // + // export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string } + // export const L = [ + // { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }, + // ]; + // ⇒ rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0 + // + // ⛔ A SKIPPED TYPE MEMBER IS SILENT ON PURPOSE, and that is NOT the silence `outsideCode` + // exists to break. A prose-quoted lead is a would-be row sitting where the mask says code + // is not, so naming it tells the reader something they can act on. A type member is a + // correct declaration of a TYPE: the `route: string;` member all seven entry interfaces + // declare produces nothing today under the same rule, `declarationsIn` has excluded the + // literal-union spelling since #10500, and `--self-test` pins BOTH directions — deleting + // the interface changes no count at all. This makes the recognizer agree with the position + // the rest of the file already held rather than inventing a second one. + // + // The population move was priced on the tree where it is provably free: 0 quoted `route:` + // or `client:` leads inside a type declaration across all seven ledgers, and + // `259 of 259` / `221 of 221` / 176 UNREACHABLE are byte-identical across the change. const code = codeOnly(text); + // The SECOND discriminator, taken over the already-blanked source — the same call + // `declarationsIn` makes, reusing the same region list rather than growing a second idea + // of what a type member is. + const typeDecls = typeDeclRegions(code); + const inTypeDecl = (i) => typeDecls.some(([a, b]) => i >= a && i <= b); // The value as WRITTEN. `codeOnly` blanks string CONTENTS, so a capture group taken off // `code` is a run of spaces of the right length and never the route. Blanking preserves // LENGTH, so cutting the raw text by the masked match's offsets is exact for any content @@ -1525,6 +1560,9 @@ function parseLedgerSource(text) { const routeRe = /route\s*:\s*'([^']+)'/g; let m; while ((m = routeRe.exec(code)) !== null) { + // …and never a TYPE member (#10793). A literal-union member opens with the very quote + // this regex reads, which is exactly why `typeDeclRegions` and not a spelling test. + if (inTypeDecl(m.index)) continue; const rest = code.slice(m.index, routeRe.lastIndex + 1200); const nextRoute = rest.slice(1).search(/route\s*:\s*'/); const window = nextRoute === -1 ? rest : rest.slice(0, nextRoute + 1); @@ -1590,7 +1628,13 @@ function parseLedgerSource(text) { // denominator that counted prose-quoted leads while the numerator no longer read them // would turn this migration into a permanently red ratio on the very files it fixed. // Both terms move together, which is the invariant the block above insists on. - const routesDeclared = [...code.matchAll(/route\s*:\s*'/g)].length + declined.filter((d) => d.key === 'route').length; + // + // …and it skips TYPE MEMBERS on the same list the loop above does (#10793). Both terms + // move together or the partition `rows + declined === routesDeclared` breaks: a member + // counted here but skipped there would read as a row this parse declined to read, and + // fire a PARTIAL-read verdict on an accurate ledger. + const routesDeclared = [...code.matchAll(/route\s*:\s*'/g)].filter((d) => !inTypeDecl(d.index)).length + + declined.filter((d) => d.key === 'route').length; const clientsDeclared = rows.filter((r) => r.client).length + declined.filter((d) => d.key === 'client').length; declined.sort((a, b) => a.line - b.line || a.key.localeCompare(b.key)); // THE REPORTING HALF (#10683). The mask closes the phantom-row hole by making a @@ -2278,6 +2322,103 @@ function selfTest() { check('parseLedgerSource', 'a `route:` inside a string PAYLOAD is not a row', 'row count', 1, payload.rows.length); check('parseLedgerSource', 'and it is named, not silently dropped', 'outsideCode', 1, payload.outsideCode.length); + // ---- A LITERAL-UNION `route:` TYPE MEMBER IS NOT A ROW (#10793) --------------- + // The OTHER half of "is this in code position", and the half `codeOnly` cannot answer: + // type declarations ARE code, so a member spelled `route: 'GET /a' | 'GET /b'` opens with + // the very quote the recognizer reads and used to become a ROW. Silent in the same two + // ways as the comment case above — `rows` and `routesDeclared` moved together so the + // partial-read guard saw no gap, and `outsideCode` saw nothing either because the lead + // really IS in code position. `orphanSource` above pins this exact spelling on the + // `client:` key, written there because a literal-union member opens with that quote; this + // is the `route:` twin, which had nothing at all keeping it out of the row loop. + // + // ⚠️ PINNED IN BOTH DIRECTIONS, like the mask cases: a fix that reached the type member by + // swallowing the table AFTER it — a brace match that ran away — would pass a test asserting + // only that the phantom is gone, while silently dropping all 259 live rows. + const typeUnionSource = [ + "export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }", + 'export const L = [', + " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },", + '];', + ].join('\n'); + const typeUnion = parseLedgerSource(typeUnionSource); + check('parseLedgerSource', 'a literal-union `route:` TYPE member is not a row', 'row count', + 1, typeUnion.rows.length); + check('parseLedgerSource', 'and the row in CODE still is — carrying its VALUE, not the member\'s', 'row', + 'GET /api/v1/meta → meta.getTypes', `${typeUnion.rows[0]?.route} → ${typeUnion.rows[0]?.client}`); + check('parseLedgerSource', 'the denominator drops it too, so no phantom gap opens', 'declared', + '1 route / 1 client', `${typeUnion.routesDeclared} route / ${typeUnion.clientsDeclared} client`); + check('parseLedgerSource', 'and nothing is billed as declined for it', 'declined', + 0, typeUnion.declined.length); + // THE REJECT SIDE, ASSERTED POSITIVELY — `orphanSource`'s discipline on the other key. A + // type member must reach NO report: not `declined` (it is not an unread row) and not + // `outsideCode` (it is not prose). "Produces nothing" is correct here and only here, + // because it is a declaration of a TYPE and never a table row. + check('parseLedgerSource', 'a type member reaches no report at all — it is not an unread row', 'line 1 entries', + false, typeUnion.declined.some((d) => d.line === 1) || typeUnion.outsideCode.some((d) => d.line === 1)); + const typeUnionNoInterface = parseLedgerSource(typeUnionSource.split('\n').slice(1).join('\n')); + check('parseLedgerSource', 'and deleting the interface changes NOTHING — it contributed no count', + 'declared', '1 row / 1 route / 1 client / 0 declined', + `${typeUnionNoInterface.rows.length} row / ${typeUnionNoInterface.routesDeclared} route / ${typeUnionNoInterface.clientsDeclared} client / ${typeUnionNoInterface.declined.length} declined`); + + // The expensive shape, as on the comment side: a member that unions a `client:` too used to + // mint a FULLY CLIENT-BOUND phantom, which then joined the UNREACHABLE population — no + // registrar tail can match a route nobody mounts — and inflated the 176 with no verdict. + const typeUnionClient = parseLedgerSource([ + "export interface Entry { route: 'GET /api/v1/gone'; client: 'meta.getGone' }", + 'export const L = [', + " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },", + '];', + ].join('\n')); + check('parseLedgerSource', 'a member unioning a `client:` too mints no client-bound phantom', 'client rows', + 1, typeUnionClient.rows.filter((r) => r.client).length); + check('parseLedgerSource', 'and the surviving row keeps its OWN binding, never the member\'s', 'client', + 'meta.getTypes', typeUnionClient.rows[0]?.client); + + // `type X = { … }` is the other spelling `typeDeclRegions` recognises, and the recognizer + // reads the same region list rather than a second idea of what a type member is. + const typeAlias = parseLedgerSource([ + "export type Entry = { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string };", + 'export const L = [', + " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },", + '];', + ].join('\n')); + check('parseLedgerSource', 'a `type X = { … }` member is skipped on the same list', 'row count', + 1, typeAlias.rows.length); + check('parseLedgerSource', 'and its row still reads', 'row', 'GET /api/v1/meta → meta.getTypes', + `${typeAlias.rows[0]?.route} → ${typeAlias.rows[0]?.client}`); + + // THE RUNAWAY-BRACE DIRECTION, from the other side: the region ends where the declaration's + // brace does, so a table BEFORE a type declaration is read whole and the member after it is + // still skipped. The failure this pins is a skip that starts at the member and never ends. + const typeUnionTrailing = parseLedgerSource([ + 'export const L = [', + " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },", + " { route: 'GET /api/v1/docs', family: 'metadata', disposition: 'server-only' },", + '];', + "export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }", + ].join('\n')); + check('parseLedgerSource', 'a table BEFORE a type declaration is read whole', 'row count', + 2, typeUnionTrailing.rows.length); + check('parseLedgerSource', 'with both values and the one binding intact', 'rows', + 'GET /api/v1/meta → meta.getTypes | GET /api/v1/docs → null', + typeUnionTrailing.rows.map((r) => `${r.route} → ${r.client}`).join(' | ')); + check('parseLedgerSource', 'and the trailing member still contributes no count', 'declared', + '2 route / 1 client / 0 declined', + `${typeUnionTrailing.routesDeclared} route / ${typeUnionTrailing.clientsDeclared} client / ${typeUnionTrailing.declined.length} declined`); + + // The verdict a reader actually sees carries none of it — a ledger whose entry interface + // spells its `route:` as a union is an ACCURATE ledger, and reddening CI over it would be + // the same false red the type-member exclusion exists to prevent. + const typeUnionCov = bridgeCoverageFrom([{ file: 'i-route-ledger.ts', ...typeUnion }], ['/api/v1/meta']); + check('bridgeCoverageFrom', 'a type-member lead carries NO broken-scan verdict', 'brokenScan', + 0, typeUnionCov.brokenScan.length); + check('bridgeCoverageFrom', 'and the ratios it must not move are whole', 'read', + '1 of 1 route / 1 of 1 client', + `${typeUnionCov.rowsParsed} of ${typeUnionCov.routesDeclared} route / ${typeUnionCov.clientRows} of ${typeUnionCov.clientsDeclared} client`); + check('bridgeCoverageFrom', 'and it is not billed as a prose lead either', 'leadsOutsideCode', + 0, typeUnionCov.leadsOutsideCode); + // ⛔ REPORTED, NEVER A VERDICT. A comment explaining a retired row by quoting its old path // is legitimate prose; reddening CI over it is the false red the #9747 family declines. const phantomCov = bridgeCoverageFrom([{ file: 'h-route-ledger.ts', ...phantom }], ['/api/v1/meta']); From bfe43f064f34edd76ec2e72e352aa7e8c7b3c1c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 16:25:24 +0000 Subject: [PATCH 2/2] docs(devx): state that the row recognizer reads through both exclusions The README's type-declaration paragraph described the exclusion as the COUNTER's. After #10683 and #10793 the recognizer reads through both masks too, and a paragraph that stops at the counter reads as if the recognizer still parses prose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- scripts/docs-audit/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/docs-audit/README.md b/scripts/docs-audit/README.md index 7915d924aa..284a3edb15 100644 --- a/scripts/docs-audit/README.md +++ b/scripts/docs-audit/README.md @@ -163,6 +163,23 @@ sentence *"It never named a mounted route: the branch"*. Both exclusions are pin today's accurate ledgers red (`259 of 266`), which is why a bare "count every `route:`" check was never an option. +**The row recognizer reads through both of those exclusions too** — comments and string +payloads since #10683, type declarations since #10793. Before each, the recognizer read a +source the rest of the file had already ruled out, and a lead it should not have read did +not merely mis-count: it became a **row**. Both were silent for the same reason — `rows` +and the first term of `routesDeclared` moved together, so the partial-read verdict, which +keys on the gap between them, had no gap to see. The type-declaration case is the one a +quote test cannot catch on its own: a literal-union member (`route: 'GET /a' | 'GET /b'`) +opens with the very quote the recognizer reads. Both moves were priced on a tree carrying +no instance of the shape, and `259 of 259` / `221 of 221` / 176 unreachable came out +byte-identical across each. + +A skipped type member is reported **nowhere**, and that is the intended answer rather than +a new silence: it is a correct declaration of a *type*, not a table row — the same rule +under which the `route: string;` member of all seven entry interfaces has always produced +nothing. What the prose case gets instead is `prose-quoted leads`, because a lead sitting +where the mask says code is not is a would-be row and worth naming. + ### What it cannot see is reported, never implied `anchorlessChanges` lists changed files that yielded no anchor at all; a non-empty value