@@ -1608,7 +1608,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
16081608 'the local path composed: an unread diff is not a narrow diff' : 7 ,
16091609 'the surfaces, imported rather than restated' : 11 ,
16101610 'T1 — a new key on a Zod object schema' : 14 ,
1611- 'T2 — a new member of a closed set' : 63 ,
1611+ 'T2 — a new member of a closed set' : 64 ,
16121612 '#16822 — the two accidental variables, and the evidence each one needs' : 15 ,
16131613 '#16943 — the net member/key delta: a replaced line is not a net addition' : 23 ,
16141614 '#17618 — a PARAMETER is not a key, and a closed set RE-SPELLED around fewer values is not a new one' : 24 ,
@@ -3064,8 +3064,10 @@ export function refusalOnlyReaders(source, name) {
30643064 * T2's verdict on one MEMBER line — `fires`, `silent` or `narrowing` — with the
30653065 * construct reading it rests on. `readSource` is a thunk answering the file's
30663066 * head blob (or `null`); it is called only for the one reading that needs it.
3067+ * `readers` caches {@link refusalOnlyReaders} per binding for one file, so a
3068+ * set gaining many members reads its file once rather than once per member.
30673069 */
3068- export function t2MemberVerdict ( side , index , readSource = null ) {
3070+ export function t2MemberVerdict ( side , index , readSource = null , readers = null ) {
30693071 const reading = enclosingConstruct ( side , index ) ;
30703072 const verdict = reading . construct === null ? 'fires' : ( T2_CONSTRUCT_VERDICTS [ reading . construct ] ?? 'fires' ) ;
30713073 if ( verdict !== 'fires' ) return { ...reading , verdict } ;
@@ -3075,10 +3077,13 @@ export function t2MemberVerdict(side, index, readSource = null) {
30753077 ! reading . exported &&
30763078 typeof readSource === 'function'
30773079 ) {
3078- const source = readSource ( ) ;
3079- if ( typeof source === 'string' && refusalOnlyReaders ( source , reading . binding ) . narrowing ) {
3080- return { ...reading , verdict : 'narrowing' } ;
3080+ let narrows = readers instanceof Map ? readers . get ( reading . binding ) : undefined ;
3081+ if ( narrows === undefined ) {
3082+ const source = readSource ( ) ;
3083+ narrows = typeof source === 'string' && refusalOnlyReaders ( source , reading . binding ) . narrowing ;
3084+ if ( readers instanceof Map ) readers . set ( reading . binding , narrows ) ;
30813085 }
3086+ if ( narrows ) return { ...reading , verdict : 'narrowing' } ;
30823087 }
30833088 return { ...reading , verdict : 'fires' } ;
30843089}
@@ -4484,6 +4489,7 @@ export function tellsInFile(
44844489 if ( headSource === undefined ) headSource = typeof readSource === 'function' ? readSource ( file ) : null ;
44854490 return headSource ;
44864491 } ;
4492+ const readersByBinding = new Map ( ) ;
44874493 // #17300 — is THIS file the ADR-0087 ledger? A licence clears a row in the
44884494 // ledger table and nowhere else: the same string added to any other file on
44894495 // any other surface still tells, with its own file:line.
@@ -4568,7 +4574,7 @@ export function tellsInFile(
45684574 // #17955's reason: a line that is no member must neither FIRE nor SPEND a
45694575 // unit a real member in the same block is owed. `narrowing` is decided at
45704576 // the row, below — a member it is, of a set that loses a value.
4571- const member = kind === 'T2' ? t2MemberVerdict ( newFile , newAt . get ( i ) , headSourceOnce ) : null ;
4577+ const member = kind === 'T2' ? t2MemberVerdict ( newFile , newAt . get ( i ) , headSourceOnce , readersByBinding ) : null ;
45724578 if ( member ?. verdict === 'silent' ) continue ;
45734579 // #17955 — a `retiredKey()` tombstone DECLARES a key unwritable. It is read
45744580 // BEFORE the budget, and that ordering is the whole repair rather than a
@@ -5614,6 +5620,12 @@ export function selfTest() {
56145620 return v . state === 'clean' && exitForRefusal ( v ) === EXIT_OK && v . narrowing . length === 1 && v . narrowing [ 0 ] ?. binding === 'SINGLE_SERIES_TYPES' && narrowingLines ( v . narrowing ) . some ( ( l ) => l . includes ( `${ CONSTRUCT_FILE } :4` ) ) ;
56155621 } ) ( ) ) ;
56165622 t ( '⛔ …while an empty list prints NOTHING — a heading with no rows would read as a finding' , narrowingLines ( [ ] ) . length === 0 && narrowingLines ( null ) . length === 0 ) ;
5623+ t ( 'a set gaining THREE members reads its head blob ONCE, and all three read as narrowing' , ( ( ) => {
5624+ let reads = 0 ;
5625+ const narrowing = [ ] ;
5626+ const rows = tellsInFile ( { filename : CONSTRUCT_FILE , status : 'modified' , patch : "@@ -1,2 +1,5 @@\n const SINGLE_SERIES_TYPES = [\n+ 'pie',\n+ 'donut',\n+ 'sankey',\n ] as const satisfies readonly ChartType[];" } , { readSource : ( ) => { reads += 1 ; return DENY_SET_SOURCE ; } , narrowing } ) ;
5627+ return reads === 1 && rows . length === 0 && narrowing . length === 3 ;
5628+ } ) ( ) ) ;
56175629
56185630 // -- #16822: the accidental variables ------------------------------------
56195631 //
0 commit comments