@@ -1477,6 +1477,15 @@ function declinedIn(s) {
14771477 * all seven ledgers. `outsideCode` is what keeps the fix from trading a phantom row for a
14781478 * new silence — it NAMES every lead the mask dropped, and carries no verdict.
14791479 *
1480+ * …AND THROUGH BOTH OF #10500'S DISCRIMINATORS, NOT ONE (#10793). `codeOnly` was only half
1481+ * of the answer: type declarations are CODE, so a literal-union `route: 'GET /a' | 'GET /b'`
1482+ * TYPE member still minted a row — the identical silent shape arriving through the other
1483+ * discriminator, with `rows` and `routesDeclared` moving together again so no verdict fired.
1484+ * The recognizer and the first term of its denominator now skip `typeDeclRegions` the way
1485+ * `declarationsIn` has since #10500, so the two scans answer "is this in code position?" the
1486+ * same way instead of drifting while both look right. Delta 0 on all seven ledgers again:
1487+ * none of them declares a quoted `route:` inside a type declaration.
1488+ *
14801489 * @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}>} }
14811490 */
14821491function parseLedgerSource ( text ) {
@@ -1509,13 +1518,39 @@ function parseLedgerSource(text) {
15091518 // one tree where the move is provably free: no ledger quotes a `route:` in prose today,
15101519 // and `rows`, `routesDeclared`, `clientsDeclared` and the 176 UNREACHABLE rows are
15111520 // byte-identical across the change (all seven ledgers, delta 0).
1512- // ⛔ ONE of #10500's two discriminators, not both. `typeDeclRegions` is still NOT applied
1513- // here, so a literal-union `route: 'GET /a' | 'GET /b'` TYPE member is still read as a row
1514- // — the same silent shape, through the other discriminator. Measured on this tree: 0 such
1515- // members across all seven ledgers (every one declares `route: string;`). Applying it is a
1516- // SECOND population move and wants its own before/after, which is why it is #10793 and not
1517- // a rider here.
1521+ // BOTH OF #10500'S DISCRIMINATORS NOW, NOT ONE (#10793). `codeOnly` blanks comments and
1522+ // string CONTENTS; it does not blank TYPE DECLARATIONS, and has no reason to — they are
1523+ // code. So a literal-union `route: 'GET /a' | 'GET /b'` TYPE member was still read as a
1524+ // ROW: the identical silent shape the paragraph above describes, arriving through the
1525+ // other discriminator. It stayed silent for the same two reasons — `rows` and
1526+ // `routesDeclared` moved together, so the partial-read guard saw no gap, and `outsideCode`
1527+ // could not see it either, because the lead genuinely IS in code position. Measured on the
1528+ // tree this landed on, one file declaring ONE row:
1529+ //
1530+ // export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }
1531+ // export const L = [
1532+ // { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' },
1533+ // ];
1534+ // ⇒ rows 2 · routesDeclared 2 · clientsDeclared 1 · declined 0 · outsideCode 0
1535+ //
1536+ // ⛔ A SKIPPED TYPE MEMBER IS SILENT ON PURPOSE, and that is NOT the silence `outsideCode`
1537+ // exists to break. A prose-quoted lead is a would-be row sitting where the mask says code
1538+ // is not, so naming it tells the reader something they can act on. A type member is a
1539+ // correct declaration of a TYPE: the `route: string;` member all seven entry interfaces
1540+ // declare produces nothing today under the same rule, `declarationsIn` has excluded the
1541+ // literal-union spelling since #10500, and `--self-test` pins BOTH directions — deleting
1542+ // the interface changes no count at all. This makes the recognizer agree with the position
1543+ // the rest of the file already held rather than inventing a second one.
1544+ //
1545+ // The population move was priced on the tree where it is provably free: 0 quoted `route:`
1546+ // or `client:` leads inside a type declaration across all seven ledgers, and
1547+ // `259 of 259` / `221 of 221` / 176 UNREACHABLE are byte-identical across the change.
15181548 const code = codeOnly ( text ) ;
1549+ // The SECOND discriminator, taken over the already-blanked source — the same call
1550+ // `declarationsIn` makes, reusing the same region list rather than growing a second idea
1551+ // of what a type member is.
1552+ const typeDecls = typeDeclRegions ( code ) ;
1553+ const inTypeDecl = ( i ) => typeDecls . some ( ( [ a , b ] ) => i >= a && i <= b ) ;
15191554 // The value as WRITTEN. `codeOnly` blanks string CONTENTS, so a capture group taken off
15201555 // `code` is a run of spaces of the right length and never the route. Blanking preserves
15211556 // LENGTH, so cutting the raw text by the masked match's offsets is exact for any content
@@ -1525,6 +1560,9 @@ function parseLedgerSource(text) {
15251560 const routeRe = / r o u t e \s * : \s * ' ( [ ^ ' ] + ) ' / g;
15261561 let m ;
15271562 while ( ( m = routeRe . exec ( code ) ) !== null ) {
1563+ // …and never a TYPE member (#10793). A literal-union member opens with the very quote
1564+ // this regex reads, which is exactly why `typeDeclRegions` and not a spelling test.
1565+ if ( inTypeDecl ( m . index ) ) continue ;
15281566 const rest = code . slice ( m . index , routeRe . lastIndex + 1200 ) ;
15291567 const nextRoute = rest . slice ( 1 ) . search ( / r o u t e \s * : \s * ' / ) ;
15301568 const window = nextRoute === - 1 ? rest : rest . slice ( 0 , nextRoute + 1 ) ;
@@ -1590,7 +1628,13 @@ function parseLedgerSource(text) {
15901628 // denominator that counted prose-quoted leads while the numerator no longer read them
15911629 // would turn this migration into a permanently red ratio on the very files it fixed.
15921630 // Both terms move together, which is the invariant the block above insists on.
1593- const routesDeclared = [ ...code . matchAll ( / r o u t e \s * : \s * ' / g) ] . length + declined . filter ( ( d ) => d . key === 'route' ) . length ;
1631+ //
1632+ // …and it skips TYPE MEMBERS on the same list the loop above does (#10793). Both terms
1633+ // move together or the partition `rows + declined === routesDeclared` breaks: a member
1634+ // counted here but skipped there would read as a row this parse declined to read, and
1635+ // fire a PARTIAL-read verdict on an accurate ledger.
1636+ const routesDeclared = [ ...code . matchAll ( / r o u t e \s * : \s * ' / g) ] . filter ( ( d ) => ! inTypeDecl ( d . index ) ) . length
1637+ + declined . filter ( ( d ) => d . key === 'route' ) . length ;
15941638 const clientsDeclared = rows . filter ( ( r ) => r . client ) . length + declined . filter ( ( d ) => d . key === 'client' ) . length ;
15951639 declined . sort ( ( a , b ) => a . line - b . line || a . key . localeCompare ( b . key ) ) ;
15961640 // THE REPORTING HALF (#10683). The mask closes the phantom-row hole by making a
@@ -2278,6 +2322,103 @@ function selfTest() {
22782322 check ( 'parseLedgerSource' , 'a `route:` inside a string PAYLOAD is not a row' , 'row count' , 1 , payload . rows . length ) ;
22792323 check ( 'parseLedgerSource' , 'and it is named, not silently dropped' , 'outsideCode' , 1 , payload . outsideCode . length ) ;
22802324
2325+ // ---- A LITERAL-UNION `route:` TYPE MEMBER IS NOT A ROW (#10793) ---------------
2326+ // The OTHER half of "is this in code position", and the half `codeOnly` cannot answer:
2327+ // type declarations ARE code, so a member spelled `route: 'GET /a' | 'GET /b'` opens with
2328+ // the very quote the recognizer reads and used to become a ROW. Silent in the same two
2329+ // ways as the comment case above — `rows` and `routesDeclared` moved together so the
2330+ // partial-read guard saw no gap, and `outsideCode` saw nothing either because the lead
2331+ // really IS in code position. `orphanSource` above pins this exact spelling on the
2332+ // `client:` key, written there because a literal-union member opens with that quote; this
2333+ // is the `route:` twin, which had nothing at all keeping it out of the row loop.
2334+ //
2335+ // ⚠️ PINNED IN BOTH DIRECTIONS, like the mask cases: a fix that reached the type member by
2336+ // swallowing the table AFTER it — a brace match that ran away — would pass a test asserting
2337+ // only that the phantom is gone, while silently dropping all 259 live rows.
2338+ const typeUnionSource = [
2339+ "export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }" ,
2340+ 'export const L = [' ,
2341+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2342+ '];' ,
2343+ ] . join ( '\n' ) ;
2344+ const typeUnion = parseLedgerSource ( typeUnionSource ) ;
2345+ check ( 'parseLedgerSource' , 'a literal-union `route:` TYPE member is not a row' , 'row count' ,
2346+ 1 , typeUnion . rows . length ) ;
2347+ check ( 'parseLedgerSource' , 'and the row in CODE still is — carrying its VALUE, not the member\'s' , 'row' ,
2348+ 'GET /api/v1/meta → meta.getTypes' , `${ typeUnion . rows [ 0 ] ?. route } → ${ typeUnion . rows [ 0 ] ?. client } ` ) ;
2349+ check ( 'parseLedgerSource' , 'the denominator drops it too, so no phantom gap opens' , 'declared' ,
2350+ '1 route / 1 client' , `${ typeUnion . routesDeclared } route / ${ typeUnion . clientsDeclared } client` ) ;
2351+ check ( 'parseLedgerSource' , 'and nothing is billed as declined for it' , 'declined' ,
2352+ 0 , typeUnion . declined . length ) ;
2353+ // THE REJECT SIDE, ASSERTED POSITIVELY — `orphanSource`'s discipline on the other key. A
2354+ // type member must reach NO report: not `declined` (it is not an unread row) and not
2355+ // `outsideCode` (it is not prose). "Produces nothing" is correct here and only here,
2356+ // because it is a declaration of a TYPE and never a table row.
2357+ check ( 'parseLedgerSource' , 'a type member reaches no report at all — it is not an unread row' , 'line 1 entries' ,
2358+ false , typeUnion . declined . some ( ( d ) => d . line === 1 ) || typeUnion . outsideCode . some ( ( d ) => d . line === 1 ) ) ;
2359+ const typeUnionNoInterface = parseLedgerSource ( typeUnionSource . split ( '\n' ) . slice ( 1 ) . join ( '\n' ) ) ;
2360+ check ( 'parseLedgerSource' , 'and deleting the interface changes NOTHING — it contributed no count' ,
2361+ 'declared' , '1 row / 1 route / 1 client / 0 declined' ,
2362+ `${ typeUnionNoInterface . rows . length } row / ${ typeUnionNoInterface . routesDeclared } route / ${ typeUnionNoInterface . clientsDeclared } client / ${ typeUnionNoInterface . declined . length } declined` ) ;
2363+
2364+ // The expensive shape, as on the comment side: a member that unions a `client:` too used to
2365+ // mint a FULLY CLIENT-BOUND phantom, which then joined the UNREACHABLE population — no
2366+ // registrar tail can match a route nobody mounts — and inflated the 176 with no verdict.
2367+ const typeUnionClient = parseLedgerSource ( [
2368+ "export interface Entry { route: 'GET /api/v1/gone'; client: 'meta.getGone' }" ,
2369+ 'export const L = [' ,
2370+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2371+ '];' ,
2372+ ] . join ( '\n' ) ) ;
2373+ check ( 'parseLedgerSource' , 'a member unioning a `client:` too mints no client-bound phantom' , 'client rows' ,
2374+ 1 , typeUnionClient . rows . filter ( ( r ) => r . client ) . length ) ;
2375+ check ( 'parseLedgerSource' , 'and the surviving row keeps its OWN binding, never the member\'s' , 'client' ,
2376+ 'meta.getTypes' , typeUnionClient . rows [ 0 ] ?. client ) ;
2377+
2378+ // `type X = { … }` is the other spelling `typeDeclRegions` recognises, and the recognizer
2379+ // reads the same region list rather than a second idea of what a type member is.
2380+ const typeAlias = parseLedgerSource ( [
2381+ "export type Entry = { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string };" ,
2382+ 'export const L = [' ,
2383+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2384+ '];' ,
2385+ ] . join ( '\n' ) ) ;
2386+ check ( 'parseLedgerSource' , 'a `type X = { … }` member is skipped on the same list' , 'row count' ,
2387+ 1 , typeAlias . rows . length ) ;
2388+ check ( 'parseLedgerSource' , 'and its row still reads' , 'row' , 'GET /api/v1/meta → meta.getTypes' ,
2389+ `${ typeAlias . rows [ 0 ] ?. route } → ${ typeAlias . rows [ 0 ] ?. client } ` ) ;
2390+
2391+ // THE RUNAWAY-BRACE DIRECTION, from the other side: the region ends where the declaration's
2392+ // brace does, so a table BEFORE a type declaration is read whole and the member after it is
2393+ // still skipped. The failure this pins is a skip that starts at the member and never ends.
2394+ const typeUnionTrailing = parseLedgerSource ( [
2395+ 'export const L = [' ,
2396+ " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk', client: 'meta.getTypes' }," ,
2397+ " { route: 'GET /api/v1/docs', family: 'metadata', disposition: 'server-only' }," ,
2398+ '];' ,
2399+ "export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }" ,
2400+ ] . join ( '\n' ) ) ;
2401+ check ( 'parseLedgerSource' , 'a table BEFORE a type declaration is read whole' , 'row count' ,
2402+ 2 , typeUnionTrailing . rows . length ) ;
2403+ check ( 'parseLedgerSource' , 'with both values and the one binding intact' , 'rows' ,
2404+ 'GET /api/v1/meta → meta.getTypes | GET /api/v1/docs → null' ,
2405+ typeUnionTrailing . rows . map ( ( r ) => `${ r . route } → ${ r . client } ` ) . join ( ' | ' ) ) ;
2406+ check ( 'parseLedgerSource' , 'and the trailing member still contributes no count' , 'declared' ,
2407+ '2 route / 1 client / 0 declined' ,
2408+ `${ typeUnionTrailing . routesDeclared } route / ${ typeUnionTrailing . clientsDeclared } client / ${ typeUnionTrailing . declined . length } declined` ) ;
2409+
2410+ // The verdict a reader actually sees carries none of it — a ledger whose entry interface
2411+ // spells its `route:` as a union is an ACCURATE ledger, and reddening CI over it would be
2412+ // the same false red the type-member exclusion exists to prevent.
2413+ const typeUnionCov = bridgeCoverageFrom ( [ { file : 'i-route-ledger.ts' , ...typeUnion } ] , [ '/api/v1/meta' ] ) ;
2414+ check ( 'bridgeCoverageFrom' , 'a type-member lead carries NO broken-scan verdict' , 'brokenScan' ,
2415+ 0 , typeUnionCov . brokenScan . length ) ;
2416+ check ( 'bridgeCoverageFrom' , 'and the ratios it must not move are whole' , 'read' ,
2417+ '1 of 1 route / 1 of 1 client' ,
2418+ `${ typeUnionCov . rowsParsed } of ${ typeUnionCov . routesDeclared } route / ${ typeUnionCov . clientRows } of ${ typeUnionCov . clientsDeclared } client` ) ;
2419+ check ( 'bridgeCoverageFrom' , 'and it is not billed as a prose lead either' , 'leadsOutsideCode' ,
2420+ 0 , typeUnionCov . leadsOutsideCode ) ;
2421+
22812422 // ⛔ REPORTED, NEVER A VERDICT. A comment explaining a retired row by quoting its old path
22822423 // is legitimate prose; reddening CI over it is the false red the #9747 family declines.
22832424 const phantomCov = bridgeCoverageFrom ( [ { file : 'h-route-ledger.ts' , ...phantom } ] , [ '/api/v1/meta' ] ) ;
0 commit comments