Skip to content

Commit 22eafdc

Browse files
committed
wip(pm): read a set's refusal-only readers once per file, pinned by a counting case
Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
1 parent b5cf545 commit 22eafdc

1 file changed

Lines changed: 18 additions & 6 deletions

File tree

‎scripts/pm/check-widening-tells.mjs‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)