Skip to content

Commit ba2d8d4

Browse files
committed
fix(tooling): give a non-literal ledger route: a verdict instead of silence
`parseLedgerSource` reported every `route:` / `client:` STRING LITERAL it declined to read (#9896), but a value in no quote at all — `route: ROUTES.health`, `route: BASE + '/types'` — was read by neither half: the recognizer needs a leading `'`, the counter needs a leading `"` or backtick. Such a row left the population with no verdict of any kind, absent even from the denominator. Measured on a real ledger: adding one such row left `--bridge-coverage` reporting `259 of 259 declared`, exit 0, silent. `unreadableIn` counts those. #9896 kept the quote requirement because it was the only EXACT discriminator it had against the `route: string;` member all seven entry interfaces declare; this adds a second exact one — a type member sits inside an `interface`/`type` declaration and a table row never does — so the counter can widen without billing that member as a row. Comments and string contents are blanked by the same `js-comment-mask.mjs` the rest of the repo uses, which removes the English sentence in `runtime/src/route-ledger.ts` by construction. No TypeScript parse: `affected-docs.mjs` runs in `docs-drift-check.yml`, the one job that deliberately never runs `pnpm install`, and `scripts/ts-parse.mjs` imports `typescript`. Verified: the mapper self-tests clean with no `node_modules`, while importing `ts-parse.mjs` there fails ERR_MODULE_NOT_FOUND. The recognizer is untouched — `rows` is byte for byte what it was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
1 parent 5e50e2e commit ba2d8d4

2 files changed

Lines changed: 190 additions & 25 deletions

File tree

scripts/docs-audit/README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,21 @@ declared` — and a declined declaration is a `brokenScan` verdict that **names
147147
reports its narrowness, it does not widen it, and `--self-test` pins the population at its
148148
old value so a silent widening fails there.
149149

150-
⚠️ **Boundary, declared not discovered:** a `route:` whose value is not a string literal at
151-
all (`route: ROUTES.health`) is invisible to the recognizer *and* to this counter — the only
152-
exact discriminator against the `route: string;` member each ledger's own entry interface
153-
declares is the opening quote. Pinned in `--self-test`; filed separately rather than papered
154-
over with a heuristic.
150+
**Values in no quote at all are counted too.** A `route:` / `client:` whose value is not a
151+
string literal (`route: ROUTES.health`, `route: BASE + '/x'`) is read by neither the
152+
recognizer (which needs a leading `'`) nor the declined-spelling counter (which needs a
153+
leading `"` or backtick). Such a row used to leave the population with **no verdict at all**
154+
not read, not declined, absent from the denominator. It is now reported as unread, so the
155+
row ends up in a verdict either way.
156+
157+
The `route: string;` member each ledger's own entry interface declares is **not** a row, and
158+
is excluded on a structural discriminator rather than on the quote: a type member sits inside
159+
an `interface`/`type` declaration and a table row never does. Comments and string payloads
160+
are masked out for the same reason — `runtime/src/route-ledger.ts` contains the English
161+
sentence *"It never named a mounted route: the branch"*. Both exclusions are pinned in
162+
`--self-test` in both directions; removing the type-declaration one turns all seven of
163+
today's accurate ledgers red (`259 of 266`), which is why a bare "count every `route:`"
164+
check was never an option.
155165

156166
### What it cannot see is reported, never implied
157167

scripts/docs-audit/affected-docs.mjs

Lines changed: 175 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
155155
import { join, relative } from 'node:path';
156156
// The one answer to "is this span a comment, or code?" (#9367). Dependency-free and
157157
// side-effect-free on import, so the no-install contract this script runs under holds.
158-
import { maskComments } from '../js-comment-mask.mjs';
158+
import { blank, maskComments, scanSource } from '../js-comment-mask.mjs';
159159

160160
const repoRoot = execSync('git rev-parse --show-toplevel').toString().trim();
161161
const args = process.argv.slice(2);
@@ -1183,8 +1183,8 @@ function bridgeCoverageFrom(ledgers, tails) {
11831183
const named = l.declined.slice(0, 3).map((d) => `line ${d.line}: ${d.text}`).join(', ');
11841184
brokenScan.push(
11851185
`${l.file} is a PARTIAL read of that ledger, not its shape — the row recognizer reads single-quoted values only`
1186-
+ ` and read ${l.rowsParsed} of ${l.routesDeclared} declared string-literal \`route:\` value(s)`
1187-
+ ` and ${l.clientRows} of ${l.clientsDeclared} declared string-literal \`client:\` value(s)`
1186+
+ ` and read ${l.rowsParsed} of ${l.routesDeclared} declared \`route:\` value(s)`
1187+
+ ` and ${l.clientRows} of ${l.clientsDeclared} declared \`client:\` value(s)`
11881188
+ (l.declined.length
11891189
? `; declined ${l.declined.length}: ${named}${l.declined.length > 3 ? `, +${l.declined.length - 3} more` : ''}`
11901190
: '')
@@ -1240,6 +1240,91 @@ function scanRouteSurface() {
12401240
return { registrarFiles, ledgers, ledgerRows, registrarByTail };
12411241
}
12421242

1243+
/**
1244+
* The source with comments AND string/template/regex CONTENTS blanked, quotes and all other
1245+
* code bytes kept in place. Both masks come from the one answer to "is this span code?"
1246+
* (`js-comment-mask.mjs`), so this cannot drift from what the rest of the repo means by it.
1247+
*
1248+
* The quote characters SURVIVE the blanking, which is the property `unreadableIn` rides on:
1249+
* a value that still opens with a quote here is one the recognizer or `declinedIn` already
1250+
* accounts for, and a value that does not is one nothing has read.
1251+
*/
1252+
function codeOnly(source) {
1253+
const { comment, literal } = scanSource(source);
1254+
const both = new Uint8Array(comment.length);
1255+
for (let i = 0; i < both.length; i++) both[i] = comment[i] || literal[i];
1256+
return blank(source, both);
1257+
}
1258+
1259+
/**
1260+
* The spans of every `interface X { … }` / `type X = { … }` declaration in already-blanked
1261+
* source. This is the EXACT discriminator against the `route: string;` member that all seven
1262+
* ledgers' own entry interfaces declare — the one thing a widened counter must not bill as a
1263+
* row (#10500). It is structural, not a guess about the value's spelling: a type member is
1264+
* inside a type declaration's braces and a table row never is, whatever either is written as.
1265+
*
1266+
* A `type X = string;` with no object body is skipped rather than brace-matched onto some
1267+
* later block: the `;` arriving before any `{` is what says the declaration has no members.
1268+
*/
1269+
function typeDeclRegions(code) {
1270+
const regions = [];
1271+
const re = /\b(?:interface\s+[A-Za-z_$][\w$]*|type\s+[A-Za-z_$][\w$]*\s*=)/g;
1272+
let m;
1273+
while ((m = re.exec(code)) !== null) {
1274+
const open = code.indexOf('{', m.index);
1275+
if (open === -1) continue;
1276+
const semi = code.indexOf(';', m.index);
1277+
if (semi !== -1 && semi < open) continue;
1278+
let depth = 0;
1279+
let end = code.length;
1280+
for (let i = open; i < code.length; i++) {
1281+
if (code[i] === '{') depth++;
1282+
else if (code[i] === '}' && --depth === 0) { end = i; break; }
1283+
}
1284+
regions.push([m.index, end]);
1285+
}
1286+
return regions;
1287+
}
1288+
1289+
/**
1290+
* Every `route:` / `client:` declaration a ledger makes that is not a string literal in ANY
1291+
* quote — `route: ROUTES.health`, `route: BASE + '/types'` — and so is read by NEITHER the
1292+
* recognizer (which needs a leading `'`) NOR `declinedIn` (which needs a leading `"` or
1293+
* backtick). Before #10500 such a row left the population with no verdict at all: not read,
1294+
* not declined, not counted in the denominator. That is the same silence #9896 closed for the
1295+
* quote spellings, one spelling further out, and silence is the defect either way.
1296+
*
1297+
* ⛔ This does NOT widen the recognizer either. Like `declinedIn`, it only reports what could
1298+
* not be read — `rows` is untouched, byte for byte.
1299+
*
1300+
* Two exclusions, both exact rather than heuristic, and both measured on the seven live
1301+
* ledgers (`route:` 267 raw occurrences, 259 read, 8 non-quoted — and all 8 are excluded here):
1302+
* • COMMENTS and STRING CONTENTS are blanked, so the English sentence in
1303+
* `runtime/src/route-ledger.ts` ("It never named a mounted route: the branch") is gone by
1304+
* construction rather than by an allowlist.
1305+
* • TYPE DECLARATIONS are skipped, so the `route: string;` member each of the seven entry
1306+
* interfaces declares is not billed as an unread row. #9896 kept the quote requirement
1307+
* precisely because it was the only exact discriminator it had against that member;
1308+
* `typeDeclRegions` is a second exact one, which is what lets the counter widen at all.
1309+
*/
1310+
function unreadableIn(text) {
1311+
const code = codeOnly(text);
1312+
const skip = typeDeclRegions(code);
1313+
const out = [];
1314+
const re = /\b(route|client)\s*:[ \t]*/g;
1315+
let m;
1316+
while ((m = re.exec(code)) !== null) {
1317+
const at = m.index + m[0].length;
1318+
const ch = code[at];
1319+
if (ch === "'" || ch === '"' || ch === '`') continue;
1320+
if (skip.some(([a, b]) => m.index >= a && m.index <= b)) continue;
1321+
const line = text.slice(at).split('\n')[0];
1322+
const cut = line.search(/[,;]/);
1323+
out.push({ index: m.index, key: m[1], text: `${m[1]}: ${(cut === -1 ? line : line.slice(0, cut)).trim().slice(0, 60)}` });
1324+
}
1325+
return out;
1326+
}
1327+
12431328
/**
12441329
* Every `route:` / `client:` declaration in a ledger whose value opens with a quote the row
12451330
* recognizer below does NOT read. Written once, here, so the recognizer and its complement
@@ -1292,11 +1377,14 @@ function declinedIn(s) {
12921377
* would move the measured population, which is a separate decision with a before/after
12931378
* standard attached — see the header of `--bridge-coverage`.
12941379
*
1295-
* ⚠️ KNOWN BOUNDARY, pinned in `--self-test` rather than left to be discovered: a `route:`
1296-
* whose value is not a string literal at all (`route: ROUTES.health`) is invisible to the
1297-
* recognizer AND to this counter, because the only exact discriminator against the
1298-
* `route: string;` member every ledger's own entry interface declares is the quote. The
1299-
* numerator below therefore says "string-literal declaration(s)" and claims nothing wider.
1380+
* THAT BOUNDARY IS NOW CLOSED (#10500). A `route:` whose value is not a string literal at
1381+
* all (`route: ROUTES.health`, `route: BASE + '/x'`) used to be invisible to the recognizer
1382+
* AND to this counter — read by neither half, so the row left the population with no verdict
1383+
* of any kind. `unreadableIn` counts those too, and the `route: string;` member every
1384+
* ledger's own entry interface declares stays out of the count on an EXACT discriminator
1385+
* rather than on the quote heuristic: the member sits inside a type declaration and a table
1386+
* row never does. The denominator below is therefore every declared `route:` value, in any
1387+
* spelling, and the partition `rows + declined === routesDeclared` is pinned in `--self-test`.
13001388
*
13011389
* @returns {{rows: Array<{route: string, client: string|null}>, declined: Array<{key: string, line: number, text: string}>, routesDeclared: number, clientsDeclared: number}}
13021390
*/
@@ -1325,6 +1413,12 @@ function parseLedgerSource(text) {
13251413
for (const d of declinedIn(text)) {
13261414
if (d.key === 'route') declined.push({ key: 'route', line: lineAt(d.index), text: d.text });
13271415
}
1416+
// …and the values no quote-keyed scan can see at all (#10500). File-wide for the same
1417+
// reason: a row whose `route:` is not a literal has no window either, which is exactly
1418+
// why it used to leave the population without a verdict of any kind.
1419+
for (const d of unreadableIn(text)) {
1420+
declined.push({ key: d.key, line: lineAt(d.index), text: d.text });
1421+
}
13281422
// THE DENOMINATOR. Leads the recognizer's own regex would start on, plus the ones it
13291423
// declined — so `routesDeclared - rows.length` is every declared row value this parse did
13301424
// not turn into a row, whatever the reason: a declined quote, or the `route: ''` that its
@@ -1708,7 +1802,7 @@ function selfTest() {
17081802
check('parseLedgerSource', 'and the declined client, on its own line', 'line 5 backtick client',
17091803
true, partial.declined.some((d) => d.key === 'client' && d.line === 5 && d.text.includes('meta.getAudit')));
17101804
// The partition — the reason a shared spelling constant is not needed and would not help.
1711-
// Every declared string-literal `route:` is either read as a row or declined; a future
1805+
// Every declared `route:`, in ANY spelling, is either read as a row or declined; a future
17121806
// edit that lets one fall between the two fails HERE rather than in production silence.
17131807
check('parseLedgerSource', 'read + declined accounts for every declared `route:`', 'partition',
17141808
partial.routesDeclared, partial.rows.length + partial.declined.filter((d) => d.key === 'route').length);
@@ -1717,7 +1811,7 @@ function selfTest() {
17171811
check('bridgeCoverageFrom', 'a partial read is a VERDICT, not a smaller number', 'brokenScan',
17181812
true, partialCov.brokenScan.some((v) => v.includes('PARTIAL read')));
17191813
check('bridgeCoverageFrom', 'and the verdict carries the numerator', '2 of 4',
1720-
true, partialCov.brokenScan.some((v) => v.includes('2 of 4 declared string-literal `route:`')));
1814+
true, partialCov.brokenScan.some((v) => v.includes('2 of 4 declared `route:`')));
17211815
check('bridgeCoverageFrom', 'and NAMES the entry it could not read', 'i18n/locales',
17221816
true, partialCov.brokenScan.some((v) => v.includes('GET /api/v1/i18n/locales')));
17231817
// ⛔ THE POINT. The pre-existing guard is `rowsParsed === 0` — all-or-nothing — so it is
@@ -1752,21 +1846,82 @@ function selfTest() {
17521846
true, stealCov.brokenScan.some((v) => v.includes('PARTIAL read')));
17531847
check('bridgeCoverageFrom', 'and the correctly spelled twin carries none', 'brokenScan', 0, cleanCov.brokenScan.length);
17541848

1755-
// ⚠️ THE COUNTER'S OWN BOUNDARY, declared rather than discovered later. A `route:` whose
1756-
// value is not a string literal is invisible to the recognizer AND to the counter: the
1757-
// only exact discriminator against the `route: string;` member that every ledger's entry
1758-
// interface declares (7 of 7 on this tree, one each) is the opening quote. Filed rather
1759-
// than absorbed — see the issue linked from this block's PR.
1760-
const nonLiteral = parseLedgerSource([
1761-
'export interface Entry { route: string; client?: string }',
1849+
// ---- A NON-LITERAL `route:` IS A VERDICT TOO (#10500) ---------------------
1850+
// #9896 made the QUOTED spellings loud and declared this one out of scope: a value that
1851+
// is not a string literal at all (`route: ROUTES.health`, `route: BASE + '/x'`) was read
1852+
// by neither the recognizer nor the counter, so the row left the population with no
1853+
// verdict — the same silence, one spelling further out. The quote requirement was kept
1854+
// because it was the only EXACT discriminator against the `route: string;` member every
1855+
// ledger's entry interface declares. `unreadableIn` has a second exact one — the member
1856+
// is inside a type declaration and a row never is — so the counter can widen without
1857+
// billing that member as a row. Both directions are pinned here, because counting the
1858+
// interface member is precisely how a fix here goes wrong.
1859+
const nonLiteralSource = [
1860+
'export interface Entry { route: string; client: string }',
17621861
'export const L = [',
17631862
' { route: ROUTES.health, family: \'ops\', disposition: \'server-only\' },',
17641863
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
17651864
'];',
1766-
].join('\n'));
1767-
check('parseLedgerSource', 'a non-literal `route:` is out of scope for the counter, and so is `route: string;`',
1768-
'declared', '1 route / 1 client / 0 declined',
1865+
].join('\n');
1866+
const nonLiteral = parseLedgerSource(nonLiteralSource);
1867+
check('parseLedgerSource', 'a non-literal `route:` is COUNTED now — read + unread accounts for every row',
1868+
'declared', '2 route / 1 client / 1 declined',
17691869
`${nonLiteral.routesDeclared} route / ${nonLiteral.clientsDeclared} client / ${nonLiteral.declined.length} declined`);
1870+
check('parseLedgerSource', 'the narrow population is UNCHANGED — this reports, it does not widen', 'row count',
1871+
1, nonLiteral.rows.length);
1872+
check('parseLedgerSource', 'and the unread row NAMES itself, with its line', 'line 3 ROUTES.health',
1873+
'line 3 route: ROUTES.health',
1874+
nonLiteral.declined.map((d) => `line ${d.line} ${d.text}`).join(' | '));
1875+
1876+
// THE NEGATIVE CONTROL, asserted positively rather than by the absence of a failure: the
1877+
// `route: string;` / `client: string` members on line 1 are TYPES, not rows. Two ways to
1878+
// say it, because "0 extra declined" alone would also pass if the scan had stopped working.
1879+
check('parseLedgerSource', 'the `route: string;` interface member is NOT billed as an unread row',
1880+
'no member in the declined list', false,
1881+
nonLiteral.declined.some((d) => /\bstring\b/.test(d.text)));
1882+
const noInterface = parseLedgerSource(nonLiteralSource.split('\n').slice(1).join('\n'));
1883+
check('parseLedgerSource', 'and deleting the interface changes NOTHING — it contributed no count',
1884+
'declared', `${noInterface.routesDeclared} route / ${noInterface.declined.length} declined`,
1885+
`${nonLiteral.routesDeclared} route / ${nonLiteral.declined.length} declined`);
1886+
1887+
// The verdict actually reaches the surface a reader sees.
1888+
const nonLiteralCov = bridgeCoverageFrom([{ file: 'f-route-ledger.ts', ...nonLiteral }], ['/api/v1/meta']);
1889+
check('bridgeCoverageFrom', 'an unreadable row is a VERDICT, not a smaller number', 'brokenScan',
1890+
true, nonLiteralCov.brokenScan.some((v) => v.includes('PARTIAL read') && v.includes('ROUTES.health')));
1891+
1892+
// …and the correctly spelled twin still carries none, so this cannot false-red an
1893+
// accurate ledger — the failure mode a naive "count every `route:`" would have had on
1894+
// all seven of today's ledgers at once.
1895+
const literalTwin = parseLedgerSource(nonLiteralSource.replace('ROUTES.health', "'GET /api/v1/health'"));
1896+
check('parseLedgerSource', 'the single-quoted twin declines nothing', 'declined', 0, literalTwin.declined.length);
1897+
check('bridgeCoverageFrom', 'and carries no verdict', 'brokenScan', 0,
1898+
bridgeCoverageFrom([{ file: 'f-route-ledger.ts', ...literalTwin }], ['/api/v1/meta', '/api/v1/health']).brokenScan.length);
1899+
1900+
// A non-literal `client:` is the same defect on the other key: the row keeps its seat and
1901+
// loses its binding, which no count comparison can see.
1902+
const nonLiteralClient = parseLedgerSource([
1903+
'export interface Entry { route: string; client: string }',
1904+
'export const L = [',
1905+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: CLIENTS.getTypes },",
1906+
'];',
1907+
].join('\n'));
1908+
check('parseLedgerSource', 'a non-literal `client:` costs the row its binding, and is COUNTED',
1909+
'1 route / 1 client / 1 declined', '1 route / 1 client / 1 declined',
1910+
`${nonLiteralClient.routesDeclared} route / ${nonLiteralClient.clientsDeclared} client / ${nonLiteralClient.declined.length} declined`);
1911+
check('parseLedgerSource', 'and the declined entry is the CLIENT one', 'client', 'client',
1912+
nonLiteralClient.declined[0]?.key);
1913+
1914+
// Comments and string payloads are not declarations. `runtime/src/route-ledger.ts` carries
1915+
// the live instance of the first ("It never named a mounted route: the branch"), which is
1916+
// why a raw `/route\s*:/` scan would red on an accurate ledger.
1917+
const prose = parseLedgerSource([
1918+
'// It never named a mounted route: the branch was dead.',
1919+
"const msg = 'route: not a declaration';",
1920+
'export const L = [',
1921+
" { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },",
1922+
'];',
1923+
].join('\n'));
1924+
check('parseLedgerSource', 'prose and string payloads are not unread rows', 'declined', 0, prose.declined.length);
17701925

17711926
// End to end over those three fixtures: the #9192 recall miss must come back.
17721927
// `auditMetaItem` (changed) → `/:type/:name/audit` (registrar) → `meta.getAudit`

0 commit comments

Comments
 (0)