From 9aeff306e18399e130b8339252bd3150c7d07f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 02:16:27 +0000 Subject: [PATCH 1/2] fix(pm): read the one OUTSIDE enumeration on the --ran and --commands lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list of blocks that sit outside a card's runnable total is one claim. #16398 built it once for the human lane, as `outsideBlockNames`. The other two output lanes kept their own short prose copies: `--ran` named three of the five blocks a plain run prints, and the `--commands` / `--json` stderr accounting named exactly one — on the very channel this file's own header tells consumers to use INSTEAD of harvesting the prose. `outsideBlockNames` / `outsideBlocksPhrase` are exported now and all three lanes read them. The third name ("the pending-changeset families") was still spelled unconditionally while `pendingChangesetLines` returns nothing at zero, so it is conditional on its own count too, from the array that renders the block — like the two beside it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HxLw5aKDPR5RJgyUR7Exkd --- scripts/pm/dispatch-gates.mjs | 369 +++++++++++++++++++++++++++++----- 1 file changed, 314 insertions(+), 55 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index b2232d75d7..ba9480fc99 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -10998,10 +10998,12 @@ export function commandsFor({ matchedRows = [], kindGroups = [], alwaysRunsRows * the artifact rosters and the declared WIDE population. A harvester who * followed the enumeration and stopped never reached either, and CI caught the * difference on a family printed in the WIDE block (#16398). The list now - * exists ONCE, as `outsideBlockNames` in `familyReconciliationLines`, built - * from the block counts on `recon` so a block that printed cannot be missing - * from it and a block that did not print cannot be named. Amend it there; - * there is no second copy here to keep in step. + * exists ONCE, as the exported `outsideBlockNames`, built from the block counts + * its callers hand it so a block that printed cannot be missing from it and a + * block that did not print cannot be named. All THREE output lanes read it — + * this one, `--ran` and the `--commands`/`--json` stderr accounting, which used + * to carry short prose copies of their own (#16795). Amend it there; there is + * no second copy here to keep in step. * * `staleRows` and the row/family gap are surfaced rather than smoothed. A * consumer counting PRINTED rows in the convention block and comparing them @@ -11010,7 +11012,7 @@ export function commandsFor({ matchedRows = [], kindGroups = [], alwaysRunsRows * hit by two kinds prints twice. Both are stated in the rendering. */ export function familyReconciliation({ - matchedRows = [], kindGroups = [], alwaysRunsRows = [], rosterRows = [], widePopulationRows = [], + matchedRows = [], kindGroups = [], alwaysRunsRows = [], rosterRows = [], widePopulationRows = [], pendingRows = [], } = {}) { const commands = commandsFor({ matchedRows, kindGroups, alwaysRunsRows }); // The SAME expression commandsFor uses for its matched half. Written as a @@ -11084,15 +11086,22 @@ export function familyReconciliation({ staleRows, ciOnlyConventionRows, notRunnableConventionRows, - // Two counts that are NOT terms of the total and never enter the closure - // assertion below — they are the SIZES of two blocks printed under this + // Three counts that are NOT terms of the total and never enter the closure + // assertion below — they are the SIZES of three blocks printed under this // line, carried here for the same reason `ciOnly` and `notRunnable` are: // the rendering has to name what sits outside the answer, and a count it // reads from the arrays that RENDER those blocks cannot disagree with them - // (#16398). `familyReconciliationLines` is where they are used; its - // `outsideBlockNames` is the only place the list of outside blocks exists. + // (#16398). They are handed to `outsideBlockNames`, which is the only place + // the list of outside blocks exists. artifactRosters: rosterRows.length, widePopulation: widePopulationRows.length, + // The THIRD block size (#16795). Its name was the one the enumeration still + // spelled UNCONDITIONALLY, while `pendingChangesetLines` returns nothing at + // zero — so on a card with no pending family the sentence pointed a reader + // below at a heading that is not there. That is this same defect facing the + // other way, and it is fixed the same way: by counting the array that + // renders the block rather than by writing the name out. + pendingChangeset: pendingRows.length, }; if (recon.matched + recon.convention - recon.both + recon.alwaysRunsOnly !== recon.total) { throw new Error( @@ -11106,6 +11115,83 @@ export function familyReconciliation({ return recon; } +/** + * ⭐ The blocks that sit OUTSIDE a card's runnable total, enumerated ONCE for + * ALL THREE output lanes. + * + * This is one claim, and it has been spelled four different ways. The human + * rendering and a comment beside it each wrote it out in prose; the two copies + * drifted, and both named three of the five blocks the same run printed — the + * artifact rosters and the declared WIDE population, the two nothing else tells + * a seat to run. A harvester who followed the enumeration and stopped reached + * neither, and CI reddened on a family printed in the WIDE block (#16398). That + * card built the list once, for the human lane. The other two lanes kept their + * own short prose copies: `--ran` named three of five, and the + * `--commands`/`--json` stderr accounting named exactly ONE — on the very + * channel this file's own header tells consumers to use INSTEAD of harvesting + * the prose, which makes a whole block missing from it the same failure on the + * lane that was supposed to be the safe one (#16795). + * + * ⛔ So a lane never spells its own copy. It calls this with the counts it + * holds, and a block it does not know about is absent by construction rather + * than by care. Four copies of one sentence is how the sentence went wrong; + * this is the one copy. + * + * ⭐ Built from the block COUNTS, never written out: each count is the length + * of the very array `artifactRosterLines`, `widePopulationLines` or + * `pendingChangesetLines` renders, so the enumeration cannot name a set the + * output does not contain. A count of 0 drops the name, because at zero rows + * all three of those return nothing — pointing a reader "below" at a heading + * that is not there is this same defect facing the other way, and the + * pending-changeset name carried exactly that bug for as long as it was + * spelled unconditionally. The last two names have no count and no condition + * because their renderings have none: `unreachableLines` and `alwaysRunLines` + * print on EVERY run, at zero as at fifty. + * + * Named in the order a plain run PRINTS them, so a reader walking down the + * output meets the blocks in the order this list promised them. + */ +export function outsideBlockNames({ artifactRosters = 0, widePopulation = 0, pendingChangeset = 0 } = {}) { + return [ + ...(artifactRosters > 0 ? [`the ${artifactRosters} artifact-roster famil(ies)`] : []), + ...(widePopulation > 0 ? [`the ${widePopulation} declared WIDE-population famil(ies)`] : []), + ...(pendingChangeset > 0 ? [`the ${pendingChangeset} pending-changeset famil(ies)`] : []), + 'the unreachable listing', + 'the always-runs tail', + ]; +} + +/** + * The same enumeration as one English phrase — the only place the join is + * spelled, for the reason the list itself has only one place. + * + * Every name opens with a lowercase article, so a sentence-initial use raises + * the leading letter at the point of use rather than keeping a second, + * capitalised copy of the list — which is the duplication this pair exists to + * remove. + */ +export function outsideBlocksPhrase(counts) { + const names = outsideBlockNames(counts); + return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`; +} + +/** + * The three block sizes a `recon` from `familyReconciliation` carries, read + * back off it in the shape `outsideBlockNames` takes. + * + * A named reader rather than an object literal at each call site: the human + * lane and `--ran` both hand the same three counts across, and two literals + * spelling the same three key names is the smallest possible version of the + * duplication this whole construction removes. + */ +export function outsideBlockCounts(recon) { + return { + artifactRosters: recon?.artifactRosters ?? 0, + widePopulation: recon?.widePopulation ?? 0, + pendingChangeset: recon?.pendingChangeset ?? 0, + }; +} + /** * The reconciliation, rendered. Printed on EVERY completed derivation, hit or * not — including at zero. @@ -11144,30 +11230,12 @@ export function familyReconciliationLines(recon) { ' Named under their own heading above with the variable in the value position, carried on their row in --json,' + ' and omitted from --commands by design.' : null; - // ⭐ The enumeration of what sits OUTSIDE this total — built ONCE here and - // rendered by BOTH branches below, because it is one claim and two copies of - // it is exactly how it went wrong. It was prose, and the prose named three - // blocks while the same run printed five; the two it omitted, the artifact - // rosters and the declared WIDE population, are the ones nothing else in this - // rendering tells a seat to run. A harvester who follows this line and stops - // reached neither, and CI reddened on a family printed in the WIDE block - // (#16398). - // - // Assembled from the block COUNTS on `recon` rather than written out, so the - // list cannot disagree with what was printed: the counts are the lengths of - // the very arrays `artifactRosterLines` and `widePopulationLines` render. A - // count of 0 drops the name, because at zero rows both of those return - // nothing — pointing a reader "below" at a heading that is not there is the - // same defect facing the other way. Named in the order they are PRINTED, so - // a reader walking down the output meets them as promised. - const outsideBlockNames = [ - ...(recon.artifactRosters > 0 ? [`the ${recon.artifactRosters} artifact-roster famil(ies)`] : []), - ...(recon.widePopulation > 0 ? [`the ${recon.widePopulation} declared WIDE-population famil(ies)`] : []), - 'the pending-changeset families', - 'the unreachable listing', - 'the always-runs tail', - ]; - const outsideBlocks = `${outsideBlockNames.slice(0, -1).join(', ')} and ${outsideBlockNames[outsideBlockNames.length - 1]}`; + // ⭐ The enumeration of what sits OUTSIDE this total, READ from the one place + // it exists (`outsideBlockNames`) and rendered by BOTH branches below. It is + // one claim, and a copy of it per lane is exactly how it went wrong — see + // that function for the two measured drifts and for why every name is + // conditional on the count of the array that renders its block. + const outsideBlocks = outsideBlocksPhrase(outsideBlockCounts(recon)); // Both uses below are sentence-initial and every name opens with a lowercase // article, so the leading letter is raised here rather than by keeping a // second, capitalised copy of the list — which is the duplication this whole @@ -11533,8 +11601,19 @@ export function runReconciliation({ * remainder is classified by the tool: a verdict a reader has to assemble from * several counts is a verdict that gets waved through once the counts are * routinely non-empty. + * + * `outside` carries the SIZES of the blocks a run without `--ran` prints below + * its reconciliation — the same three counts `familyReconciliation` puts on its + * own recon, taken from the same arrays. They are a parameter rather than a + * term of `runReconciliation` on purpose: this reconciliation's arithmetic is + * record-against-derived and closes over its own classes, and a block size is + * not one of them — it is an input to the SENTENCE, so it arrives where the + * sentence is written. Defaulting to zero counts keeps every caller that has no + * derivation beside it (the self-test's fixtures) naming exactly the two blocks + * that print unconditionally, which is the true answer for a run with none of + * the other three (#16795). */ -export function runReconciliationLines(recon) { +export function runReconciliationLines(recon, outside = {}) { const lines = []; const marker = RUN_RECORD_UNMEASURED_MARKER; lines.push( @@ -11595,9 +11674,17 @@ export function runReconciliationLines(recon) { for (const command of recon.conflicts) { lines.push(` ⚠️ '${command}' is recorded BOTH as run and as ${marker}. Read as run; fix the record so it states one thing.`); } + // ⭐ The enumeration READ from the one place it exists, not a prose copy. + // This sentence used to spell three of the five blocks a plain run prints, + // omitting the artifact rosters and the declared WIDE population — the same + // short list, on a second lane, that #16398 measured a CI round trip for on + // the first one (#16795). It also named the pending-changeset families + // unconditionally, at a heading that is not printed when there are none. + // Both are structural now: `outsideBlockNames` cannot name a block whose + // count is zero, and cannot omit one whose count is not. lines.push( ' ⛔ This answers ONE link: what this card DERIVES against what you RAN. It is not a complete account of what CI runs on the PR —' + - ' the always-runs tail, the unreachable listing and the pending-changeset families are each outside the derived total, each printed under its own heading by a run without --ran.', + ` ${outsideBlocksPhrase(outside)} are each outside the derived total, each printed under its own heading by a run without --ran.`, ); lines.push( recon.ok @@ -11685,10 +11772,13 @@ export function derivationJson({ paths, matchedRows, kindGroups, pending, counts * documented — there is no prose in the stream to pattern-match, and therefore * no spelling for a pattern to prefer. * - * The stderr accounting exists so the two things stdout deliberately omits — - * the pending-changeset families and the always-runs tail — are omitted OUT - * LOUD. A quiet omission is the defect this mode was added to fix, and adding a - * new one inside the fix is how that defect reproduces itself one layer up. + * The stderr accounting exists so everything stdout deliberately omits is + * omitted OUT LOUD. A quiet omission is the defect this mode was added to fix, + * and adding a new one inside the fix is how that defect reproduces itself one + * layer up — which is exactly what the closing disclaimer did: it named the + * always-runs tail and stopped, while FIVE blocks sit outside the command list + * and the declared WIDE population was not mentioned in it at all. It reads + * `outsideBlockNames` now, with the counts this function already holds (#16795). */ function machineReadableOutput(mode, { paths, matchedRows, kindGroups, pending, counts, alwaysRunsRows = [], widePopulationRows = [], rosters = [] }) { const identity = repoIdentity(); @@ -11776,8 +11866,23 @@ function machineReadableOutput(mode, { paths, matchedRows, kindGroups, pending, // unreachable. ⛔ Never merged into the command list — these families are // `silent`, and no path a caller passes can move them. for (const line of artifactRosterLines(rosters)) console.error(` ${line}`); + // ⭐ The closing disclaimer, READ from the one place the list exists. It named + // the always-runs tail alone, so a `--commands` consumer — the reader this + // file's own header sends here INSTEAD of the prose — was never told the + // declared WIDE population exists, which is the same defect on the lane that + // was supposed to be the safe one (#16795). + // + // The counts are the LENGTHS OF THE ARRAYS THAT RENDER THE BLOCKS on a plain + // run, deliberately not the runnable subsets the count lines above report: + // `widePopulationLines` prints its refused rows too, so sizing this name off + // `wideRunnable` would name a block by a number the block does not show. Each + // name is the block, not the part of it this lane happens to summarise. console.error( - ' ⛔ Not a complete account of what CI runs on this PR: the always-runs tail (workflows with no path filter) is NOT here. Run without --commands/--json for it.', + ` ⛔ Not a complete account of what CI runs on this PR: ${outsideBlocksPhrase({ + artifactRosters: rosters.length, + widePopulation: widePopulationRows.length, + pendingChangeset: pending.length, + })} are each OUTSIDE the ${commands.length} command(s) on stdout. Run without --commands/--json to see every one of them named.`, ); } @@ -11937,7 +12042,17 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [] } = pendingCommands: new Set(pending.map(({ entry }) => runnableInvocation(entry))), record: runRecord, }); - for (const line of runReconciliationLines(recon)) console.log(line); + // The block sizes handed across as the SAME arrays a plain run renders + // those blocks from — never as recounts of them, for the reason the human + // lane's own call states: a second count could name a set the output does + // not contain (#16398, #16795). `--ran` prints none of these blocks itself; + // the sentence points at the run that does, and it must point at what THAT + // run would print for these paths, which is what these three arrays are. + for (const line of runReconciliationLines(recon, { + artifactRosters: rosters.length, + widePopulation: widePopulationRows.length, + pendingChangeset: pending.length, + })) console.log(line); return recon.ok ? 0 : 1; } @@ -11970,13 +12085,13 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [] } = // the block it counts) and the reconciliation line below. Recomputing it in // either place would be two readings of one derivation, which is the drift // this card is about. - // `rosters` and `widePopulationRows` are handed in as the SAME arrays the two - // blocks below the reconciliation are rendered from, never as recounts of - // them: the line has to name every block that sits outside this total, and a - // second count of those rows could name a set the output does not contain - // (#16398). + // `rosters`, `widePopulationRows` and `pending` are handed in as the SAME + // arrays the three conditional blocks below the reconciliation are rendered + // from, never as recounts of them: the line has to name every block that sits + // outside this total, and a second count of those rows could name a set the + // output does not contain (#16398, #16795). const recon = familyReconciliation({ - matchedRows, kindGroups, alwaysRunsRows, rosterRows: rosters, widePopulationRows, + matchedRows, kindGroups, alwaysRunsRows, rosterRows: rosters, widePopulationRows, pendingRows: pending, }); console.log(`dispatch-gates: ${byCheck.size} check famil(ies) discovered across ${workflows.length} workflow file(s) — derived at runtime, nothing listed in this script.\n`); @@ -21609,11 +21724,14 @@ function selfTest() { // could name three of the five blocks the same run printed and stay green // here for the whole time a harvester following it was missing two of them // (#16398). Every name below is its own assertion: dropping ONE reds. + // Three DISTINCT counts (2 / 1 / 3), so a name wired to the wrong array + // reds here instead of reading right by coincidence. const outsideRecon = familyReconciliation({ matchedRows: rRows, kindGroups: rKinds, rosterRows: [{ check: 'check:r1' }, { check: 'check:r2' }], widePopulationRows: [{ check: 'check:w1' }], + pendingRows: [{ check: 'check:p1' }, { check: 'check:p2' }, { check: 'check:p3' }], }); // The enumeration is read case-insensitively per NAME, because the leading // name is raised to open the sentence; the exact rendered phrase is pinned @@ -21624,7 +21742,7 @@ function selfTest() { for (const name of [ 'the 2 artifact-roster famil(ies)', 'the 1 declared WIDE-population famil(ies)', - 'the pending-changeset families', + 'the 3 pending-changeset famil(ies)', 'the unreachable listing', 'the always-runs tail', ]) { @@ -21635,26 +21753,41 @@ function selfTest() { // assert against it, and the order is the claim — a reader walking down the // output meets the blocks in the order this line promised them. t('and spells them in the order they are PRINTED below, as one phrase', (outsideLine ?? '').includes( - 'The 2 artifact-roster famil(ies), the 1 declared WIDE-population famil(ies), the pending-changeset families,' + 'The 2 artifact-roster famil(ies), the 1 declared WIDE-population famil(ies), the 3 pending-changeset famil(ies),' + ' the unreachable listing and the always-runs tail below are each OUTSIDE it, each with its own count.', )); - // The two counts are the lengths of the arrays that RENDER those blocks, so - // the enumeration cannot name a block the run did not print: at zero rows - // artifactRosterLines and widePopulationLines both return nothing, and a - // name pointing "below" at an absent heading is this same defect reversed. + // The THREE counts are the lengths of the arrays that RENDER those blocks, + // so the enumeration cannot name a block the run did not print: at zero rows + // artifactRosterLines, widePopulationLines and pendingChangesetLines all + // return nothing, and a name pointing "below" at an absent heading is this + // same defect reversed. const noBlocksLine = familyReconciliationLines(r).find((l) => l.includes('NOT a complete account of what CI runs')); t('and names NEITHER block on a run that printed neither', !(noBlocksLine ?? '').toLowerCase().includes('artifact-roster') && !(noBlocksLine ?? '').toLowerCase().includes('wide-population')); - t('...while still naming the three blocks that print unconditionally', namesOutside(noBlocksLine, ['the pending-changeset families', 'the unreachable listing', 'the always-runs tail'])); + // ⭐ The third name was the one still spelled unconditionally after #16398: + // `pendingChangesetLines` returns nothing at zero, so on a card with no + // pending family the sentence pointed below at a heading that is not there + // (#16795). It is conditional on its own count now, like the two above it. + t('nor the pending-changeset block, whose heading is absent at zero too', !(noBlocksLine ?? '').toLowerCase().includes('pending-changeset')); + t('...while still naming the two blocks that print unconditionally', namesOutside(noBlocksLine, ['the unreachable listing', 'the always-runs tail'])); + // ...and the CONTROL for that pair: a run with pending families and nothing + // else names the third block and neither of the other two, so the case + // above cannot be passing because the name went away for good. + const pendingOnlyLine = familyReconciliationLines(familyReconciliation({ + matchedRows: rRows, kindGroups: rKinds, pendingRows: [{ check: 'check:p1' }, { check: 'check:p2' }], + })).find((l) => l.includes('NOT a complete account of what CI runs')); + t('and a run that printed ONLY the pending block names exactly that one', namesOutside(pendingOnlyLine, ['the 2 pending-changeset famil(ies)']) + && !(pendingOnlyLine ?? '').toLowerCase().includes('artifact-roster') && !(pendingOnlyLine ?? '').toLowerCase().includes('wide-population')); // The ZERO-total branch renders the SAME list from the SAME expression: a // card with no runnable family of its own still owes every block below, and // two branches spelling this claim separately is how it drifted before. const zeroOutside = familyReconciliationLines(familyReconciliation({ matchedRows: [], kindGroups: [], rosterRows: [{ check: 'check:r1' }], widePopulationRows: [{ check: 'check:w1' }], + pendingRows: [{ check: 'check:p1' }], })); t('the zero branch enumerates the same blocks rather than naming one of them', zeroOutside.some((l) => namesOutside(l, [ 'the 1 artifact-roster famil(ies)', 'the 1 declared WIDE-population famil(ies)', - 'the pending-changeset families', + 'the 1 pending-changeset famil(ies)', 'the unreachable listing', 'the always-runs tail', ]))); @@ -22303,6 +22436,132 @@ function selfTest() { && !explainedText.includes("Outside this card's derivation"), ); + // ── ⭐ The OUTSIDE enumeration on the OTHER TWO lanes (#16795) ────────── + // + // The same claim as the human line's, and it was short on both. `--ran` + // named three of the five blocks a plain run prints — omitting the artifact + // rosters and the declared WIDE population, exactly the pair #16398 + // measured a CI round trip for on the human lane. Both lanes read + // `outsideBlockNames` now, so a name here can neither drift from that list + // nor survive the block it points at being absent. + // + // ⭐ Pinned NAME BY NAME and with a NEGATIVE beside every positive, because + // the weak shape is what failed before: a case asking only for a substring + // stayed green for the whole time the sentence was naming three of five. + const laneCounts = { artifactRosters: 2, widePopulation: 1, pendingChangeset: 3 }; + const outsideOf = (lines) => (lines.find((l) => l.includes('This answers ONE link')) ?? ''); + const ranAllBlocks = outsideOf(runReconciliationLines(full, laneCounts)); + for (const name of [ + 'the 2 artifact-roster famil(ies)', + 'the 1 declared WIDE-population famil(ies)', + 'the 3 pending-changeset famil(ies)', + 'the unreachable listing', + 'the always-runs tail', + ]) { + t(`--ran's disclaimer names "${name}" — every block a plain run prints, not a subset`, ranAllBlocks.includes(name)); + } + t('and spells them in PRINT order, as the one phrase the human lane spells', ranAllBlocks.includes( + 'the 2 artifact-roster famil(ies), the 1 declared WIDE-population famil(ies), the 3 pending-changeset famil(ies),' + + ' the unreachable listing and the always-runs tail are each outside the derived total', + )); + // The NEGATIVE: at zero rows those three blocks are not printed by the run + // this sentence points at, so naming them would send a reader to headings + // that are not there — the same defect facing the other way. + const ranNoBlocks = outsideOf(runReconciliationLines(full, { artifactRosters: 0, widePopulation: 0, pendingChangeset: 0 })); + t( + '--ran names NONE of the three conditional blocks on a derivation that has none', + !ranNoBlocks.toLowerCase().includes('artifact-roster') + && !ranNoBlocks.toLowerCase().includes('wide-population') + && !ranNoBlocks.toLowerCase().includes('pending-changeset'), + ); + t('...while still naming the two that print unconditionally', ranNoBlocks.includes('the unreachable listing and the always-runs tail')); + + // ── Lane 2: the `--commands` / `--json` stderr accounting ─────────────── + // + // Driven through `machineReadableOutput` itself rather than through a + // helper, because the defect was in what that function PRINTS: it named the + // always-runs tail alone, and a `--commands` consumer — the reader this + // file's own header sends here INSTEAD of the prose — was never told the + // declared WIDE population exists at all. A case against the helper would + // have stayed green through exactly that. + const captureStderr = (run) => { + const captured = []; + const realError = console.error; + const realLog = console.log; + console.error = (...args) => captured.push(args.join(' ')); + console.log = () => {}; + try { + run(); + } finally { + console.error = realError; + console.log = realLog; + } + return captured; + }; + const laneTwo = (over = {}) => captureStderr(() => machineReadableOutput('commands', { + paths: ['scripts/pm/dispatch-gates.mjs'], + matchedRows: [{ check: 'check:m1', command: 'pnpm check:m1' }], + kindGroups: [], + pending: [{ check: 'check:p1' }, { check: 'check:p2' }, { check: 'check:p3' }], + counts: {}, + alwaysRunsRows: [], + widePopulationRows: [{ check: 'check:w1', command: 'pnpm check:w1', workflows: ['w.yml'], reason: 'whole root', refused: null }], + rosters: [ + { check: 'check:r1', command: 'pnpm check:r1', workflows: ['w.yml'], artifacts: [], dir: 'scripts', coversYourPath: false, checkerHealth: null }, + { check: 'check:r2', command: 'pnpm check:r2', workflows: ['w.yml'], artifacts: [], dir: 'scripts', coversYourPath: false, checkerHealth: null }, + ], + ...over, + })).find((l) => l.includes('Not a complete account of what CI runs')) ?? ''; + const commandsAllBlocks = laneTwo(); + for (const name of [ + 'the 2 artifact-roster famil(ies)', + 'the 1 declared WIDE-population famil(ies)', + 'the 3 pending-changeset famil(ies)', + 'the unreachable listing', + 'the always-runs tail', + ]) { + t(`--commands' closing disclaimer names "${name}", not the always-runs tail alone`, commandsAllBlocks.includes(name)); + } + // ⭐ The card's own repro, as a case: `grep -c 'WIDE' commands.err` read 0. + // The token is what a consumer greps for, and it was absent from the whole + // stream because the one line that could carry it named a different block. + t("...so the stream a --commands consumer reads carries the token 'WIDE' at all", commandsAllBlocks.includes('WIDE')); + // The NEGATIVE, for the reason the --ran one is there: a sentence that + // cannot drop a name is the same broken instrument as one that cannot add + // one, pointed the other way. + const commandsNoBlocks = laneTwo({ rosters: [], widePopulationRows: [], pending: [] }); + t( + '--commands names NONE of the three conditional blocks when the run has none of them', + !commandsNoBlocks.toLowerCase().includes('artifact-roster') + && !commandsNoBlocks.toLowerCase().includes('wide-population') + && !commandsNoBlocks.toLowerCase().includes('pending-changeset'), + ); + t('...while still naming the two that print unconditionally', commandsNoBlocks.includes('the unreachable listing and the always-runs tail')); + // ⛔ And the stream stays a STREAM: the accounting is stderr-only, so a + // consumer redirecting stdout gets commands with no prose in front of them. + // That is the property the whole mode exists for, and a disclaimer that + // grew onto stdout would delete it. + const laneTwoStdout = []; + { + const realLog = console.log; + const realError = console.error; + console.log = (...args) => laneTwoStdout.push(args.join(' ')); + console.error = () => {}; + try { + machineReadableOutput('commands', { + paths: ['scripts/pm/dispatch-gates.mjs'], + matchedRows: [{ check: 'check:m1', command: 'pnpm check:m1' }], + kindGroups: [], pending: [{ check: 'check:p1' }], counts: {}, alwaysRunsRows: [], + widePopulationRows: [{ check: 'check:w1', command: 'pnpm check:w1', workflows: ['w.yml'], reason: 'whole root', refused: null }], + rosters: [], + }); + } finally { + console.log = realLog; + console.error = realError; + } + } + t('and the enumeration never reaches stdout, which carries the commands and nothing else', laneTwoStdout.join('\n') === 'pnpm check:m1'); + // ── argv: a two-token flag's value must not become a path ─────────────── const ranSplit = splitArgv([RAN_FLAG, 'ran.list', 'packages/spec/src/index.ts', '--residue']); t('the run record value never falls through into the path list', ranSplit.paths.length === 1 && ranSplit.runRecord === 'ran.list'); From 47e1d7101de3a6f6d03a594c6df605ce34b994a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 02:28:37 +0000 Subject: [PATCH 2/2] test(pm): reword one lane-2 case name so it reads on the always-runs row Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HxLw5aKDPR5RJgyUR7Exkd --- scripts/pm/dispatch-gates.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index ba9480fc99..41584fa2df 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -22520,7 +22520,7 @@ function selfTest() { 'the unreachable listing', 'the always-runs tail', ]) { - t(`--commands' closing disclaimer names "${name}", not the always-runs tail alone`, commandsAllBlocks.includes(name)); + t(`--commands' closing disclaimer names "${name}" — every block outside the command list, where it used to name one`, commandsAllBlocks.includes(name)); } // ⭐ The card's own repro, as a case: `grep -c 'WIDE' commands.err` read 0. // The token is what a consumer greps for, and it was absent from the whole