Skip to content

Commit 71a90d2

Browse files
committed
fix(devx): published-README gate's green line reports what each half READ, not a ratio over an empty set (#9767)
With the ledger at `entries: []` -- the success state PR #9764 recorded -- the green line ended `0 of the findings are call sites`: a ratio over an EMPTY SET, printed by the one clause whose whole job is to say "clean" rather than "unmeasured". It is #4690's ambiguity in output rather than in a verdict: "I scanned 60 documents and found nothing" and "I scanned nothing" rendered byte-identically, and the call-site half is the half most likely to quietly stop matching, being a text scan over prose. Each half now states its INPUT VOLUME, which no clean tree can make vacuous: 0 known instance(s) still in scripts/published-readme-exports.baseline.json. Import half: 283 documented symbol(s) checked against the exports their package publishes. Call-site half: 8 documented `X.y(...)` call(s) checked, on 225 import-bound name(s). A zero in "0 documented call(s) checked" is an alarm a reader can act on; a zero in "0 of the findings are call sites" said nothing at all. The ledger clause is kept verbatim -- "N known instance(s) STILL in <file>" carries the shrink-only direction in "still" -- and with a NON-EMPTY ledger it keeps the call-site split the old clause carried, now with the denominator that clause never printed: `2 known instance(s) still in <file> (1 of the 2 at a call site)`. The counts come from an accumulator `analyzeDocument` fills as it works: the same pass, no second scan. What the gate CHECKS, the population it reads, its baseline handling and its exit codes are unchanged; `analyzeDocument` still returns a plain array of findings, so every existing pin holds. Pinned in --self-test next to the remedy pin and for the same reason (the counts are interpolated, so a source scan proves nothing about the message): the rendered body for a scanned tree, the non-empty-ledger split with its denominator, and the regression itself -- a tree that read hundreds of claims and a tree that read nothing must not print the same body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
1 parent 06f9848 commit 71a90d2

1 file changed

Lines changed: 111 additions & 5 deletions

File tree

‎scripts/check-published-readme-exports.mjs‎

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,15 @@ function typeSurface(absEntries) {
569569
* @param resolve `(specifier) => null` when the target is not a workspace
570570
* package (skip it), or
571571
* `{ declared, entryMissing, exports, hasMember }`.
572+
* @param measured optional accumulator, mutated in place, recording how much
573+
* each half READ rather than what it found. Findings alone
574+
* cannot answer "did it look?" -- zero is the same number for a
575+
* clean tree and for a scan that matched nothing (#4690), and
576+
* the call-site half is the half most likely to quietly stop
577+
* matching, being a text scan over prose. Passed in rather than
578+
* returned so the return type stays a plain array of findings.
572579
*/
573-
export function analyzeDocument(doc, resolveTarget) {
580+
export function analyzeDocument(doc, resolveTarget, measured = null) {
574581
const findings = [];
575582
const bound = new Map(); // local name -> { symbol, hasMember, specifier, imported }
576583
for (const imp of extractImports(doc.text)) {
@@ -600,6 +607,7 @@ export function analyzeDocument(doc, resolveTarget) {
600607
const names = [...imp.named];
601608
if (imp.defaultLocal) names.push({ imported: 'default', local: imp.defaultLocal });
602609
for (const { imported, local } of names) {
610+
if (measured) measured.symbolChecks++;
603611
const symbol = target.exports.get(imported);
604612
if (!symbol) {
605613
findings.push({
@@ -627,9 +635,11 @@ export function analyzeDocument(doc, resolveTarget) {
627635
}
628636
}
629637
}
638+
if (measured) measured.receivers += bound.size;
630639
for (const call of extractMemberCalls(doc.text, [...bound.keys()])) {
631640
const b = bound.get(call.object);
632641
if (!b) continue;
642+
if (measured) measured.callChecks++;
633643
if (b.hasMember(b.symbol, call.member)) continue;
634644
findings.push({
635645
id: `${doc.pkg}|${doc.file}|member|${b.specifier}|${b.imported}.${call.member}`,
@@ -679,6 +689,53 @@ function freshRemedy() {
679689
);
680690
}
681691

692+
/**
693+
* The GREEN line's body -- what a PASSING run tells the reader.
694+
*
695+
* Rendered by a function, next to `freshRemedy()` and for the same reason: the
696+
* counts are interpolated, so reading the SOURCE proves nothing about the
697+
* sentence an author gets. The self-test asserts on the returned text.
698+
*
699+
* ## Why it reports what was READ, not a ratio over what was found (#9767)
700+
*
701+
* This clause used to end `${memberChecks} of the findings are call sites`.
702+
* With the ledger populated that was informative -- it said how much of the
703+
* baseline the call-site half accounted for. With `entries: []` (the success
704+
* state, #9649) it is structurally always `0 of the findings are call sites`:
705+
* a ratio over an EMPTY SET, printed by the one clause whose whole job is to
706+
* say "clean" rather than "unmeasured". A zero there is the #4690 ambiguity in
707+
* output rather than in a verdict -- "I scanned 60 documents and found nothing"
708+
* and "I scanned nothing" render identically -- and the call-site half is the
709+
* half most likely to quietly stop matching, being a text scan over prose.
710+
*
711+
* So each half states its INPUT VOLUME, which no clean tree can make vacuous:
712+
* a zero in `283 documented symbol(s) checked` or `8 documented call(s)
713+
* checked` says the half read nothing, which is the alarm, whereas a zero in
714+
* `0 of the findings are call sites` said nothing at all. The counts come from
715+
* the accumulator `analyzeDocument` fills as it works -- the same pass, not a
716+
* second one; this gate's verdict, population and exit codes are unchanged.
717+
*
718+
* The ledger clause is kept as it was: `N known instance(s) STILL in <file>`
719+
* carries the shrink-only direction in the word "still", and the header above
720+
* it already names the population that was read, so zero reads as "none left
721+
* to repair". Non-empty, it also carries the call-site split the old clause
722+
* had -- WITH its denominator, which `N of the findings` never printed.
723+
*/
724+
function successSummary({ measured, baselineCount, memberChecks }) {
725+
const ledger =
726+
baselineCount === 0
727+
? `${baselineCount} known instance(s) still in ${BASELINE_REL}.`
728+
: `${baselineCount} known instance(s) still in ${BASELINE_REL} ` +
729+
`(${memberChecks} of the ${baselineCount} at a call site).`;
730+
return (
731+
` ${ledger}\n` +
732+
` Import half: ${measured.symbolChecks} documented symbol(s) checked against the exports ` +
733+
`their package publishes.\n` +
734+
` Call-site half: ${measured.callChecks} documented \`X.y(…)\` call(s) checked, on ` +
735+
`${measured.receivers} import-bound name(s).`
736+
);
737+
}
738+
682739
// ---------------------------------------------------------------------------
683740
// Run
684741
// ---------------------------------------------------------------------------
@@ -788,7 +845,8 @@ function run() {
788845
};
789846

790847
const findings = [];
791-
for (const doc of docs) findings.push(...analyzeDocument(doc, resolveTarget));
848+
const measured = { symbolChecks: 0, receivers: 0, callChecks: 0 };
849+
for (const doc of docs) findings.push(...analyzeDocument(doc, resolveTarget, measured));
792850

793851
// Reconcile against the shrink-only baseline, BOTH directions.
794852
const baseline = loadBaseline();
@@ -822,8 +880,7 @@ function run() {
822880
if (fresh.length === 0 && stale.length === 0) {
823881
console.log(`✓ check:published-readme-exports — ${header}`);
824882
console.log(
825-
` ${baseline.length} known instance(s) still in ${BASELINE_REL}; ` +
826-
`${memberChecks} of the findings are call sites.`,
883+
successSummary({ measured, baselineCount: baseline.length, memberChecks }),
827884
);
828885
return 0;
829886
}
@@ -1179,6 +1236,53 @@ function selfTest() {
11791236
['subpath'],
11801237
);
11811238

1239+
// The GREEN line (#9767). Its counts are interpolated too, so — exactly like
1240+
// the remedy below — reading the SOURCE is not evidence about the sentence a
1241+
// reader gets. Driven here instead, in the three states a passing run has.
1242+
const measuredReal = { symbolChecks: 283, receivers: 225, callChecks: 8 };
1243+
const scanned = successSummary({ measured: measuredReal, baselineCount: 0, memberChecks: 0 });
1244+
eq(
1245+
'successSummary — with the ledger EMPTY the line says what each half read',
1246+
scanned.split('\n'),
1247+
[
1248+
' 0 known instance(s) still in scripts/published-readme-exports.baseline.json.',
1249+
' Import half: 283 documented symbol(s) checked against the exports their package publishes.',
1250+
' Call-site half: 8 documented `X.y(…)` call(s) checked, on 225 import-bound name(s).',
1251+
],
1252+
);
1253+
eq(
1254+
'successSummary — a NON-EMPTY ledger keeps the call-site split, WITH its denominator',
1255+
successSummary({ measured: measuredReal, baselineCount: 2, memberChecks: 1 }).split('\n')[0],
1256+
' 2 known instance(s) still in scripts/published-readme-exports.baseline.json (1 of the 2 at a call site).',
1257+
);
1258+
1259+
// THE regression this card exists for. A tree where both halves read hundreds
1260+
// of claims and a tree where they read NOTHING must not print the same green
1261+
// body — that is #4690 moved out of the verdict and into the output. The
1262+
// clause these pins replaced ("N of the findings are call sites") was
1263+
// byte-identical in both, because with an empty ledger it is a ratio over an
1264+
// empty set.
1265+
const unscanned = successSummary({
1266+
measured: { symbolChecks: 0, receivers: 0, callChecks: 0 },
1267+
baselineCount: 0,
1268+
memberChecks: 0,
1269+
});
1270+
if (scanned === unscanned) {
1271+
failures.push(
1272+
'the GREEN body renders IDENTICALLY for a tree that was scanned and one that was not.\n' +
1273+
' A reader cannot tell "60 documents read, nothing wrong" from "nothing was read",\n' +
1274+
' which is exactly the ambiguity this gate is emphatic about everywhere else.',
1275+
);
1276+
}
1277+
if (/of the findings/.test(scanned)) {
1278+
failures.push(
1279+
'the GREEN body must state each half\'s INPUT VOLUME, never a ratio over the findings.\n' +
1280+
' A zero in "0 documented call(s) checked" is an alarm a reader can act on; a zero\n' +
1281+
' in "0 of the findings are call sites" is a ratio over an empty set and says\n' +
1282+
' nothing at all.',
1283+
);
1284+
}
1285+
11821286
// The authority convention (#8435): the baseline path must never be offered
11831287
// as a co-equal author remedy.
11841288
const remedy = freshRemedy();
@@ -1204,7 +1308,9 @@ function selfTest() {
12041308
console.log(
12051309
'✓ check:published-readme-exports --self-test — extraction, scoping, resolution and both\n' +
12061310
' analysis directions pinned (fabricated import reported, fabricated static reported,\n' +
1207-
' honest README clean, and every measured prose/bash/diff false positive silent).',
1311+
' honest README clean, and every measured prose/bash/diff false positive silent), plus\n' +
1312+
' the author-facing text: the remedy refuses the baseline, and the green line reports\n' +
1313+
' what each half READ, so a scanned tree and an unread one cannot print the same body.',
12081314
);
12091315
}
12101316

0 commit comments

Comments
 (0)