Skip to content

Commit c2e01dd

Browse files
committed
tooling(pm): name the artifact-roster silences in their own block, beside the derivation
A family whose declared literals all name tracked FILES declares a roster — a baseline, an allowlist of the members it already has — and never a population. A list of the files that already exist can never contain one added tomorrow, so this derivation scores those families `silent` for every card in the tree, and no path a caller passes can move them. Two measured CI reds were carried by exactly that shape: `check:optional-error-sink` on PR #14866 and `check:error-code-provenance` on PR #14930, both invisible to a `--commands` harvest by construction, for every card. `artifactOnlyNote` already said all of this — per family, but only inside the silent listing, which is behind a flag no dispatch brief tells anyone to pass. The block says it where the default run shows it, and where `--commands` puts every other accounting: on stderr, so the stream a consumer executes still carries commands and nothing else. Measured before building it, over the SILENT bucket for the diff of PR #14866: 32 of 120 silent families declare only tracked artifacts, 5 of them with the roster sitting in a directory one of that card's paths is in. A minority, so the block enumerates rather than only counting, and marks the correlated subset. The block is never counted among the derived families and never merged into the runnable list. That is structural, not a filter: rosters are `silent`, and `commandsFor` reads only the matched, convention and always-runs rows. It deliberately does NOT call these gates repo-wide scanners. Whether a roster is a baseline sitting in a directory or a census taken of that directory is intent, and intent is not in the tree — the two live side by side here. The block states what is true of every member instead, and points at the producer-side remedy the residue already carries: declare the scan surface beside the roster. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
1 parent fc4520b commit c2e01dd

1 file changed

Lines changed: 184 additions & 4 deletions

File tree

scripts/pm/dispatch-gates.mjs

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)