Skip to content

tooling(pm): key the gate derivation on (script, args) and name the artifact-roster silences - #15081

Queued
claude[bot] wants to merge 5 commits into
mainfrom
claude/issue-14880-dispatch-gates-coverage
Queued

tooling(pm): key the gate derivation on (script, args) and name the artifact-roster silences#15081
claude[bot] wants to merge 5 commits into
mainfrom
claude/issue-14880-dispatch-gates-coverage

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #14880
Fixes #15036

Three commits on two files under scripts/pm/: dispatch-gates.mjs (the derivation key, the --commands harvest, the usage line, the self-test) and bare-root-worklist.mjs (its recorded-verdict table only — see "Patch round" at the end for why that file joined the surface). No gate script under scripts/check-*.mjs and no workflow is edited: a gate whose declared roster is its own artifacts is reported here, per gate, never fixed here.

Verification head: 2412b019 (git rev-parse --short HEAD of the run that produced every reading below, taken after the last commit).

Authored in Claude Code session session_019RfFHiRCSs3JXLK4cwcfox (recorded in prose because a body edit normalises the footer's session URL away).


Commit 1 — the derivation key is (script, args), one entry per workflow invocation

Mechanism 3 on the card. discoverFamilies keyed a family on the script path, because the direct matcher captured the path and dropped the argument tail. CI's two invocations of one script therefore collapsed into one entry, and the entry kept was the plain one.

Measured on PR #14958 by the seat that filed the mechanism: the red was carried entirely by node scripts/check-tenant-audit-census.mjs --self-test (exit 1, 1 of 19 case(s) failed) while the plain run exited 0 — and the derived list named only the plain run. The list did not merely omit a gate; it returned a pass for a family CI fails.

Measurement first, as the claim asked — how common is the collapsed shape?

Read from the workflow text through the tool's own run: body reader, never typed:

  • 28 scripts in .github/workflows/lint.yml are invoked more than once under different argv.
  • 41 across all workflow files (check-links.yml 1, ci.yml 1, cut-rc.yml 3, governed-surface-guard.yml 1, lint.yml 28, pr-automation.yml 3, prerelease-pin-watch.yml 1, release-coverage-patrol.yml 2, validate-deps.yml 1).
  • 28 of 28 in lint.yml carry a check- basename, so every one of them collapsed. There is no multi-argv script in lint.yml that escaped the old key.

The 28 in lint.yml:

scripts/check-adr-symbol-anchors.mjs        scripts/check-aggregator-roster.mjs
scripts/check-ci-filter-parity.mjs          scripts/check-closing-keyword-parity.mjs
scripts/check-comment-mask-adoption.mjs     scripts/check-console-intercept-disarm.mjs
scripts/check-cross-repo-closer-outcome.mjs scripts/check-declaration-mirrors.mjs
scripts/check-doc-frontmatter.mjs           scripts/check-doc-route-spelling.mjs
scripts/check-docs-nav-label.mjs            scripts/check-docs-section-name.mjs
scripts/check-keyed-text-bounds.mjs         scripts/check-merge-queue-triage-outcome.mjs
scripts/check-plugin-teardown-shape.mjs     scripts/check-position-name-fold-loaders.mjs
scripts/check-published-list-mirrors.mjs    scripts/check-release-section-coverage.mjs
scripts/check-sdui-manifest.mjs             scripts/check-section-landing-index.mjs
scripts/check-self-test-wired.mjs           scripts/check-self-test-workflow-commands.mjs
scripts/check-skills-token-ratchet.mjs      scripts/check-step-collectors.mjs
scripts/check-system-context-census.mjs     scripts/check-tenant-audit-census.mjs
scripts/check-undeclared-dep-imports.mjs    scripts/check-whole-set-label-write.mjs

The census pair CI runs, quoted from the workflow as it stands today (the line numbers on the card have moved; grepped by text):

.github/workflows/lint.yml:1565:          node scripts/check-tenant-audit-census.mjs --self-test
.github/workflows/lint.yml:1566:          node scripts/check-tenant-audit-census.mjs

And the collapse, confirmed before the edit, on the harvest for PR #14958's own diff paths — one derived entry, the green one:

cmds-14958-before.txt:17:node scripts/check-tenant-audit-census.mjs

The change

The key becomes script plus the invocation's argv, and the argv half is admitted only when the tool can render the invocation runnably: a complete run of flag-shaped tokens. A tail carrying a value keeps the bare path key it has today rather than being truncated into a command that cannot run. Live refused specimens, none invented: check-adr-0087-registration.mjs --base "$MERGE_BASE", check-engine-split-ratio.mjs --days 90, check-test-completeness.mjs "$RUNNER_TEMP/test-core.log", and check-shard-attestation.mjs --emit continued onto two more lines that carry --job, --total and --out.

A line-continuation backslash is deliberately not a tail terminator — it stays inside the tail so renderableArgv refuses the whole thing. Ending the tail at the backslash was measured to be worse than the status quo: it produces --emit, which reads as a complete argv and is not. A redirection is a terminator, because the redirection belongs to the shell and not to this argv.

Effect on the tree, measured: 204 discovered families become 242; 36 scripts now carry more than one family; three check- scripts CI never invokes plainly lose a bare key that named a command nobody runs (check-dev-prereqs.mjs, check-doc-route-spelling.mjs, check-skill-frame-freshness.mjs).

Before/after --commands, on the three diffs the card was measured on

Re-derived with --repo objectstack-ai/objectstack --commands over each PR's real file list.

card diff before after added removed
PR #14866 (mechanism 1) 67 81 +15 −1
PR #14930 (mechanism 2) 66 80 +15 −1
PR #14958 (mechanism 3) 69 83 +15 −1

The delta is identical on all three (the split is a property of the workflows, not of the card). Added:

node scripts/check-adr-0087-registration.mjs --self-test
node scripts/check-changeset-no-major.mjs --self-test
node scripts/check-closing-keyword-parity.mjs --self-test
node scripts/check-comment-mask-adoption.mjs --self-test
node scripts/check-doc-frontmatter.mjs --self-test
node scripts/check-doc-route-spelling.mjs --advisory
node scripts/check-doc-route-spelling.mjs --self-test
node scripts/check-docs-section-name.mjs --self-test
node scripts/check-empty-changeset.mjs --self-test
node scripts/check-keyed-text-bounds.mjs --self-test
node scripts/check-plugin-teardown-shape.mjs --self-test
node scripts/check-section-landing-index.mjs --self-test
node scripts/check-system-context-census.mjs --self-test
node scripts/check-tenant-audit-census.mjs --self-test
node scripts/check-undeclared-dep-imports.mjs --self-test

Removed, on all three: node scripts/check-doc-route-spelling.mjs — a command CI never runs. It is invoked only as --advisory and as --self-test, both of which are now derived in its place.

⭐ The line the card is about is in the harvest for PR #14958: node scripts/check-tenant-audit-census.mjs --self-test.

One more narrowing, in the same commit and for the same reason

A --self-test invocation is never CI-measured-only. payloadEnvDependence reads the gate's module body with self-test bodies masked out, so the payload access that classification rests on belongs to the script's work — the invocation this one is not. Without it the split would have created node scripts/pm/check-governed-queue-guard.mjs --self-test and then suppressed it from --commands, hiding a command a dev can run. Cost on the tree today: zero — all twelve pre-existing self-test families score payloadEnv null, and the ciOnly count is 1 before and 1 after.

The #15036 member, folded in

The usage line printed on the derivation-failure path had [--residue] outside the alternation, which is that notation's way of saying it combines with every member — including the --tier the CLI has refused since #14753. It moves inside, attached to the three modes it really does modify:

before: usage: ... [--residue] [--tier | --commands | --json | --ran FILE] [--repo owner/name] [PATH ...] | --changed | --self-test
after:  usage: ... [--tier | [--residue] [--commands | --json | --ran FILE]] [--repo owner/name] [PATH ...] | --changed | --self-test

(The two placeholders are spelled FILE and PATH here; the source keeps its angle-bracket spelling.) Deleting [--residue] instead would understate it — the flag is legal with the other three and with the plain human rendering. The line moved into a constant so the pin can run: reaching the print site needs a checkout where changedPathsFromGit() refuses.


Commit 2 — the artifact-roster silences, named in their own block

Mechanisms 1 and 2. A family whose declared literals all name tracked FILES declares a roster — a baseline, an allowlist of the members it already has — never a population. A list of the files that already exist can never contain one added tomorrow, so the derivation scores those families silent for every card in the tree and no path a caller passes can move them. Both measured reds were carried by that shape: check:optional-error-sink (PR #14866) and check:error-code-provenance (PR #14930).

Measurement first, and the decision it forced

Over the SILENT residue bucket for PR #14866's diff, at this head:

  • 32 of 120 silent families declare only tracked artifacts.
  • 5 of those 32 keep the roster in a directory one of that card's paths is in — the correlated subset the triage asked any solution to use.

32 of 120 is a minority, not the whole or nearly the whole of the bucket, so the stop condition does not fire and the block is built and enumerates rather than only counting.

The 32, by command (--residue on PR #14866's diff, ⛔ marks the 5 correlated ones):

⛔ pnpm check:authz-resolver                 pnpm check:console-injection
   pnpm check:docs-image-tag                 pnpm check:engine-double-contract
⛔ pnpm check:error-code-casing              pnpm --filter @objectstack/spec run check:error-code-provenance
   pnpm check:error-status-conformance    ⛔ pnpm check:filter-alias-parity
   pnpm check:i18n-stale-fill                pnpm --filter @objectstack/spec run check:meta-url-spelling
   pnpm check:overlay-whitelist-table        pnpm check:partof-closing-keyword
   pnpm check:pm-governed-prose              pnpm check:pm-label-desc-cap
   pnpm check:pm-skill-ratchet               pnpm check:published-readme-exports
   pnpm --filter @objectstack/spec run check:react-blocks
   pnpm --filter @objectstack/spec run check:react-declaration-parity
   pnpm check:single-claim-paths             pnpm --filter @objectstack/spec run check:spec-changes
   pnpm check:stack-collection-maps       ⛔ pnpm check:swallow-census-controls
   pnpm check:widget-option-census        ⛔ node scripts/check-changeset-fixed.mjs
   node scripts/check-ci-filter-parity.mjs --self-test
   node scripts/check-partof-closing-keyword.mjs
   node scripts/check-published-list-mirrors.mjs --self-test
   node scripts/check-published-list-mirrors.mjs
   node scripts/check-single-claim-paths.mjs
   node scripts/check-skills-token-ratchet.mjs --self-test
   node scripts/check-skills-token-ratchet.mjs
   node scripts/pr-labels.mjs --self-test

Where the block goes, and what it refuses to say

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 in the default human run, directly under the reconciliation total (the placement is the claim: everything under that line is outside the answer), and on stderr in --commands, where every other accounting there goes. stdout keeps carrying commands and nothing else; a labelled block in that stream is prose for a harvest to pattern-match. --json gets the same rows under artifactRosterSilences, so no rendering is shorter than another.

The block is never counted among the derived families and never merged into the runnable list, and that is structural rather than a filter someone has to remember: roster families are silent, and commandsFor / familyReconciliation read only the matched, convention and always-runs rows.

⚠️ One deviation from the dispatch wording, declared. The dispatch named the block "N repo-wide scanner(s) … run them". The block does not use that label, because the tool cannot support it: 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 — artifactOnlyNote's own docblock carries the measurement that the two live side by side here (check:where-matcher names one baseline and walks packages/**; check-entry-guard named ten files under scripts/ and walked all of it). Calling all 32 scanners would be a fabricated lead over the members for which it is false, which this file's header prices as the expensive direction. The block states what is true of every member instead — their silence is a fact about a list, not about your paths; run them or read them, never read the silence as a clearance — marks the correlated subset ⛔, and points at the producer-side remedy the residue already carries: declare the scan surface beside the roster.

⚠️ And a boundary worth recording: check:optional-error-sink, the card's own headline instance, is in the UNDETERMINED bucket (it declares no path at all), not in SILENT. The measurement the dispatch scoped to the SILENT bucket therefore reaches mechanism 2 and not mechanism 1. The block closes the harvest gap for the artifact-roster class; the undetermined class remains what it was.


Two existing self-test cases repaired, both reported rather than rewritten

Both are cases whose stated invariant survives and whose implementation detail was the thing this card changes. Neither intent was rewritten.

  1. a check- script invoked with the flag stays ONE family under its bare path key (the dualWf fixture, [finding] dispatch-gates never discovers a gate whose script lacks a check- prefix - five required-lane steps, including the one that shipped a red on PR #11397, are absent from the family universe entirely #11404). It asserted two things at once: a count (one invocation, one family — the no-double-count property the check- skip protects) and a key (the bare path — the defect). Old: length === 1 && names[0] === 'scripts/check-adr-0087-registration.mjs'. New: the count half is asserted alone and unchanged, and the key expectation moves to 'scripts/check-adr-0087-registration.mjs --self-test', with two further cases pinning that it still resolves to the script FILE and is marked as a self-test invocation.

  2. promoting N module(s) to gate files subtracts no inherited hint from any other family. Its input was "files of self-test families", which after the split includes files that were already gate files (CI also runs them plainly), so it read a refusal that predates the self-test admission as a loss that admission caused — four false LOSTs (check-adr-links.mjs, check-self-test-workflow-commands.mjs and two more). Narrowed to the modules a self-test family is the only reason to treat as a gate file: 14 of them, subtracted: 0.

  3. because those families are in the MATCHED list instead, each one exactly once counted a substring, and check-empty-changeset is now legitimately two families. It counts per command now — no duplicate rows, and the plain command exactly once — which is the invariant it always protected.


Ablation

Run from the committed state, one mutation script with a trap … EXIT INT TERM restore and absolute paths. Both legs proved on disk, not by exit code.

Mutation leg — the derivation key put back on the script path (empty argv capture) and --residue put back outside the alternation. Anchors: each 1 hit(s) before, then deleted-text 0 and injected-text 1 for both. Worktree blob moved 9733568a…ea621093…, so the edit reached the disk.

Resultdispatch-gates self-test: 9 of 1307 case(s) failed (lock VERDICT command-exit 1), and the nine are exactly the cases about this change:

✗ ⭐ …and its key carries the flag, so the invocation CI runs is the one derived (#14880)
✗ …and it is marked as a self-test invocation, which is what the follow narrowings read
✗ ⭐ the two census invocations derive TWO entries, not one collapsed onto the plain run
✗ …and both print as commands a dev can paste, each reproducing the invocation CI runs
✗ a complete flag run followed by a redirection IS keyed — the redirection is the shell's, never this argv
✗ lint.yml really invokes 0 script(s) more than once under different argv, so the cases above judge a live convention
✗ ⭐ and the census pair CI runs on two lines derives as two families on the real workflow, not one
✗ ⭐ the usage line no longer presents --residue as combinable with --tier (#15036)
✗ …and it still offers --residue with the three modes it really does modify

The sixth line is worth reading twice: the live case prints the count it derived, and under ablation that count is 0, which is the proof the number is read from the workflow rather than typed.

Restore leggit checkout HEAD -- ABSOLUTE_PATH, then git hash-object back to 9733568a0a40d9e6ce87293e4addf7c3023aebd1 (equal to the HEAD blob), git diff HEAD empty, git status --porcelain empty. No rebuild leg exists: the self-test spawns the tool by path.

The commit-2 block has no separate ablation run; its refusal is pinned instead (⛔ a roster command is not in the runnable union, whatever the block prints, driven against the real commandsFor), and the live end-to-end case asserts on a real --commands run that not one block row appears in stdout.


Gates

Derived at 2412b019 with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set from the merge base itself). 24 commands: the 20 the dispatch named, plus three --self-test families this PR's own change created, plus node scripts/pm/bare-root-worklist.mjs --self-test, which the derivation added the moment that file entered the diff. Every exit code captured before any pipe (cmd > log 2>&1; EXIT=$?); each row quotes the gate's own verdict line.

command exit verdict line
node scripts/check-ci-filter-parity.mjs 0 OK: all 135 declared cross-package glob(s) (95 unique) are covered by core or crosspkg …
node scripts/check-closing-keyword-parity.mjs 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords …)
node scripts/check-closing-keyword-parity.mjs --self-test 0 ✓ check-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.
node scripts/check-comment-mask-corpus.mjs 0 ✓ comment-mask corpus sweep: 5845 files, 0 disagree, 0 unparseable, 47.9s
node scripts/check-cross-package-test-inputs.mjs 0 OK: 25 package(s) read outside themselves, all declared …
node scripts/check-self-test-wired.mjs 0 ✓ check-self-test-wired: every one of the 166 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-self-test-wired.mjs --self-test 0 check-self-test-wired --self-test: 4 live ledger row(s) verified … every battery at or above its pinned floor.
node scripts/check-shard-attestation.mjs 0 ✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-test-completeness.mjs 3 NOT MEASURED, in the gate's own words: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named. Its text names this exact situation: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … the local reading for this gate is NOT MEASURED. ⛔ It is not a red."
node scripts/check-whole-set-label-write.mjs 0 ✓ check-whole-set-label-write: 0 violations — 257 file(s) over 3 root(s) …
node scripts/check-whole-set-label-write.mjs --self-test 0 ✓ check-whole-set-label-write --self-test: all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)
node scripts/pm/bare-root-worklist.mjs --self-test 0 OK self-test: 66 live row(s), 58 unreachable as spelled, 58 recorded verdict(s) — none stale, none missing, none contradicted. 27 record(s) carry a spelling and every one of 14 distinct spelling(s) is pinned LIVE, PRECISE and COMPLETE …
pnpm check:agent-test-spelling 0 ✓ check-agent-test-spelling: 0 violations — 437 file(s) …
pnpm check:bash32-floor 0 ✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct …
pnpm check:cli-command-ids 0 ✓ check-cli-command-ids: 324 command-id literal(s) across 114 file(s) … all resolve
pnpm check:cross-package-test-inputs 0 OK: 25 package(s) read outside themselves, all declared …
pnpm check:declared-population-live 0 ✓ check:declared-population-live — 190 of 240 famil(ies) declare a path population, and every one of them reaches this tree's 8206 tracked file(s).
pnpm check:entry-guard 0 ✓ check:entry-guard: 206 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 153 export bindings, 153 of them inert on import
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 8199 text file(s) … no raw ASCII control bytes).
pnpm check:parse-guard 0 ✓ check:parse-guard: 205 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1319 cases pass. (through the shared verify lock: VERDICT command-exit 0 · held the lock 237s)
pnpm check:pnpm-filter-targets 0 ✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 33 file(s) resolve against 79 workspace package(s)
pnpm check:refd-timer-probe 0 OK check-refd-timer-probe: 5840 source file(s) swept; the process-global timer probe is read in … and nowhere else.
pnpm check:watch-hint-literal 0 ✓ check-watch-hint-literal: 48 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm lint (repo-wide ESLint, eslint . --no-inline-config) 0 exit 0 with no findings printed; through the lock, VERDICT command-exit 0 · held the lock 62s

check:declared-population-live is worth noting on its own: it reads this tool's own family count and reports 240 families, all of whose declared populations still reach the tree — the split did not invent a family with an unreachable population.

The script's own test suite is its --self-test (pnpm check:pm-dispatch-gates, above); git grep finds no vitest suite that exercises scripts/pm/dispatch-gates.mjs — the three .test.ts files naming it do so in prose.

skip-changeset: nothing is published from any released package — the diff is one file under scripts/pm/.


Patch round — the coupling this PR's own change created, and the file it added

CI's Lint & Repo Gates went red at PM bare-root worklist self-test on the first head, and it is this PR's own coupling rather than a flake:

x self-test: no gate has NEWLY joined the invisible bare-root species — FRESH:
  scripts/check-adr-0087-registration.mjs --self-test PACKAGE_ROOTS apps · … · 9 rows

bare-root-worklist keys each row on family constant word. Splitting the derivation key gives a gate CI invokes both plainly and with --self-test two family keys, so the identical bare-root literal — identical file, identical constant, identical root — is reached a second time and lands as a FRESH, untriaged row. Nine of them, with no gate having changed.

Followed the remedy that self-test itself names for a FRESH row — record a verdict — which is a table inside scripts/pm/bare-root-worklist.mjs, so that one file joins the file surface and nothing else does. The nine records sit in their own labelled section. Each carries the verdict (and, where the verdict requires one, the spelling) of the row already deciding the identical literal, because a different verdict on one population would have the map assert two decisions about it.

No count is restated in any of the nine. The map's docblock bans carrying a sibling's numbers into a new row, on the grounds that two rows are normally two populations measured at two times. Here they are one population read through two keys, so restating a figure would mint a reading this pass never took — the same defect the ban is written against, arriving by the one route its wording does not cover. Each why says that in as many words instead.

⚠️ Recorded in the section, for whoever owns the call: the class grows with the workflows, since any gate CI starts invoking a second way acquires a twin row on the next run. The structural alternative is real — key the sweep's dedupe on the gate source file rather than on the family, since a verdict is about a literal in a file and never about an invocation — and it is deliberately not taken here: it re-decides which family a surviving row is attributed to and would strand the existing twin as STALE, which is a redesign of that file's keying and not what the FRESH remedy names.

The --self-test is untouched: no case weakened, no case skipped, and it is now in this card's own derived gate list (row above), green at the new head.


Not a governed surface — re-run on the final two-file list: node scripts/pm/check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs scripts/pm/bare-root-worklist.mjs0 of 2 path(s) hit the register. No .md in the diff, so this stays a draft for the seat's contract-tier review.


Generated by Claude Code

…cript path

`dispatch-gates` keyed a discovered family on the script PATH, so CI's two
invocations of one script collapsed into a single entry — and the entry kept
was the plain one, because the direct matcher captured the path and dropped the
argument tail. Measured on PR #14958: `lint.yml` runs
`node scripts/check-tenant-audit-census.mjs --self-test` beside the plain run,
the red was carried entirely by the `--self-test` invocation, and the derived
list named only the invocation that was already green. A dev following the list
verbatim could not see the failure mode at all.

The key is now (script, args). The argv half is admitted only when this tool can
render the invocation runnably — a complete run of flag-shaped tokens; a tail
carrying a value, or continued onto the next line, keeps the bare path key it
has today rather than being truncated into a command that cannot run.

Read from this tree's workflow text: 28 scripts in `lint.yml` are invoked more
than once under different argv, 41 across all workflow files, and every one of
them carried a `check-` basename and so collapsed. 204 discovered families
become 242; three `check-` scripts CI never invokes plainly lose a bare key
that named a command nobody runs.

Also: a `--self-test` invocation is never CI-measured-only. The payload-access
read that classification rests on is taken from the gate's work body, which a
self-test run does not execute, so suppressing it from `--commands` would hide
a command a dev can run.

And the usage line printed on the derivation-failure path moves `--residue`
inside the alternation, so it no longer advertises the `--tier --residue` pair
the CLI has refused since #14753.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…side 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
…owes

The (script, args) split gives a gate CI invokes both plainly and with
`--self-test` two family keys, and `bare-root-worklist`'s rows are keyed on
`family constant word` — so nine rows landed FRESH in one edit without any gate
changing. Its self-test says so and names the remedy for a FRESH row: record a
verdict.

Recorded, one per new key, in a labelled section of their own. Each is the
twin of a row already carrying a decision about the identical literal, in the
identical file, under the identical constant, so the verdict and (where the
verdict requires one) the spelling transfer whole. No count is restated: the
map's own docblock bans carrying a sibling's numbers into a new row because two
rows are normally two populations measured at two times, and here they are one
population read through two keys — restating a figure would mint a reading this
pass never took.

The section also records the structural alternative and why it was not taken
here: keying the sweep's dedupe on the gate SOURCE FILE rather than on the
family would fold the twins, but it re-decides which family a surviving row is
attributed to and would strand the existing row as STALE — a redesign of this
file's keying, which is not what the FRESH remedy names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant