@@ -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 : [ ] } ;
10591132const baselined = new Set ( baseline . unpinned ?? [ ] ) ;
10601133const 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
10631138if ( update ) {
10641139 writeFileSync (
@@ -1143,7 +1218,9 @@ for (const f of result.emittedNotDocumented) failures.push(emittedNotDocumentedM
11431218for ( const f of result . documentedNotReachable ) failures . push ( documentedNotReachableMessage ( f ) ) ;
11441219for ( const u of doc . unreadableHeadings ) failures . push ( unreadableHeadingMessage ( u ) ) ;
11451220for ( 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
11481225if ( failures . length ) {
11491226 console . error ( '' ) ;
0 commit comments