@@ -155,7 +155,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
155155import { 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
160160const repoRoot = execSync ( 'git rev-parse --show-toplevel' ) . toString ( ) . trim ( ) ;
161161const 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 (?: i n t e r f a c e \s + [ A - Z a - z _ $ ] [ \w $ ] * | t y p e \s + [ A - Z a - 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 ( r o u t e | c l i e n t ) \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 ) => / \b s t r i n g \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