@@ -1422,12 +1422,51 @@ function unclaimedClientsIn(text, claimed) {
14221422 * A FUNCTION DECLARATION rather than a `const` arrow, deliberately: `--self-test`
14231423 * short-circuits near the top of this file, before any `const` down here has initialized,
14241424 * and a TDZ error there takes the whole self-test down instead of failing one check.
1425+ *
1426+ * IT NO LONGER DECIDES "IS THIS A TYPE MEMBER?" FOR ITSELF (#10901). It used to run over
1427+ * raw text through NEITHER of #10500's discriminators while the recognizer read through
1428+ * both, so a literal-union `route: "GET /a" | "GET /b"` TYPE member — the identical shape
1429+ * #10793 kept out of `rows`, written in either of the two quotes this scan reads — was
1430+ * billed as a value the parse FAILED to read. That is a verdict, not a number: the entry
1431+ * is NAMED with its line and `bridgeCoverageFrom` raises a PARTIAL read with exit 1, on a
1432+ * ledger that is completely accurate. Measured on the tree this landed on, one file
1433+ * declaring ONE row:
1434+ *
1435+ * export interface Entry { route: "GET /api/v1/gone" | "GET /api/v1/meta"; client: string }
1436+ * export const L = [
1437+ * { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },
1438+ * ];
1439+ * ⇒ rows 1 · routesDeclared 2 · declined 1 · brokenScan 1 (backtick: identical)
1440+ *
1441+ * So the region list arrives as a PARAMETER, from the one caller that already computes it,
1442+ * and there is no default: a call site that forgot the discriminator would silently
1443+ * reintroduce exactly the second opinion this closes, and the file argues twice that two
1444+ * scans each deciding "in code position" separately are two scans that can drift into
1445+ * disagreeing while both look right. `offset` is the absolute position of `s` in the file,
1446+ * so the translation between the in-window slice's coordinates and the region list's
1447+ * happens HERE, once, instead of at one of the two call sites — the returned `index` is
1448+ * absolute for both.
1449+ *
1450+ * ⛔ THE OTHER DISCRIMINATOR IS DELIBERATELY NOT APPLIED HERE. This still reads RAW bytes,
1451+ * so a `route: "GET /x"` written in a COMMENT is still billed as a declined row. That is
1452+ * #10794, closed `not planned` on the reasoning that it is the LOUD direction and has no
1453+ * puller — and it is not this card's to reverse. It is also not a free change of lens:
1454+ * `codeOnly` blanks string CONTENTS, and this scan's whole job is to quote the unread
1455+ * spelling back at the reader, so a masked window would name `route: ""` for every entry.
1456+ * `--self-test` pins that boundary in place, and a future card closing #10794 is expected
1457+ * to move that pin rather than to find it missing.
14251458 */
1426- function declinedIn ( s ) {
1427- return [ ...s . matchAll ( / ( r o u t e | c l i e n t ) \s * : \s * ( [ " ` ] ) ( [ ^ \n ] { 0 , 120 } ) / g) ] . map ( ( d ) => {
1459+ function declinedIn ( s , offset , inTypeDecl ) {
1460+ const out = [ ] ;
1461+ for ( const d of s . matchAll ( / ( r o u t e | c l i e n t ) \s * : \s * ( [ " ` ] ) ( [ ^ \n ] { 0 , 120 } ) / g) ) {
1462+ const index = offset + d . index ;
1463+ // …and never a TYPE member (#10901), on the SAME region list the recognizer and the
1464+ // denominator read — not a second idea of what a type member is.
1465+ if ( inTypeDecl ( index ) ) continue ;
14281466 const end = d [ 3 ] . indexOf ( d [ 2 ] ) ;
1429- return { index : d . index , key : d [ 1 ] , text : `${ d [ 1 ] } : ${ d [ 2 ] } ${ end === - 1 ? d [ 3 ] . slice ( 0 , 60 ) : d [ 3 ] . slice ( 0 , end ) } ${ d [ 2 ] } ` } ;
1430- } ) ;
1467+ out . push ( { index, key : d [ 1 ] , text : `${ d [ 1 ] } : ${ d [ 2 ] } ${ end === - 1 ? d [ 3 ] . slice ( 0 , 60 ) : d [ 3 ] . slice ( 0 , end ) } ${ d [ 2 ] } ` } ) ;
1468+ }
1469+ return out ;
14311470}
14321471
14331472/**
@@ -1580,20 +1619,27 @@ function parseLedgerSource(text) {
15801619 // …over the RAW bytes of that same window. `declinedIn`'s whole job is to NAME an
15811620 // unread spelling, and the masked window would name `client: ""` for every one of them.
15821621 // The BYTE RANGE is the code-derived window's, so this is the same span, read for its
1583- // text. ⛔ That leaves `declinedIn` itself still reading prose — a `route: "GET /x"`
1584- // written in a comment is still billed as a declined row. It is the SAME asymmetry one
1585- // scan over, but it fails in the opposite direction (a declined entry is NAMED and
1586- // carries a verdict, so it is a false RED, not a phantom row), which makes it a
1587- // separate call with its own before/after rather than a rider here. Filed as #10794.
1588- for ( const d of declinedIn ( text . slice ( m . index , m . index + window . length ) ) ) {
1622+ // text — but it now reads that span through the TYPE-DECLARATION discriminator (#10901),
1623+ // on the same `typeDecls` list the loop above and the denominator below read. An entry
1624+ // interface trailing the table lands INSIDE this window (measured: `client: "a" | "b"`
1625+ // on the line after the table's `];` was billed as a declined client, `clientsDeclared`
1626+ // 2 on a file declaring 1, exit 1), and a type member is not a row this parse failed to
1627+ // read — it is a correct declaration of a TYPE.
1628+ //
1629+ // ⛔ Still RAW, and still through no `codeOnly`: a `route: "GET /x"` written in a
1630+ // COMMENT is still billed as a declined row. That is #10794, closed `not planned`
1631+ // (loud direction, no puller), and this card does not reverse it — see `declinedIn`.
1632+ for ( const d of declinedIn ( text . slice ( m . index , m . index + window . length ) , m . index , inTypeDecl ) ) {
15891633 if ( d . key !== 'client' ) continue ;
1590- claimed . add ( m . index + d . index ) ;
1591- declined . push ( { key : 'client' , line : lineAt ( m . index + d . index ) , text : d . text } ) ;
1634+ claimed . add ( d . index ) ;
1635+ declined . push ( { key : 'client' , line : lineAt ( d . index ) , text : d . text } ) ;
15921636 }
15931637 }
15941638 // `route:` is counted file-wide, because a declined row has no window to be found in —
1595- // which is precisely why it was invisible.
1596- for ( const d of declinedIn ( text ) ) {
1639+ // which is precisely why it was invisible. Type members are skipped here on the same list
1640+ // (#10901): a leading entry interface sits in no row window at all, so this is the call
1641+ // site the card's own fixture arrives through.
1642+ for ( const d of declinedIn ( text , 0 , inTypeDecl ) ) {
15971643 if ( d . key === 'route' ) declined . push ( { key : 'route' , line : lineAt ( d . index ) , text : d . text } ) ;
15981644 }
15991645 // …and the values no quote-keyed scan can see at all (#10500). File-wide for the same
@@ -2419,6 +2465,130 @@ function selfTest() {
24192465 check ( 'bridgeCoverageFrom' , 'and it is not billed as a prose lead either' , 'leadsOutsideCode' ,
24202466 0 , typeUnionCov . leadsOutsideCode ) ;
24212467
2468+ // ---- THE SAME TYPE MEMBER, IN THE TWO QUOTES THE RECOGNIZER DECLINES (#10901) ----
2469+ // #10793 (above) taught the ROW RECOGNIZER and the first term of its denominator to read
2470+ // through both of #10500's discriminators. Its complement did not move: `declinedIn` ran
2471+ // over RAW text through NEITHER, so the very same member — a `route:` union written in a
2472+ // double quote or a backtick instead of a single one — was billed as a value the parse
2473+ // FAILED to read. That is the LOUD twin of the bug above: not a phantom row joining the
2474+ // population in silence, but a named entry and a PARTIAL-read verdict with exit 1, on a
2475+ // ledger that is completely accurate. The type-declaration exclusion exists to prevent
2476+ // exactly that false red, and this is the last scan in the file that was not reading it.
2477+ //
2478+ // BOTH SPELLINGS ARE PINNED SEPARATELY. They come out of one regex alternation and one
2479+ // code path, and were measured behaving identically — which is the reason to pin them
2480+ // apart rather than to trust one for both: a later narrowing of that alternation would
2481+ // otherwise take one of them with nothing to say so.
2482+ for ( const [ spelling , q ] of [ [ 'double-quoted' , '"' ] , [ 'backtick-quoted' , '`' ] ] ) {
2483+ const src = [
2484+ `export interface Entry { route: ${ q } GET /api/v1/gone${ q } | ${ q } GET /api/v1/meta${ q } ; client: string }` ,
2485+ 'export const L = [' ,
2486+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2487+ '];' ,
2488+ ] . join ( '\n' ) ;
2489+ const r = parseLedgerSource ( src ) ;
2490+ const cov = bridgeCoverageFrom ( [ { file : 'j-route-ledger.ts' , ...r } ] , [ '/api/v1/meta' ] ) ;
2491+ check ( 'parseLedgerSource' , `a ${ spelling } literal-union \`route:\` TYPE member is not an unread row` ,
2492+ 'declined' , 0 , r . declined . length ) ;
2493+ check ( 'parseLedgerSource' , `and a ${ spelling } member moves no denominator either` , 'declared' ,
2494+ '1 row / 1 route / 1 client' ,
2495+ `${ r . rows . length } row / ${ r . routesDeclared } route / ${ r . clientsDeclared } client` ) ;
2496+ check ( 'parseLedgerSource' , `and the row in CODE still reads, beside a ${ spelling } member` , 'row' ,
2497+ 'GET /api/v1/meta → meta.getTypes' , `${ r . rows [ 0 ] ?. route } → ${ r . rows [ 0 ] ?. client } ` ) ;
2498+ // The verdict is the whole point: the number moving is a symptom, the exit code is the
2499+ // defect. An accurate ledger must carry NO broken-scan verdict in any spelling.
2500+ check ( 'bridgeCoverageFrom' , `a ${ spelling } type member carries NO broken-scan verdict` , 'brokenScan' ,
2501+ 0 , cov . brokenScan . length ) ;
2502+ // ⚠️ PINNED IN BOTH DIRECTIONS, like #10793's fixture: an exclusion that reached the
2503+ // member by swallowing the table after it would pass every assertion above while
2504+ // silently dropping every live row. Deleting the interface must change NOTHING.
2505+ const noInterface = parseLedgerSource ( src . split ( '\n' ) . slice ( 1 ) . join ( '\n' ) ) ;
2506+ check ( 'parseLedgerSource' , `and deleting a ${ spelling } member's interface changes NOTHING` , 'declared' ,
2507+ `${ r . rows . length } row / ${ r . routesDeclared } route / ${ r . declined . length } declined` ,
2508+ `${ noInterface . rows . length } row / ${ noInterface . routesDeclared } route / ${ noInterface . declined . length } declined` ) ;
2509+ }
2510+
2511+ // THE OTHER CALL SITE, and the one no `route:` fixture can reach: declined `client:`
2512+ // values are collected per ROW WINDOW, so a member only arrives there when the entry
2513+ // interface TRAILS the table and lands inside the 1200-byte window. Measured before the
2514+ // fix on exactly this shape: `clientsDeclared` 2 on a file declaring one client, one
2515+ // named declined entry, exit 1. The `route:` twin below trails the table too, which is
2516+ // the file-wide call site reached from the other side.
2517+ for ( const [ spelling , q ] of [ [ 'double-quoted' , '"' ] , [ 'backtick-quoted' , '`' ] ] ) {
2518+ const trailingClient = parseLedgerSource ( [
2519+ 'export const L = [' ,
2520+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2521+ '];' ,
2522+ `export interface Entry { route: string; client: ${ q } meta.getTypes${ q } | ${ q } meta.getAudit${ q } }` ,
2523+ ] . join ( '\n' ) ) ;
2524+ check ( 'parseLedgerSource' , `a ${ spelling } literal-union \`client:\` TYPE member inside the row window is not an unread row` ,
2525+ 'declined' , 0 , trailingClient . declined . length ) ;
2526+ check ( 'parseLedgerSource' , `and a ${ spelling } \`client:\` member moves no denominator` , 'declared' ,
2527+ '1 route / 1 client' , `${ trailingClient . routesDeclared } route / ${ trailingClient . clientsDeclared } client` ) ;
2528+ const trailingRoute = parseLedgerSource ( [
2529+ 'export const L = [' ,
2530+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2531+ '];' ,
2532+ `export interface Entry { route: ${ q } GET /api/v1/meta${ q } | ${ q } GET /api/v1/gone${ q } ; client: string }` ,
2533+ ] . join ( '\n' ) ) ;
2534+ check ( 'parseLedgerSource' , `a TRAILING ${ spelling } \`route:\` member is not an unread row either` ,
2535+ 'declared' , '1 route / 0 declined' ,
2536+ `${ trailingRoute . routesDeclared } route / ${ trailingRoute . declined . length } declined` ) ;
2537+ }
2538+
2539+ // `type X = { … }` is the other spelling `typeDeclRegions` recognises, and this scan now
2540+ // reads the same region list rather than a second idea of what a type member is.
2541+ const declinedTypeAlias = parseLedgerSource ( [
2542+ 'export type Entry = { route: "GET /api/v1/gone" | "GET /api/v1/meta"; client: string };' ,
2543+ 'export const L = [' ,
2544+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2545+ '];' ,
2546+ ] . join ( '\n' ) ) ;
2547+ check ( 'parseLedgerSource' , 'a `type X = { … }` member is skipped in the declined spellings too' ,
2548+ 'declared' , '1 row / 1 route / 0 declined' ,
2549+ `${ declinedTypeAlias . rows . length } row / ${ declinedTypeAlias . routesDeclared } route / ${ declinedTypeAlias . declined . length } declined` ) ;
2550+
2551+ // ⚠️ THE LOAD-BEARING DIRECTION, asserted positively. Everything above says a spelling
2552+ // STOPS being reported; an exclusion that swallowed the declined report wholesale would
2553+ // pass every one of those and give back the silence #9896 closed. A real table row whose
2554+ // `route:` is spelled in a quote the recognizer declines is NOT a type member, and must
2555+ // still be named, still move the denominator, and still carry the verdict.
2556+ for ( const [ spelling , q ] of [ [ 'double-quoted' , '"' ] , [ 'backtick-quoted' , '`' ] ] ) {
2557+ const realRow = parseLedgerSource ( [
2558+ 'export interface Entry { route: string; client: string }' ,
2559+ 'export const L = [' ,
2560+ ` { route: ${ q } GET /api/v1/gone${ q } , family: 'metadata', disposition: 'sdk' },` ,
2561+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2562+ '];' ,
2563+ ] . join ( '\n' ) ) ;
2564+ const realCov = bridgeCoverageFrom ( [ { file : 'k-route-ledger.ts' , ...realRow } ] , [ '/api/v1/meta' ] ) ;
2565+ check ( 'parseLedgerSource' , `a ${ spelling } route in a REAL table row is still declined` , 'declined' ,
2566+ 1 , realRow . declined . length ) ;
2567+ check ( 'parseLedgerSource' , `and the ${ spelling } entry still NAMES itself, with its line` , 'line 3' ,
2568+ `3: route: ${ q } GET /api/v1/gone${ q } ` ,
2569+ realRow . declined . map ( ( d ) => `${ d . line } : ${ d . text } ` ) . join ( ' | ' ) ) ;
2570+ check ( 'parseLedgerSource' , `and the partition still holds for the ${ spelling } row — read + declined === declared` , 'partition' ,
2571+ realRow . routesDeclared , realRow . rows . length + realRow . declined . filter ( ( d ) => d . key === 'route' ) . length ) ;
2572+ check ( 'bridgeCoverageFrom' , `and the PARTIAL-read verdict still fires for a ${ spelling } real row` , 'brokenScan' ,
2573+ true , realCov . brokenScan . some ( ( v ) => v . includes ( 'PARTIAL read' ) ) ) ;
2574+ }
2575+
2576+ // ⛔ THE BOUNDARY THIS CARD DELIBERATELY DID NOT CROSS. `declinedIn` still reads RAW
2577+ // bytes, so a `route:` quoted in a COMMENT in a declined spelling is still billed as an
2578+ // unread row — #10794, closed `not planned` because it is the loud direction with no
2579+ // puller. Pinned so the boundary is a recorded decision rather than an oversight, and so
2580+ // the card that eventually closes #10794 MOVES this pin instead of finding none.
2581+ const proseDeclined = parseLedgerSource ( [
2582+ '// The retired row read route: "GET /api/v1/gone" before #1234.' ,
2583+ 'export const L = [' ,
2584+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2585+ '];' ,
2586+ ] . join ( '\n' ) ) ;
2587+ check ( 'parseLedgerSource' , 'a declined spelling in PROSE is still billed as unread — #10794, deliberately unmoved' ,
2588+ 'declined' , 1 , proseDeclined . declined . length ) ;
2589+ check ( 'parseLedgerSource' , 'and its denominator still counts it — the type-member fix moved this none' ,
2590+ 'declared' , '1 row / 2 route' , `${ proseDeclined . rows . length } row / ${ proseDeclined . routesDeclared } route` ) ;
2591+
24222592 // ⛔ REPORTED, NEVER A VERDICT. A comment explaining a retired row by quoting its old path
24232593 // is legitimate prose; reddening CI over it is the false red the #9747 family declines.
24242594 const phantomCov = bridgeCoverageFrom ( [ { file : 'h-route-ledger.ts' , ...phantom } ] , [ '/api/v1/meta' ] ) ;
0 commit comments