You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(pm): a binary change to a tell surface is UNREAD, not narrow
`splitUnifiedDiff` wrote `additions: addedLines(patch).length` for every row,
and `addedLines(null)` is empty — so a BINARY change stamped a count nobody
took. `unreadFiles` skips a row that added nothing, so a binary edit to
`packages/spec/api-surface/*.json` arriving through the local path reported
`state: 'clean'`: the gate's own contract, declared and not enforced, inside
the gate.
The three states are now told apart by what the diff SAYS — a hunk gives the
count, a `Binary files`/`GIT binary patch` marker gives `null` (UNKNOWN), and
neither gives a real `0` (a mode-only change or a pure rename adds no line).
`addedNothing` interprets both input paths in one place, so a MISSING count can
never become a zero; GitHub's own `additions: 0` on a binary row is kept,
because that reading was taken by something that can see the blob.
The self-test pinned the two halves separately and never composed them, which
is why it stayed green. Seven composed cases now drive splitUnifiedDiff into
wideningRefusal.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX
t('…and null is what `unreadFiles` counts as a gap when it is on a surface',unreadFiles([{filename: 'packages/spec/api-surface/kernel.json',patch: null}]).length===1);
784
859
t('⛔ a file OFF every surface with no patch is not a gap — it decides nothing here',unreadFiles([{filename: 'README.md',patch: null}]).length===0);
785
860
t('the local path and the API path produce the same verdict on the same bytes',JSON.stringify(wideningTells(splitUnifiedDiff(twoFiles)).map((r)=>r.tell))==='["T1"]');
861
+
// `additions` is a COUNT THAT WAS TAKEN. The three states below are told
862
+
// apart by what the diff SAYS, and conflating them is what let a binary
863
+
// change to a tell surface read as clean.
864
+
t('⛔ a binary row carries additions `null` — UNKNOWN, never a fabricated 0',splitUnifiedDiff('diff --git a/i.png b/i.png\nBinary files a/i.png and b/i.png differ')[0]?.additions===null);
865
+
t('…and the `GIT binary patch` spelling reads as UNKNOWN too',splitUnifiedDiff('diff --git a/i.png b/i.png\nGIT binary patch\nliteral 0\nHcmV?d00001')[0]?.additions===null);
866
+
t('a MODE-ONLY change carries a real 0 — no hunk AND no binary marker is git saying nothing was added',splitUnifiedDiff('diff --git a/x b/x\nold mode 100644\nnew mode 100755')[0]?.additions===0);
867
+
t('a pure rename carries a real 0 for the same reason',splitUnifiedDiff('diff --git a/x b/y\nsimilarity index 100%\nrename from x\nrename to y')[0]?.additions===0);
868
+
869
+
// -- the local path composed, end to end ----------------------------------
870
+
//
871
+
// The two halves below were each pinned separately before, and the defect
872
+
// lived exactly between them: `patch: null` was asserted on one fixture and
873
+
// "null is a gap" on a DIFFERENT, hand-built row that carried no `additions`
874
+
// at all — so nothing drove a real binary row through `unreadFiles`. These
875
+
// cases compose the actual functions, in the order a caller calls them.
876
+
battery('the local path composed: an unread diff is not a narrow diff');
t('⭐ a BINARY change to a tell surface reads INCOMPLETE, never clean',composed(BINARY_ON_SURFACE).state==='incomplete');
880
+
t('…and maps to exit 2, the one exit that must never be mistaken for 0',exitForRefusal(composed(BINARY_ON_SURFACE))===EXIT_INCOMPLETE);
881
+
t('…naming the file whose content could not be read',composed(BINARY_ON_SURFACE).gaps[0]==='packages/spec/api-surface/kernel.json');
882
+
t('⛔ but a binary change OFF every surface is clean — it decides nothing here',composed('diff --git a/docs/logo.png b/docs/logo.png\nBinary files a/docs/logo.png and b/docs/logo.png differ').state==='clean');
883
+
t('a MODE-ONLY change to a tell surface is clean — it really did add nothing',composed('diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json\nold mode 100644\nnew mode 100755').state==='clean');
884
+
t('addedNothing: a MISSING count is never a zero',addedNothing({filename: 'x',additions: null})===false&&addedNothing({filename: 'x'})===false);
885
+
t('…while a count that WAS taken is one',addedNothing({filename: 'x',additions: 0})===true&&addedNothing({filename: 'x',additions: 3})===false);
786
886
787
887
// -- the surfaces ---------------------------------------------------------
788
888
battery('the surfaces, imported rather than restated');
@@ -964,7 +1064,8 @@ export function selfTest() {
964
1064
`✓ check-widening-tells self-test: ${cases.length} cases pass (the patch reader with its `+
965
1065
'line-number directions, the unified-diff splitter, the three imported/declared surfaces, the '+
966
1066
"four tells, #16448's four positive controls each with its file:line, its negative controls — "+
967
-
'the same diffs with `yes`, and a removal-only diff with `no` — and the exit register).',
1067
+
'the same diffs with `yes`, and a removal-only diff with `no` — the local path composed end '+
1068
+
'to end so a binary change to a tell surface cannot read as clean — and the exit register).',
0 commit comments