Skip to content

Commit 793567f

Browse files
os-steveclaude
andauthored
fix(tooling): error-status gate names the real cause when a code leaves the ungraded set (#9563) (#9577)
check-error-status-conformance.mjs's nowPinnedMessage() hard-coded a single cause ("a producer now declares its status") for ANY baselined-unpinned code that left result.unpinned -- but that subtraction has two distinct causes, split at the documented.has(code) juncture reconcile() already branches on: - a producer appeared (runtime.size > 0), or - the code's doc entry was removed, so documented.has(code) went false, with no producer either. Split the derivation into a pure nowPinned() function returning a reason per code, and two message functions (nowPinnedProducerMessage / nowPinnedDocRemovedMessage) so each cause gets its own accurate sentence. Both remedies stay `--update`; no baseline, catalog, or grading-model change. Added self-test cases 21/21b (producer branch) and 22/22b (doc-removed branch), following the file's own ENTRY_HEADING_SHAPES precedent that an unexercised branch is the defect. CASES: 36 -> 40. Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5d2593 commit 793567f

1 file changed

Lines changed: 85 additions & 8 deletions

File tree

scripts/check-error-status-conformance.mjs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,34 @@ export function reconcile({ vocabulary, emitted, claimed, covered, documented })
606606
return { emittedNotDocumented, documentedNotReachable, unpinned: unpinned.sort(), reconciledCodes, reconciledPairs };
607607
}
608608

609+
/**
610+
* The two causes a BASELINED-as-unpinned code can leave the `unpinned` census
611+
* on a later run — split at the same `documented.has(code)` juncture
612+
* `reconcile()` branches on, rather than assuming the first cause for a
613+
* subtraction that has two:
614+
*
615+
* 'producer' a producer now declares a status for it (`runtime.size > 0`
616+
* in `reconcile()`), while the code is still documented.
617+
* 'doc-removed' its doc entry was removed — nothing documents the code any
618+
* more (`!documented.has(code)`), and no producer appeared
619+
* either, or `reconcile()` would already have counted it as a
620+
* reconciled code, not an unpinned one.
621+
*
622+
* A code cannot leave `unpinned` for any OTHER reason: `reconcile()` only
623+
* ever adds a code to `unpinned` when `runtime.size === 0 && documented.has
624+
* (code)`, so losing that membership means one of those two flipped.
625+
*
626+
* @param {string[]} baselined codes recorded unpinned in the baseline file
627+
* @param {{ unpinned: string[], vocabulary: string[], documented: Set<string> }} input
628+
* @returns {{ code: string, reason: 'producer' | 'doc-removed' }[]}
629+
*/
630+
export function nowPinned({ baselined, unpinned, vocabulary, documented }) {
631+
return baselined
632+
.filter((c) => !unpinned.includes(c) && vocabulary.includes(c))
633+
.sort()
634+
.map((code) => ({ code, reason: documented.has(code) ? 'producer' : 'doc-removed' }));
635+
}
636+
609637
// ───────────────────────────────────────────────────────────────────────────
610638
// Messages — named and pure, so the self-test can assert the exact text
611639
// ───────────────────────────────────────────────────────────────────────────
@@ -649,10 +677,17 @@ export function unreadableHeadingMessage(u) {
649677
);
650678
}
651679

652-
export function nowPinnedMessage(code) {
680+
export function nowPinnedProducerMessage(code) {
653681
return `${code}: baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update.`;
654682
}
655683

684+
export function nowPinnedDocRemovedMessage(code) {
685+
return (
686+
`${code}: baselined as unpinned, but its doc entry was removed, so nothing claims a status for it any more `
687+
+ '— ratchet the baseline down with --update.'
688+
);
689+
}
690+
656691
// ───────────────────────────────────────────────────────────────────────────
657692
// Self-test
658693
// ───────────────────────────────────────────────────────────────────────────
@@ -725,6 +760,7 @@ function runFixture({ files, handling, catalog, members }) {
725760
ungraded: ungradedEntries(doc),
726761
unresolved: derived.unresolved,
727762
emitted: derived.emitted,
763+
documented: doc.documented,
728764
};
729765
}
730766

@@ -841,8 +877,9 @@ function selfTest() {
841877
// 11 — the ratchet-authority convention holds on the weakening remedy only.
842878
check('11 the baseline-expanding remedy is marked maintainer-only',
843879
RATCHET_EXPANSION_OFFER.test(newUnpinnedMessage('X')) && newUnpinnedMessage('X').includes(RATCHET_AUTHORITY_MARKER));
844-
check('11b the ratchet-DOWN remedy stays the author\'s own',
845-
!nowPinnedMessage('X').includes(RATCHET_AUTHORITY_MARKER));
880+
check('11b both ratchet-DOWN remedies stay the author\'s own',
881+
!nowPinnedProducerMessage('X').includes(RATCHET_AUTHORITY_MARKER)
882+
&& !nowPinnedDocRemovedMessage('X').includes(RATCHET_AUTHORITY_MARKER));
846883

847884
// 12 — the vocabulary bound: a ledger code is derived but not reconciled.
848885
const ledger = runFixture({
@@ -992,7 +1029,42 @@ function selfTest() {
9921029
check('20c an entry that DOES publish a graded status is not in the census',
9931030
ungradedEntries({ entries: [{ code: 'TIMEOUT', where: 'x:1' }], claimed: new Map([['TIMEOUT', new Map([[504, []]])]]), covered: new Map() }).length === 0);
9941031

995-
const CASES = 36;
1032+
// 21 — nowPinned, the PRODUCER branch: a baselined code that GAINS a
1033+
// producer while remaining documented is named a producer, never a
1034+
// doc removal.
1035+
const producerCase = runFixture({
1036+
files: { 'a/e.ts': "export class E extends Error {\n readonly code = 'TRANSACTION_FAILED';\n readonly status = 500;\n}" },
1037+
handling: '#### `TRANSACTION_FAILED`\n**HTTP Status:** 500 \n', catalog: '', members: ['TRANSACTION_FAILED'],
1038+
});
1039+
const producerFindings = nowPinned({
1040+
baselined: ['TRANSACTION_FAILED'], unpinned: producerCase.unpinned,
1041+
vocabulary: producerCase.vocabulary, documented: producerCase.documented,
1042+
});
1043+
check('21 nowPinned names the producer branch when the code stays documented',
1044+
producerFindings.length === 1 && producerFindings[0].code === 'TRANSACTION_FAILED' && producerFindings[0].reason === 'producer',
1045+
JSON.stringify(producerFindings));
1046+
check('21b the producer message names a producer, not a doc removal',
1047+
nowPinnedProducerMessage('TRANSACTION_FAILED').includes('a producer now declares its status')
1048+
&& !nowPinnedProducerMessage('TRANSACTION_FAILED').includes('doc entry was removed'));
1049+
1050+
// 22 — nowPinned, the DOC-REMOVED branch: the #9266/#9563 counterfactual,
1051+
// reduced to a fixture. No producer, and the catalog entry is gone —
1052+
// the real cause the old single-cause message misdiagnosed as "a
1053+
// producer now declares its status" when the `## Batch Operation
1054+
// Errors` entries were deleted on `main`.
1055+
const docRemovedCase = runFixture({ files: {}, handling: '', catalog: '', members: ['TRANSACTION_FAILED'] });
1056+
const docRemovedFindings = nowPinned({
1057+
baselined: ['TRANSACTION_FAILED'], unpinned: docRemovedCase.unpinned,
1058+
vocabulary: docRemovedCase.vocabulary, documented: docRemovedCase.documented,
1059+
});
1060+
check('22 nowPinned names the doc-removed branch when nothing documents the code any more',
1061+
docRemovedFindings.length === 1 && docRemovedFindings[0].code === 'TRANSACTION_FAILED' && docRemovedFindings[0].reason === 'doc-removed',
1062+
JSON.stringify(docRemovedFindings));
1063+
check('22b the doc-removed message names a removed doc entry, not a producer',
1064+
nowPinnedDocRemovedMessage('TRANSACTION_FAILED').includes('doc entry was removed')
1065+
&& !nowPinnedDocRemovedMessage('TRANSACTION_FAILED').includes('a producer now declares'));
1066+
1067+
const CASES = 40;
9961068
if (failures.length) {
9971069
for (const f of failures) console.error(` x self-test: ${f}`);
9981070
console.error(`\n✗ check-error-status-conformance --self-test: ${failures.length}/${CASES} case(s) failed.\n`);
@@ -1004,8 +1076,9 @@ function selfTest() {
10041076
+ '(positive control), both directions fire independently, section headings absolve but never demand, '
10051077
+ 'narrated envelopes in comments are not producers, unresolvable declarations are reported, a LEDGER code '
10061078
+ 'the docs publish a status for is reconciled in both directions while one no page publishes stays out of '
1007-
+ 'the vocabulary, an entry heading in an unrecognised shape is reported instead of silently dropped, and '
1008-
+ 'the baseline-expanding remedy stays maintainer-only.',
1079+
+ 'the vocabulary, an entry heading in an unrecognised shape is reported instead of silently dropped, the '
1080+
+ 'baseline-expanding remedy stays maintainer-only, and a baselined code leaving the unpinned census is '
1081+
+ 'named a producer or a removed doc entry — never the wrong one of the two.',
10091082
);
10101083
process.exit(0);
10111084
}
@@ -1058,7 +1131,9 @@ const baseline = existsSync(BASELINE_PATH)
10581131
: { unpinned: [] };
10591132
const baselined = new Set(baseline.unpinned ?? []);
10601133
const newlyUnpinned = result.unpinned.filter((c) => !baselined.has(c));
1061-
const nowPinned = [...baselined].filter((c) => !result.unpinned.includes(c) && vocabulary.includes(c)).sort();
1134+
const nowPinnedFindings = nowPinned({
1135+
baselined: [...baselined], unpinned: result.unpinned, vocabulary, documented: doc.documented,
1136+
});
10621137

10631138
if (update) {
10641139
writeFileSync(
@@ -1143,7 +1218,9 @@ for (const f of result.emittedNotDocumented) failures.push(emittedNotDocumentedM
11431218
for (const f of result.documentedNotReachable) failures.push(documentedNotReachableMessage(f));
11441219
for (const u of doc.unreadableHeadings) failures.push(unreadableHeadingMessage(u));
11451220
for (const c of newlyUnpinned) failures.push(newUnpinnedMessage(c));
1146-
for (const c of nowPinned) failures.push(nowPinnedMessage(c));
1221+
for (const f of nowPinnedFindings) {
1222+
failures.push(f.reason === 'producer' ? nowPinnedProducerMessage(f.code) : nowPinnedDocRemovedMessage(f.code));
1223+
}
11471224

11481225
if (failures.length) {
11491226
console.error('');

0 commit comments

Comments
 (0)