Uh oh!
There was an error while loading. Please reload this page.
tooling(pm): render a value-bearing CI invocation instead of falling back to a bare key - #15114
Conversation
…back to a bare key
`dispatch-gates` keyed the argv half of `(script, args)` only when the tail was
a complete run of flag-shaped tokens. Every other invocation kept a BARE path
key — and for nine scripts that bare key is an invocation CI never makes.
Measured on this tree: `node scripts/check-test-completeness.mjs` exits 3 with
`PREREQUISITE NOT MET`; `check-required-contexts` runs the static pin check
while `required-set-patrol.yml` runs `--verify-required-set`; the three
`--base` gates answer against their default base while CI pins the merge base
or the cut snapshot.
Each argv token is now classified from the workflow text as a LITERAL (verbatim
in the workflow, same on every run) or a VARIABLE (`${{ … }}`, a shell
expansion, a step output). An all-literal invocation renders in full and enters
`--commands`; one carrying a variable renders with the variable's own name in
the value position, is marked NOT RUNNABLE LOCALLY on its own labelled line
under its own heading, and is kept out of `--commands`, out of the pasted block
and out of the published harvest — the same treatment `ciOnly` already gets,
reached by a different measurement. No per-script declaration table: the split
reads off the workflow for all fifteen live invocations.
Two mechanisms make the classification possible. `joinLineContinuations`
splices a continued command into one line before any matcher runs, so the tail
is the WHOLE argv rather than the first physical line of it (comments are never
joined — a `#` runs to the newline, so its backslash is comment text).
`tailBeforeRedirection` drops a redirection's file descriptor, which `2>&1` had
been leaving on the argv side of the boundary.
Measured, before -> after over the tracked tree:
check families discovered 240 -> 249 (+9, ZERO lost)
watch-hint (gate, file) pairs 1499 -> 1528 (+29, ZERO lost)
re-attributed 0 — every surviving family keeps its exact
workflow set and hint count
Beyond the nine the card named, the same sweep at this commit finds
`scripts/pm/check-half-states.mjs` (a tenth member: `half-state-patrol.yml`
runs it `--format=markdown --provenance="$PROVENANCE"` and nowhere else), and
`check-release-section-coverage.mjs --strict` — a second invocation in
`release-coverage-patrol.yml` that had no entry of its own because a
continuation hid it.
Self-test: 1338 cases pass. Two cases that pinned the retired refusal are
rewritten on the same fixtures to the new expectation; four that named a bare
key now name the script or the keyed invocation. New cases pin one literal
kind, one variable kind, the bare-invocation control, the tokenizer, the
continuation join and the live class read from the real workflows.
`scripts/pm/bare-root-worklist.mjs` records the verdict rows this re-keying
owes, per that script's own printed remedy: three rows for the retired bare
`check-adr-0087-registration` key are deleted and six second-key twins recorded
in their place, one per workflow invocation. Verdict and spelling carried
whole; no count restated, because this pass measured none.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfoxSide effect worth a triage read: the standing finding in #14899That card measures This PR's classifier reaches it as the tenth member of the value-bearing class, so the behaviour that card measures changes. Measured on this branch at and in the human rendering it now appears once, under the new heading, as what Two boundaries on that claim, stated so nothing here reads as more verified than it is:
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#15083
dispatch-gateskeys the derivation on(script, args)and, since PR #15081, admitted the argv half only when the tail was a complete run of flag-shaped tokens. Nine scripts whose only CI invocations carry a value or a line continuation therefore kept a bare path key CI never runs bare. This renders those invocations instead, classifying each argv token from the workflow text.Verification head sha:
e98bfc87. Every gate below and every number in this body was run or read at that commit.1. The measurement — every CI invocation of the nine, token by token
Read from
.github/workflows/*.ymlatorigin/main(369da918). A literal appears verbatim in the workflow and is the same on every run; a variable is a GitHub expression, a shell expansion, a step output, or a path an earlier step produced.check-adr-0087-registrationpr-automation.yml· "Require an ADR-0087 disposition…"--self-testpr-automation.yml· same step--base "$MERGE_BASE"$MERGE_BASE(env fromsteps.diffbase.outputs.merge_base)cut-rc.yml· "Gate — this cut introduces no unregistered breaking change"--self-testcut-rc.yml· same step--base "$SNAPSHOT_SHA"$SNAPSHOT_SHAcheck-changeset-no-majorpr-automation.yml· "…no major bump" (one-liner)--base "$MERGE_BASE"$MERGE_BASEcut-rc.yml· "Gate — this cut introduces no major bump"--self-testcut-rc.yml· same step--base "$SNAPSHOT_SHA"$SNAPSHOT_SHAcheck-empty-changesetpr-automation.yml· "…no empty-frontmatter changeset"--self-testpr-automation.yml· same step--base "$MERGE_BASE"$MERGE_BASEcut-rc.yml· "Gate — this cut introduces no empty-frontmatter changeset"--self-testcut-rc.yml· same step--base "$SNAPSHOT_SHA"$SNAPSHOT_SHAcheck-engine-split-ratiolint.yml· shallow-history guard collector--self-testengine-split-metric.yml· "Compute ratio (90-day window)"--days 9090written in the workflowcheck-test-completenessci.yml· Test Core "Test completeness guard""$RUNNER_TEMP/test-core.log" --scheduled "$RUNNER_TEMP/shard-packages.txt" --package-list "$RUNNER_TEMP/turbo-ls.json"(continued over 3 lines)$RUNNER_TEMPci.yml· Dogfood "Test completeness guard""$RUNNER_TEMP/dogfood.log"$RUNNER_TEMPcheck-shard-attestationci.yml·test"Attest this shard ran and passed"--emit --job test --shard ${{ matrix.shard }} --total 6 --out "$RUNNER_TEMP/shard-attestation"${{ matrix.shard }},$RUNNER_TEMP(test,6are literals)ci.yml·dogfood"Attest this shard ran and passed"--emit --job dogfood --shard ${{ matrix.shard }} --total 3 --out "$RUNNER_TEMP/shard-attestation"${{ matrix.shard }},$RUNNER_TEMPci.yml·dogfood-verify"Attest this leg ran and passed"--emit --job dogfood-verify --shard 1 --total 1 --out "$RUNNER_TEMP/shard-attestation"$RUNNER_TEMPonly (1,1are literals)ci.yml·test-gate"Verify test shard results"--verify --gate 'Test Core' --dir "$OS_ATTEST_DIR" --filter-result "$OS_FILTER_RESULT" --download-outcome "$OS_DOWNLOAD_OUTCOME" --leg "test/6:$OS_TEST_RESULT"'Test Core'is a literal)ci.yml·dogfood-gate"Verify dogfood shard results"--verify --gate 'Dogfood Regression Gate' --dir … --leg "dogfood/3:$OS_DOGFOOD_RESULT" --leg "dogfood-verify/1:$OS_VERIFY_RESULT"check-required-contextsrequired-set-patrol.yml· "Run the live required-set sweep"--verify-required-set(continued, then two redirections)check-cross-package-test-inputsci.yml·filterjob, affected-set union--union-into "$RUNNER_TEMP/turbo-ls.json" --changed "$RUNNER_TEMP/changed-files.txt"(continued over 3 lines)$RUNNER_TEMPcheck-prerelease-pin-watchprerelease-pin-watch.yml· "Self-test the probe"--self-testprerelease-pin-watch.yml· "Probe npm for a stable release"--verbosethen2redirected to12was the redirection's own file descriptorThree of the nine are not value-bearing at all.
check-engine-split-ratio --days 90writes its value down;check-required-contexts --verify-required-setandcheck-prerelease-pin-watch --verbosehave complete argv that were refused only because a continuation and a redirection file descriptor stood behind them. Those three now render as runnable commands.The split reads off the workflow text for all fifteen invocations, so the triage's ⛔ holds: no per-script declaration table is owed.
Two members the card's table does not name
Re-measured at
origin/mainrather than taken from the card:scripts/pm/check-half-states.mjsis a tenth member of the same class.half-state-patrol.ymlruns it--format=markdown --provenance="$PROVENANCE"(continued, then redirected) and nowhere else, wherePROVENANCEis a workflow env built fromgithub.run_id/github.sha/github.event_name. It derived under a bare key too.check-release-section-coverage.mjs --strictis a second invocation inrelease-coverage-patrol.ymlthat had no entry of its own — a continuation hid it, and the script's bare key (whichlint.ymlreally does run) absorbed it. It is now its own runnable family, and the bare key stays where CI runs it bare.What the bare keys actually did, run at
origin/mainThis is why the bare key is not a neutral omission:
node scripts/check-test-completeness.mjsPREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named.node scripts/check-required-contexts.mjsrequired-set-patrol.ymlruns the live--verify-required-setsweepnode scripts/check-shard-attestation.mjspnpm check:shard-attestation(a separate family) already runs; ci.yml's--emit/--verifyruns are a different programnode scripts/check-cross-package-test-inputs.mjspnpm check:cross-package-test-inputsalready covers it--basegatesnode scripts/check-engine-split-ratio.mjsengine-split-metric.ymlchecks out withfetch-depth: 02. What changed in
scripts/pm/dispatch-gates.mjsjoinLineContinuationssplices a continued command into one line before any matcher runs, so the captured tail is the whole argv rather than the first physical line of it. ⛔ A comment line is never joined — a#comment runs to the newline, so its trailing backslash is comment text (measured: 107 continuation lines insiderun:bodies on this tree, zero of them comments).tailBeforeRedirectiondrops a redirection's file descriptor.DIRECT_CHECK_INVOCATIONcorrectly stops the tail before the redirection operator, but2redirected to1puts its fd on the argv side of that boundary, so--verbosecaptured as--verbose 2. Discriminator is the shell's own: digits touching the operator are a descriptor, digits with a space before it are an argument.argvTokensis a quote-aware tokenizer that also holds a${{ … }}expression together — both spellings are live inci.yml(--gate 'Test Core',--shard ${{ matrix.shard }}) and a bare whitespace split would classify their pieces separately.renderedArgvreplacesrenderableArgv. It renders every tail and returns the workflow values it carries, so the caller decides what to do rather than the classifier refusing.entry.notRunnableis the second not-runnable-here classification besideentry.ciOnly, and is deliberately kept separate from it: one family cannot run here because it needs the workflow event payload, the other because it needs a workflow value, and folding them would print one count for two omissions.Value-bearing argv — matched by path, and NOT runnable hereheading in the human output (below the blank line the published harvest stops at, so it cannot reach a harvest of the pasted block), anotRunnablefield on the--jsonmatched row, a stated omission on--commandsstderr, and its own term in the reconciliation.The usage line needed nothing.
--commandspromises "one runnable command per line on stdout, nothing else", and that promise is now more true, not less — the entries this adds are the ones it must not carry. PR #15105's--changed --commandsline is untouched.3. The nine entries, before and after
Read from
discoverFamilies()at369da918and ate98bfc87.scripts/check-adr-0087-registration.mjs… --base "$MERGE_BASE"[pr-automation.yml] NOT RUNNABLE ·… --base "$SNAPSHOT_SHA"[cut-rc.yml] NOT RUNNABLEscripts/check-changeset-no-major.mjs… --base "$MERGE_BASE"NOT RUNNABLE ·… --base "$SNAPSHOT_SHA"NOT RUNNABLEscripts/check-empty-changeset.mjs… --base "$MERGE_BASE"NOT RUNNABLE ·… --base "$SNAPSHOT_SHA"NOT RUNNABLEscripts/check-engine-split-ratio.mjsnode scripts/check-engine-split-ratio.mjs --days 90— runnable, in--commandsscripts/check-test-completeness.mjs… "$RUNNER_TEMP/test-core.log" --scheduled … --package-list …NOT RUNNABLE ·… "$RUNNER_TEMP/dogfood.log"NOT RUNNABLEscripts/check-shard-attestation.mjspnpm check:shard-attestationin lint.yml is a separate family and stays runnable)scripts/check-required-contexts.mjsnode scripts/check-required-contexts.mjs --verify-required-set— runnable, in--commandsscripts/check-cross-package-test-inputs.mjs… --union-into "$RUNNER_TEMP/turbo-ls.json" --changed "$RUNNER_TEMP/changed-files.txt"NOT RUNNABLE (pnpm check:cross-package-test-inputsstays runnable)scripts/check-prerelease-pin-watch.mjsnode scripts/check-prerelease-pin-watch.mjs --verbose— runnable, in--commandsscripts/pm/check-half-states.mjs(tenth member)… --format=markdown --provenance="$PROVENANCE"NOT RUNNABLEscripts/check-release-section-coverage.mjs(bare; the--strictrun had no entry)… --strict— runnable, in--commandsThe price, measured the way PR #15081 priced its own
Over the tracked tree, before to after:
Verified as a set difference, not as a count: the only keys removed are the ten bare ones above, and every key added is one of the rendered invocations.
Live rendering,
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack .github/workflows/ci.ymlBefore, in the pasted block (four commands CI never runs):
After — one of them promoted into the pasted block as the invocation CI really runs, the other eight under their own heading:
--commandson those paths: 37 lines on stdout, zero containing a workflow variable, with the 8 stated on stderr.--jsoncarriesnotRunnableon all 8 matched rows and none of their commands appear incommands.4. Self-test — 1338 cases pass
Cases added (all PASS):
pr-automation.yml$MERGE_BASEreaches the row label${{ matrix.shard }}and$RUNNER_TEMP--days 90,--verify-required-set,--verbose--commands${{ … }}expression togethercheck-release-section-coverageas the live specimenCases rewritten — six, and each is named because "existing cases keep their verdicts" deserves an honest accounting. All six still PASS and each keeps its subject:
an invocation whose tail carries a VALUE keeps the bare path key,an invocation CONTINUED onto the next line keeps the bare key). Same fixture, same tail — the expectation moved from "keeps the bare key" to "renders in full and says whether it is runnable", because that bare key is the defect.renderableArgv keeps a complete flag run and refuses everything elsebecamerenderedArgv renders every tail and reports which values come from the workflow— the function it pins was replaced.every derived key is either the bare script path or that path plus a complete flag runbecame…plus the WHOLE argv, re-tokenising to itself— the same "never a truncated argv" property, restated for a renderer.the live Check Changeset job discovers its ADR-0087 gateand its two siblings) asserted discovery by bare key; they now assert it by script, which is what their own comment says they are about, with a new case beside them pinning the keyed half so a rewrite of the step cannot satisfy them vacuously.node scripts/check-empty-changeset.mjsexactly once; it now countsnode scripts/check-empty-changeset.mjs --base "$MERGE_BASE", which is the family that section is about.Cases untouched and still passing: everything else, including the two census cases PR #15081 added and the redirection case (
a complete flag run followed by a redirection IS keyed).5.
scripts/pm/bare-root-worklist.mjs— the pre-authorised second filenode scripts/pm/bare-root-worklist.mjs --self-testreddened exactly as the dispatch anticipated, with 3 STALE and 6 FRESH rows, all forcheck-adr-0087-registration's re-keying. Its own printed remedy was followed and nothing else in the file was touched: the three rows keyed on the retired bare path are deleted, and six second-key twins recorded in their place (one per workflow invocation), verdictSPELLABLE-UNDECLAREDand spelling carried whole from the retired row, with no count restated — this pass measured none, which is that file's own rule.Green after:
OK self-test: 69 live row(s), 61 unreachable as spelled, 61 recorded verdict(s) — none stale, none missing, none contradicted.6. Gates — every one derived for this diff, at
e98bfc87Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) after the commit. Exit code captured by redirecting first, never across a pipe; each row quotes the gate's own verdict line.node scripts/check-ci-filter-parity.mjsOK: all 143 declared cross-package glob(s) (99 unique) are covered by core or crosspkg…node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords…, all registered).node scripts/check-closing-keyword-parity.mjs --self-test✓ 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✓ comment-mask corpus sweep: 5853 files, 0 disagree, 0 unparseable, 44.4snode scripts/check-self-test-wired.mjs✓ 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-testcheck-self-test-wired --self-test: 4 live ledger row(s) verified… every battery at or above its pinned floor.node scripts/check-whole-set-label-write.mjs✓ check-whole-set-label-write: 0 violations — 257 file(s) over 3 root(s)…node scripts/check-whole-set-label-write.mjs --self-test✓ 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-testOK self-test: 69 live row(s)… none stale, none missing, none contradicted.pnpm check:agent-test-spelling✓ check-agent-test-spelling: 0 violations — 437 file(s)…pnpm check:bash32-floor✓ check-bash32-floor: 26 tracked shell file(s)… name no bash 4+ construct…pnpm check:cli-command-ids✓ check-cli-command-ids: 324 command-id literal(s) across 114 file(s)… all resolvepnpm check:cross-package-test-inputsOK: 26 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.pnpm check:declared-population-live✓ check:declared-population-live — 198 of 249 famil(ies) declare a path population, and every one of them reaches this tree's 8230 tracked file(s).pnpm check:entry-guard✓ check:entry-guard: 206 scripts/ file(s) — every entry guard goes through invoked-as.mjs…pnpm check:nul-bytescheck-nul-bytes: OK (scanned 8223 text file(s)… no raw ASCII control bytes).pnpm check:parse-guard✓ check:parse-guard: 205 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 1338 cases pass.pnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 33 file(s) resolve…pnpm check:refd-timer-probeOK check-refd-timer-probe: 5848 source file(s) swept…pnpm check:watch-hint-literal✓ check-watch-hint-literal: 48 declaration(s) across 4 rostered name(s)… no unrostered spelling of the idiom in the tree.pnpm lint(repo-wideeslint . --no-inline-config)The two long runs went through
scripts/pm/os-verify-lock.sh:pm-dispatch-gatesVERDICT command-exit 0 · held the lock 250s,lintVERDICT command-exit 0 · held the lock 60s.Two notes on scope of measurement, so nothing reads as verified that is not:
check-test-completenessis NOT MEASURED here, in its own words — it is not in this diff's derived gate list, and a bare run of it printsPREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named.(exit 3). That refusal is now the reason it leaves--commands, not a gate this PR skipped.git grepfordispatch-gates/bare-root-worklistacross*.test.*returns five files, and all five hits are prose comments citingdispatch-gates.mjs's own self-test as the thing that pins their property. That self-test ispnpm check:pm-dispatch-gatesabove.7. The design call, on the four axes
The triage left one design choice open: how to render an invocation whose values are workflow variables. Options were (A) classify each token from the workflow text — literals render, variables render with the variable named and the entry marked not runnable; (B) a per-script declaration table of which flags are value-bearing; (C) keep the bare key.
PREREQUISITE NOT MET, exit 3), four run a different program than CI runs, three answer against a different base. The consumer is real — the PM's dispatch brief and--ranboth read this list. (C) keeps a measured defect; (B) buys nothing (A) does not, at the cost of a declaration surface.--commandswith the variable unset. It runs, and it answers a question CI never asked — the tolerant-fallback shape, one layer up. (A) refuses loudly instead: the entry is named, its variables are named, and it is kept out of the list whose caption promises runnable. A missing lead, never a fabricated one.ciOnlychannel's exact shape for a second measurement. (B) would have added a per-script table that grows with the workflow corpus forever.Recommendation taken: (A). The triage's ⛔ on (B) is discharged by the measurement in section 1 rather than by assertion — every one of the fifteen invocations classifies from the workflow text alone.
Bounded scope
scripts/pm/dispatch-gates.mjsplus the pre-authorised verdict rows inscripts/pm/bare-root-worklist.mjs. No workflow edits, no.md, no other script.skip-changeset: nothing is published from these paths. Not a governed surface — draft PR for the seat's contract-tier review.Generated by Claude Code