@@ -5089,6 +5089,75 @@ export function artifactOnlyNote({ artifacts, dir, coversYourPath }) {
50895089 ];
50905090}
50915091
5092+ /**
5093+ * The artifact-roster families, as their own labelled block — printed on every
5094+ * run, never counted among the derived families (#14880).
5095+ *
5096+ * ## What this block is for, and what it deliberately does not say
5097+ *
5098+ * `artifactOnlyNote` above says all of this per family, but only under
5099+ * `--residue`, and only inside the silent listing a dev reading a dispatch
5100+ * brief is not told to ask for. Measured twice on this card: a dev derived the
5101+ * families for a diff, ran every one, and shipped a CI red carried by a gate
5102+ * whose declared literals are its own artifacts — `check:optional-error-sink`
5103+ * on PR #14866, then `check:error-code-provenance` on PR #14930, whose residue
5104+ * text names the remedy in its own words. Both are invisible to a `--commands`
5105+ * harvest by construction, for every card, not just theirs.
5106+ *
5107+ * So the block states the one thing that is true of every member and is not a
5108+ * guess about intent: this derivation scores them `silent` for EVERY card in
5109+ * the tree, so their silence is a fact about a LIST rather than a verdict about
5110+ * your paths.
5111+ *
5112+ * ⛔ It does NOT call them repo-wide scanners, and the refusal is the same one
5113+ * `artifactOnlyNote`'s docblock prices: whether a roster is a baseline sitting
5114+ * in a directory or a census taken OF that directory is exactly the intent this
5115+ * tool refuses to read out of the tree, and the two live side by side here
5116+ * (`check:where-matcher` names one baseline and walks `packages/**`;
5117+ * `check-entry-guard` named ten files under `scripts/` and walked all of it).
5118+ * A block asserting "these are scanners you must run" would be a fabricated
5119+ * lead over the members for which it is false — the expensive direction.
5120+ *
5121+ * ⛔ And it is NEVER merged into the derived list or into any count. The rows
5122+ * are `silent`, and `commandsFor`/`familyReconciliation` read only the matched,
5123+ * convention and always-runs rows, so the separation is structural rather than
5124+ * a filter someone has to remember. In `--commands` the block goes to STDERR
5125+ * for the reason every other accounting there does: stdout carries commands and
5126+ * nothing else, and a labelled block in that stream is prose for a harvest to
5127+ * pattern-match.
5128+ *
5129+ * The ⛔ subset is the correlation the card asks for by name — the rosters
5130+ * whose common directory contains one of THIS card's paths, where the silence
5131+ * is not evidence in either direction.
5132+ */
5133+ export function artifactRosterLines(rosters = []) {
5134+ if (rosters.length === 0) return [];
5135+ const inverted = rosters.filter((r) => r.coversYourPath);
5136+ const lines = [
5137+ `Artifact rosters — ${rosters.length} famil(ies) whose \`silent\` verdict is a fact about a LIST, not about your paths:`,
5138+ ' Each declares only tracked FILES — a baseline, an allowlist of the members it already has. A list of the files that',
5139+ ' already exist can never contain one added tomorrow, so this derivation scores them silent for EVERY card in the tree,',
5140+ ' and no path you pass can move them. ⛔ They are NOT in the runnable total above and are NOT counted among the derived',
5141+ ' families. Run them, or read them — but ⛔ never read their silence as a clearance.',
5142+ ' ⇒ The fix is the gate\'s, not this tool\'s: declare the scan surface beside the roster (the subtree spelling), after',
5143+ ' which the family is MATCHED here and leaves this block.',
5144+ ];
5145+ if (inverted.length) {
5146+ lines.push(
5147+ ` ⛔ ${inverted.length} of them keep that roster in a directory one of YOUR paths is in (marked ⛔ below) — there the`,
5148+ ' silence is not evidence in EITHER direction. Read those gates before treating them as passed.',
5149+ );
5150+ } else {
5151+ lines.push(' None of their rosters sits in a directory your paths are in, so none of them is a lead about this card.');
5152+ }
5153+ for (const r of [...rosters].sort((a, b) => a.command.localeCompare(b.command))) {
5154+ lines.push(
5155+ ` - ${r.command}${r.coversYourPath ? ` ⛔ roster under ${r.dir}, which one of your paths is in` : ''}`,
5156+ );
5157+ }
5158+ return lines;
5159+ }
5160+
50925161// ---------------------------------------------------------------------------
50935162// The reachability sweep — a declared population that matches NOTHING (#9883)
50945163// ---------------------------------------------------------------------------
@@ -9574,7 +9643,7 @@ export function runReconciliationLines(recon) {
95749643 * That distinction is the card's own subject matter: what is left out of a list
95759644 * must be visible in the list.
95769645 */
9577- export function derivationJson({ paths, matchedRows, kindGroups, pending, counts, identity, alwaysRunsRows = [] }) {
9646+ export function derivationJson({ paths, matchedRows, kindGroups, pending, counts, identity, alwaysRunsRows = [], rosters = [] }) {
95789647 const commands = commandsFor({ matchedRows, kindGroups, alwaysRunsRows });
95799648 const { otherCommands, ...spelling } = spellingSplit(commands);
95809649 return {
@@ -9593,6 +9662,15 @@ export function derivationJson({ paths, matchedRows, kindGroups, pending, counts
95939662 // (that would be a lead on every card) and must not have to infer it from
95949663 // the commands list either (#14189).
95959664 alwaysRunsPopulation: alwaysRunsRows,
9665+ // IN this document and ⛔ NOT in `commands`, for the reason
9666+ // `artifactRosterLines` states: these families are `silent`, so no path a
9667+ // caller passes can move them, and merging them into the runnable union
9668+ // would make every card's total a different number for a reason unrelated
9669+ // to the card. Their own key instead, so a machine consumer reads the same
9670+ // omission the human block names rather than inferring it (#14880).
9671+ artifactRosterSilences: rosters.map(({ check, command, workflows, artifacts, dir, coversYourPath }) => ({
9672+ check, command, workflows, artifacts, dir, coversYourPath,
9673+ })),
95969674 pendingChangeset: {
95979675 probePath: CHANGESET_PROBE_PATH,
95989676 families: pending.map(({ check, entry }) => ({
@@ -9621,13 +9699,13 @@ export function derivationJson({ paths, matchedRows, kindGroups, pending, counts
96219699 * LOUD. A quiet omission is the defect this mode was added to fix, and adding a
96229700 * new one inside the fix is how that defect reproduces itself one layer up.
96239701 */
9624- function machineReadableOutput(mode, { paths, matchedRows, kindGroups, pending, counts, alwaysRunsRows = [] }) {
9702+ function machineReadableOutput(mode, { paths, matchedRows, kindGroups, pending, counts, alwaysRunsRows = [], rosters = [] }) {
96259703 const identity = repoIdentity();
96269704 const commands = commandsFor({ matchedRows, kindGroups, alwaysRunsRows });
96279705 const split = spellingSplit(commands);
96289706
96299707 if (mode === 'json') {
9630- console.log(JSON.stringify(derivationJson({ paths, matchedRows, kindGroups, pending, counts, identity, alwaysRunsRows }), null, 2));
9708+ console.log(JSON.stringify(derivationJson({ paths, matchedRows, kindGroups, pending, counts, identity, alwaysRunsRows, rosters }), null, 2));
96319709 } else {
96329710 for (const command of commands) console.log(command);
96339711 }
@@ -9666,6 +9744,12 @@ function machineReadableOutput(mode, { paths, matchedRows, kindGroups, pending,
96669744 'they are derived against a path that does not exist yet. Write the changeset, then derive again.',
96679745 );
96689746 }
9747+ // The FOURTH thing stdout deliberately omits (#14880), on stderr for exactly
9748+ // the reason the three above are: the block is prose, and prose in the stream
9749+ // a consumer executes is the harvest hazard this mode exists to make
9750+ // unreachable. ⛔ Never merged into the command list — these families are
9751+ // `silent`, and no path a caller passes can move them.
9752+ for (const line of artifactRosterLines(rosters)) console.error(` ${line}`);
96699753 console.error(
96709754 ' ⛔ 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.',
96719755 );
@@ -9708,7 +9792,17 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [] } =
97089792 else if (verdict === 'undetermined') undetermined.push([check, entry]);
97099793 else silent.push([check, entry]);
97109794 }
9711- const rosters = silent.map(([, entry]) => artifactOnlySilence(entry, paths, tree)).filter(Boolean);
9795+ // The roster classification travels ON the row, for the same reason the
9796+ // matched provenance does: the human block, the `--commands` stderr
9797+ // accounting and the `--json` document are three readings of THESE rows, so
9798+ // none of them can name a different set than the residue summary counts
9799+ // (#14880).
9800+ const rosters = silent
9801+ .map(([check, entry]) => {
9802+ const roster = artifactOnlySilence(entry, paths, tree);
9803+ return roster ? { check, command: runnableInvocation(entry), workflows: [...entry.workflows], ...roster } : null;
9804+ })
9805+ .filter(Boolean);
97129806
97139807 // ONE structured answer, rendered three ways below. The human block, the
97149808 // `--commands` list and the `--json` document are readings of these same
@@ -9784,6 +9878,7 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [] } =
97849878 kindGroups,
97859879 pending,
97869880 alwaysRunsRows,
9881+ rosters,
97879882 counts: {
97889883 discovered: byCheck.size,
97899884 workflows: workflows.length,
@@ -9879,6 +9974,19 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [] } =
98799974 console.log('');
98809975 for (const line of familyReconciliationLines(recon)) console.log(line);
98819976
9977+ // Directly BELOW that total and above everything else it excludes (#14880).
9978+ // The placement IS the claim: the reconciliation line closes the runnable
9979+ // answer, and every section under it names something outside the answer. This
9980+ // one names the families no path can ever move — printed on every run, not
9981+ // only under `--residue`, because a dev reading a dispatch brief is never
9982+ // told to pass that flag and both measured CI reds on this card were carried
9983+ // by families of exactly this shape.
9984+ const rosterOut = artifactRosterLines(rosters);
9985+ if (rosterOut.length) {
9986+ console.log('');
9987+ for (const line of rosterOut) console.log(line);
9988+ }
9989+
98829990 const pendingOut = pendingChangesetLines(pending);
98839991 if (pendingOut.length) {
98849992 console.log('');
@@ -12957,6 +13065,59 @@ function selfTest() {
1295713065 .includes('ordinary one'),
1295813066 );
1295913067
13068+ // ── The roster block, printed where a dev without --residue will see it (#14880)
13069+ //
13070+ // The note above is per family and prints only inside the silent listing,
13071+ // which is behind a flag no dispatch brief tells anyone to pass. Two measured
13072+ // CI reds on this card were carried by families of exactly this shape
13073+ // (`check:optional-error-sink`, `check:error-code-provenance`), invisible to
13074+ // a `--commands` harvest for EVERY card. The block states the standing fact
13075+ // and names the families — and its whole contract is that it is a block
13076+ // BESIDE the derived list, never a part of it.
13077+ const blockRows = [
13078+ { check: 'check:b', command: 'pnpm check:b', workflows: ['lint.yml'], artifacts: ['scripts/a.mjs'], dir: 'scripts', coversYourPath: true },
13079+ { check: 'check:a', command: 'pnpm check:a', workflows: ['lint.yml'], artifacts: ['docs/x.md'], dir: 'docs', coversYourPath: false },
13080+ ];
13081+ const blockOut = artifactRosterLines(blockRows);
13082+ t('no rosters, no block — an empty section is never printed', artifactRosterLines([]).length === 0);
13083+ t('the block sizes itself and names every family, sorted by the command a dev would run', blockOut[0].includes('2 famil(ies)')
13084+ && blockOut.filter((l) => l.startsWith(' - ')).join('|') === ' - pnpm check:a| - pnpm check:b ⛔ roster under scripts, which one of your paths is in');
13085+ t(
13086+ '⭐ it says out loud that these are OUTSIDE the derived total, which is the whole reason it is a separate block',
13087+ blockOut.some((l) => l.includes('NOT counted among the derived')) && blockOut.some((l) => l.includes('NOT in the runnable total')),
13088+ );
13089+ t(
13090+ 'and it marks the correlated subset — the rosters sitting in a directory one of the card\'s paths is in',
13091+ blockOut.some((l) => l.includes('1 of them keep that roster in a directory one of YOUR paths is in')),
13092+ );
13093+ t(
13094+ 'a card no roster touches gets the standing fact instead of a warning about none of them',
13095+ artifactRosterLines([{ ...blockRows[1] }]).some((l) => l.includes('None of their rosters sits in a directory your paths are in')),
13096+ );
13097+ // ⛔ The refusal, and it is the one that keeps this block from being the
13098+ // fabricated lead `artifactOnlyNote`'s docblock prices: whether a roster is a
13099+ // baseline in a directory or a census OF it is intent, and intent is not in
13100+ // the tree. The block must not call them scanners, and must not tell anyone
13101+ // the gate reads their file.
13102+ t(
13103+ '⛔ and it never calls them scanners or claims they read your file — the half the tree cannot answer',
13104+ !/scanner|reads your file|very likely reads/.test(blockOut.join('\n')),
13105+ blockOut.join('\n'),
13106+ );
13107+ t(
13108+ 'it names the producer-side remedy the residue already carries, so the block points at a fix and not only at work',
13109+ blockOut.some((l) => l.includes('declare the scan surface beside the roster')),
13110+ );
13111+ // ⛔ STRUCTURAL, not a filter someone has to remember: `commandsFor` reads the
13112+ // matched, convention and always-runs rows only, and a roster family is
13113+ // `silent`. Asserted against the real union so a future edit that started
13114+ // feeding rosters into it reddens here rather than in a dev's harvest.
13115+ t(
13116+ '⛔ a roster command is not in the runnable union, whatever the block prints',
13117+ !commandsFor({ matchedRows: [{ check: 'check:m', command: 'pnpm check:m', ciOnly: null }], kindGroups: [], alwaysRunsRows: [] })
13118+ .some((c) => c === 'pnpm check:a' || c === 'pnpm check:b'),
13119+ );
13120+
1296013121 // ── The classifier returned a plausible WRONG CATEGORY (#13520) ───────────
1296113122 //
1296213123 // ⚠️ Every case below asserts the CATEGORY, never "it did not crash" and
@@ -17816,6 +17977,25 @@ function selfTest() {
1781617977 );
1781717978 t('and every command still on the list is one a dev can actually run here', cmdRows.length > 0 && cmdRows.every((l) => /^(pnpm|node) \S/.test(l)));
1781817979 t('the stderr accounting says the omission out loud, where it cannot corrupt the harvest', (cmdRun.stderr ?? '').includes('CI-MEASURED ONLY'));
17980+ // ⭐ #14880's block, on the same real run. Three claims, and the third is
17981+ // the one a unit case cannot make: the block exists, it is on STDERR, and
17982+ // not one of the families it names leaked into the stream a consumer
17983+ // executes. A block on stdout would be prose in the harvest — the exact
17984+ // hazard `--commands` exists to make unreachable.
17985+ const rosterBlockStart = (cmdRun.stderr ?? '').indexOf('Artifact rosters —');
17986+ t('⭐ the artifact-roster block is printed for a real card (#14880)', rosterBlockStart >= 0);
17987+ t('…on stderr, never in the stream a harvest executes', !(cmdRun.stdout ?? '').includes('Artifact rosters —'));
17988+ const rosterBlockCommands = (cmdRun.stderr ?? '')
17989+ .slice(rosterBlockStart < 0 ? 0 : rosterBlockStart)
17990+ .split('\n')
17991+ .filter((l) => /^\s+- (pnpm|node) /.test(l))
17992+ .map((l) => l.trim().slice(2).split(' ')[0].trim());
17993+ t('…and it really names families, so the two cases above judge something', rosterBlockCommands.length > 0);
17994+ t(
17995+ '⛔ and not one of them is in the runnable list — the block sits BESIDE the derivation, never inside it',
17996+ rosterBlockCommands.every((c) => !cmdRows.includes(c)),
17997+ rosterBlockCommands.filter((c) => cmdRows.includes(c)).join(', '),
17998+ );
1781917999 } finally {
1782018000 rmSync(harvestTmp, { recursive: true, force: true });
1782118001 }
0 commit comments