Skip to content

fix(devx): dispatch-gates emits the REAL check where the script declares its argv default, and its self-test harness prints the readings its call sites pass - #15570

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15441-not-runnable-defaults
Sep 4, 2026
Merged

fix(devx): dispatch-gates emits the REAL check where the script declares its argv default, and its self-test harness prints the readings its call sites pass#15570
baozhoutao merged 1 commit into
mainfrom
claude/issue-15441-not-runnable-defaults

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15441
Fixes #15539

dispatch-gates marked a gate NOT RUNNABLE LOCALLY whenever its workflow argv
mentioned a value the workflow supplies. It never asked whether the script
supplies a default for that argument. For the three --base gates the two
answers differ, so the only member --commands offered for
check-adr-0087-registration was that script's --self-test — a zero from a
command that cannot answer the question, sitting in the runnable union where
nothing distinguishes it from a green. A dev reported "50 run · 50 exit 0 · 0
red" on a PR whose Check Changeset job was red in CI the whole time.

Alongside it, the self-test harness t was declared with arity two while six
call sites passed a third argument carrying each case's diagnostic reading.
JavaScript dropped all six.

The class, enumerated by predicate before anything was repaired

Read from the workflow text with the tool's own extractCheckInvocations, over
every file in .github/workflows — not from memory, and not from the card's
table. N = 15 value-bearing invocation keys across 7 scripts.

M = 6 keys / 3 scripts where the SCRIPT declares a default for the flag the
workflow pins
(declaration line, then the line that implements it):

key workflow variable declared implemented
check-adr-0087-registration.mjs --base "$MERGE_BASE" pr-automation.yml $MERGE_BASE scripts/check-adr-0087-registration.mjs:8 :5141
check-adr-0087-registration.mjs --base "$SNAPSHOT_SHA" cut-rc.yml $SNAPSHOT_SHA same same
check-changeset-no-major.mjs --base "$MERGE_BASE" pr-automation.yml $MERGE_BASE scripts/check-changeset-no-major.mjs:7 :760
check-changeset-no-major.mjs --base "$SNAPSHOT_SHA" cut-rc.yml $SNAPSHOT_SHA same same
check-empty-changeset.mjs --base "$MERGE_BASE" pr-automation.yml $MERGE_BASE scripts/check-empty-changeset.mjs:7 :1627
check-empty-changeset.mjs --base "$SNAPSHOT_SHA" cut-rc.yml $SNAPSHOT_SHA same same

All three declare it in the same spelling: # base defaults to origin/main. All
three scan from merge-base(base, head) either way, which is why the default is
the same reading as the merge base the workflow computes —
check-empty-changeset.mjs:125 states the idempotence: "merge-base(X, head) is
X again whenever X is already the branch point".

K = 9 keys / 4 scripts genuinely unrunnable — the value has no default and
no local equivalent, so nothing here can invent one:

key workflow variables
check-cross-package-test-inputs.mjs --union-into … --changed … ci.yml $RUNNER_TEMP
check-shard-attestation.mjs --emit --job dogfood … ci.yml $RUNNER_TEMP
check-shard-attestation.mjs --emit --job dogfood-verify … ci.yml $RUNNER_TEMP
check-shard-attestation.mjs --emit --job test … ci.yml ${{ matrix.shard }}, $RUNNER_TEMP
check-shard-attestation.mjs --verify --gate 'Dogfood Regression Gate' … ci.yml $OS_ATTEST_DIR, $OS_FILTER_RESULT, $OS_DOWNLOAD_OUTCOME, $OS_DOGFOOD_RESULT, $OS_VERIFY_RESULT
check-shard-attestation.mjs --verify --gate 'Test Core' … ci.yml $OS_ATTEST_DIR, $OS_FILTER_RESULT, $OS_DOWNLOAD_OUTCOME, $OS_TEST_RESULT
check-test-completeness.mjs "$RUNNER_TEMP/dogfood.log" ci.yml $RUNNER_TEMP
check-test-completeness.mjs "$RUNNER_TEMP/test-core.log" … ci.yml $RUNNER_TEMP
check-half-states.mjs --format=markdown --provenance="$PROVENANCE" half-state-patrol.yml $PROVENANCE

6 + 9 = 15, and the split is re-asserted as a PROPERTY on the live tree by the
new cases (every repaired family reads its default off its own script, flag by
flag; every family still filed value-bearing is one whose script declares no
default for what the workflow pins) rather than as a roster typed into a test.

What changed

  1. declaredArgvDefaults(source) reads the defaults a script declares in its
    own leading usage block
    , keyed on the spelling those scripts already carry
    (<flag> defaults to <value>), and only for a flag the usage block also
    names as --flag. Reading the SCRIPT rather than a per-family table is the
    ruling's preferred direction and it cannot drift: a script that stops
    documenting the default returns its family to NOT RUNNABLE LOCALLY on the
    next run with no edit here — pinned as a case, driven on the real specimen
    source with the declaration line deleted. leadingCommentBlock is what makes
    it a declaration rather than a coincidence: over the seven scripts in the
    class the phrase appears five times and only three are declarations (one is
    an assertion message about wiring, two are docblocks deep in another file).
  2. defaultedArgv(args, defaults) renders the invocation as it runs here, with
    the default spelled out where CI writes the variable. All-or-nothing: a
    partially covered argv stays value-bearing, because a command that runs with
    one real value and one invented one answers a question CI never asked.
  3. ⛔ The derivation key is untouchedcheck stays the invocation CI
    runs, so a script CI invokes under two different variables keeps its two
    entries and nothing is re-attributed (the [finding] dispatch-gates --commands does not derive check:optional-error-sink for a diff that adds a new logger-sink interface #14880 invariant). Only what the
    row RENDERS moves, through a new localCheck read by runnableInvocation.
  4. The --self-test line is no longer a substitute for the question anywhere:
    the real check is emitted beside it, and every family that genuinely cannot
    run now prints ⊘ NOT MEASURED — <the invocation CI runs> in the human
    rendering and one ⊘ NOT MEASURED line per family on --commands stderr,
    with a line saying that a sibling invocation of the same script in the list
    above grades the script and not your diff.
  5. Two matched rows can now render one command (a script CI runs under two
    variables). The pasted block is deduplicated on the command, merging both
    rows' workflows, provenance and filled values — familyReconciliation has
    always counted DISTINCT commands, so this is also what keeps the block's
    footer and the reconciliation agreeing, and it keeps the published harvest
    free of repeats.
  6. [finding] dispatch-gates' self-test harness t takes two arguments, but six call sites pass a third — every one of those cases' diagnostic readings is silently dropped #15539: t takes the third parameter its six call sites were already
    passing. The line is rendered by a pure selfTestCaseLines, so both
    directions are pinned by cases instead of by a run that would have to fail;
    the reading prints on a RED only, so a passing run's log is byte-identical to
    what it has always been. A tail case counts how many cases really handed the
    harness a reading (floor 6), so the repair cannot go vacuous. t gained a
    parameter; ⛔ no existing case's wording or expectation was changed.

selfTestCaseLines is a production name the self-test anchor fires on, so it is
classified in COMPOUND_ANCHOR_LEDGER as accidental, per that table's own
instruction not to dodge the anchor with a rename.

Verification

Every exit code captured by redirect-then-read, never across a pipe.

command exit printed verdict
pnpm -s check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1445 cases pass.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- .changeset/some-file.md 0 stdout now carries node scripts/check-adr-0087-registration.mjs --base origin/main (plus the two sibling --base gates); stderr dispatch-gates --commands: 19 command(s) — 7 pnpm, 12 direct node (15 matched by path, …) and ⊘ NOT MEASURED — scripts/pm/check-half-states.mjs --format=markdown --provenance="$PROVENANCE". On origin/main the same run printed 16 command(s) — 7 pnpm, 9 direct node (9 matched by path, …) and + 7 famil(ies) … take a VALUE FROM THE WORKFLOW, with --self-test as the only ADR-0087 member.
pnpm check:declared-population-live 0 ✓ check:declared-population-live — 209 of 256 famil(ies) declare a path population, and every one of them reaches this tree's 7533 tracked file(s).
pnpm check:watch-hint-literal 0 ✓ check-watch-hint-literal: 55 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 7526 text file(s) … no raw ASCII control bytes).
pnpm lint (eslint . --no-inline-config, whole repo, no narrowing) 0 no output

All of the above ran on the tree of the final commit, and the union was re-run
on it after committing: git rev-parse --short HEAD = 379179037, on which
pnpm -s check:pm-dispatch-gates prints ✓ dispatch-gates self-test: 1445 cases pass. and exits 0 (case count 1415 -> 1445, +30; ⛔ no existing case was
edited to pass — one case's EXPECTATION moved with the rendering this card
deliberately changes, its subject and invariant unchanged, and the reason is
written at the case).

The specimen, re-measured with the EMITTED command

node scripts/check-adr-0087-registration.mjs --base origin/main --head <sha>
the exact command the derivation now emits, plus the --head the card used.

head what it carries exit
d5aeb0fce — PR #15432's head TODAY (the card measured 393b2173c) **BREAKING** with a valid adr-0087: disposition 0
46b06feec — PR #15434, the sha the card recorded **BREAKING** with a marker 0
13b58ed7d — PR #15436, the sha the card recorded declares no BREAKING 0

⚠️ The card's red leg is gone from those heads: PR #15432's head moved on
after the card was filed and now carries the disposition it was missing, so
today all three of its heads answer 0. A measurement whose every leg returns the
same value is not yet a reading, so the red leg was rebuilt as a control, on
this tree, with git commit-tree against origin/main — no ref, index or
working tree touched:

head what it carries exit printed verdict
adf6a8914 **BREAKING**, no adr-0087: marker 1 ::error file=.changeset/adr-0087-probe-red.md::… declares a breaking change with no valid ADR-0087 disposition
5badb1cf8 the same changeset with a marker 0 ✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.

Two legs, one differing byte, both directions — so the emitted command is a
reading of the tree and not a command that always answers the same way.

The derived family for THIS change set

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack
(exit 0) derived 24 commands. Every one was run; all exit 0. Beyond the four
above: check-reference-carrier-shape.mjs (and --self-test),
check-ci-filter-parity.mjs, check-closing-keyword-parity.mjs (and
--self-test), check-comment-mask-corpus.mjs, check-self-test-wired.mjs
(and --self-test), check-whole-set-label-write.mjs (and --self-test),
check:agent-test-spelling, check:bash32-floor, check:cli-command-ids,
check:cross-package-test-inputs, check:driver-memory-census,
check:entry-guard, check:parse-guard, check:pnpm-filter-targets,
check:ratchet-remedy-authority, check:refd-timer-probe. ⛔ Nothing exited 3,
nothing printed PREREQUISITE NOT MET, nothing raised ERR_MODULE_NOT_FOUND
there is no NOT MEASURED entry in this list.

⚠️ The derivation reported STALE TREE — 3 commit(s) behind origin/main
(branch point bbf4a5229, origin/main now cf6b67164). Of the six files it
named, only scripts/pm/check-half-states.mjs and the new
.github/workflows/os-create-smoke.yml touch this card's subject, and both were
checked against the newer copies: the new workflow contributes zero check
invocations, and the newer check-half-states.mjs still declares no argv
default, so the census above and the live cases hold on the merged tree too.

The one member this PR does not decide

cut-rc.yml pins --base "$SNAPSHOT_SHA" (a git rev-parse HEAD of the cut
snapshot, cut-rc.yml:213), and that lane is workflow_dispatch-only. The
script's declared default is origin/main, which is NOT what a release cut
means by "base" — so for those three keys the emitted command answers the gate's
question against a different base than that workflow asks it against. It is
reported rather than decided, and it is reported with the measurement that makes
it decidable: the emitted command for a cut-rc key is byte-identical to the
one its pr-automation sibling already emits, so --commands is unchanged
whichever way those three rows are labelled. That stops being true the day a
defaulted-flag script is invoked ONLY from a dispatch-only lane. See the
open_questions entry in the report on #15441.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…res its argv default, and its self-test harness prints the readings its call sites pass

The `notRunnable` rule asked whether the WORKFLOW supplies a value and never
whether the SCRIPT needs it supplied. For the three `--base` gates the two
answers differ, so the only member `--commands` offered for
check-adr-0087-registration was that script's `--self-test`: a zero from a
command that cannot answer the question, sitting in the runnable union where
nothing distinguishes it from a green.

The class was enumerated by predicate before anything was repaired, read from
the workflow text rather than recalled — 15 value-bearing invocation keys across
7 scripts. 6 keys / 3 scripts declare a default for the flag CI pins
(check-adr-0087-registration.mjs:8, check-changeset-no-major.mjs:7,
check-empty-changeset.mjs:7, each implemented at :5141 / :760 / :1627). 9 keys /
4 scripts are genuinely unrunnable: a runner temp path, a matrix shard, step
results and a provenance string this tool cannot invent.

- declaredArgvDefaults reads the declaration out of the script's own LEADING
  usage block, so no table here can drift from it; a script that stops
  documenting its default returns its family to NOT RUNNABLE LOCALLY on the
  next run, with no edit in this file.
- defaultedArgv renders the invocation as it runs here, with the default spelled
  out where CI writes the variable, and it is all-or-nothing: a partially
  covered argv stays unrunnable, because a command that runs with one real value
  and one invented one answers a question CI never asked.
- The derivation KEY is untouched. Only the rendered runnable member moves.
- Every family that genuinely cannot run now prints NOT MEASURED by name, in the
  human block and on --commands stderr, and says that a sibling invocation of
  the same script in the list above grades the script and not your diff.
- The pasted block deduplicates on the command, so two keys that render one
  command cannot repeat in the published harvest.
- The self-test harness `t` takes the third argument six call sites were already
  passing; it prints on a RED only, so a passing run's log is byte-identical to
  what it has always been.

One existing case's EXPECTATION moves with the rendering it pins (the
empty-changeset family in the matched block); its subject and its invariant are
unchanged, the reason is written at the case.

check:pm-dispatch-gates: 1415 cases before, 1445 after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15441-not-runnable-defaults (head 379179037, one file: scripts/pm/dispatch-gates.mjs, +597/−16), not the self-report.

What I checked in the diff:

  • leadingCommentBlock stops at the first line of code (shebang skipped, // runs and one JSDoc block both admitted) and declaredArgvDefaults admits a flag only when the usage block both names it as --flag and spells <name> defaults to <value> — the --endpoint prose deeper in check-half-states.mjs is pinned as NOT minting a flag.
  • defaultedArgv substitutes only a token that is nothing but the variable ("$RUNNER_TEMP/base.txt" pinned as untouched), covers --flag=value, and discoverFamilies spends it all-or-nothing (remaining.length === 0), leaving entry.check untouched — the key CI runs stays the key; only localCheck moves and only runnableInvocation reads it. [finding] dispatch-gates --commands does not derive check:optional-error-sink for a diff that adds a new logger-sink interface #14880's invariant holds.
  • The pasted block dedups on the command (two keys → one line, workflows/provenance/filled values merged), the note rides the same line and carries no [ separator, and spellingFooterLines is fed the deduplicated list so the footer and familyReconciliation agree.
  • Value-bearing rows now print ⊘ NOT MEASURED — nothing this card can run answers … plus the sibling warning matched on the script path; --commands prints one NOT MEASURED line per family on stderr, never stdout.
  • [finding] dispatch-gates' self-test harness t takes two arguments, but six call sites pass a third — every one of those cases' diagnostic readings is silently dropped #15539: t(name, cond, detail) renders through the pure selfTestCaseLines; a green line is byte-identical, a red with a reading grows one ↳ reading: line, detailedCases is counted so the repair cannot go vacuous; selfTestCaseLines is rostered in COMPOUND_ANCHOR_LEDGER per that table's own instruction.
  • The one moved expectation (the check-empty-changeset matched-block pin: --base "$MERGE_BASE"--base origin/main) keeps its subject and invariant; nothing was weakened to pass.
  • Live-class cases assert the split as a property (repaired.length > 0 && stillValueBearing.length > 0, every repaired default read off its own script), not as a typed roster.

Dev's verification quoted: pnpm -s check:pm-dispatch-gates exit 0, ✓ dispatch-gates self-test: 1445 cases pass. (1415 → 1445); derived family of 24 commands all exit 0; two-leg control built with git commit-tree (exit 1 on BREAKING without an ADR-0087 marker, exit 0 with one). CI on 379179037 is still running (Lint & Repo Gates, Type Check, Test Core 1/6) — the merge queue is the arbiter.

Ruling on the open question (cut-rc rows): A — leave as shipped. The emitted command for a cut-rc key is byte-identical to its pr-automation sibling's, so --commands is output-invariant on this tree; each row already names its workflow and the pinned variable. Revisit (option B becomes cheap) the day a defaulted-flag script is invoked only from a dispatch-only lane.

Flipping ready + enabling auto-merge. Fixes #15441, Fixes #15539. Serial queue on dispatch-gates.mjs: #15553 dispatches after this lands.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 4, 2026 21:56
@baozhoutao
baozhoutao enabled auto-merge September 4, 2026 21:56
baozhoutao pushed a commit that referenced this pull request Sep 4, 2026
…rection moves to #15553

PR #15570 rewrites the same `SELF_TEST_VERDICT` region (it inserts the
`selfTestCaseLines` docblock directly after the sentence this branch edited), and
`git merge-tree` reports a conflict between the two, so this PR would go dirty
the moment that one lands. It is ahead in the queue and the serial-file rule
allows one live PR per hot file, so the hunk is dropped here rather than raced.

`scripts/pm/dispatch-gates.mjs` is restored from `origin/main`; its blob is
byte-identical to this branch's merge base (f9dbdd6
at cf6b671, at origin/main 0c5e973 and in the tree now), so no content from
the newer main comes in with it. Nothing else on this branch changes: the anchor
repair, the DECOY fixture and the `ENTRY_BY_HAND` row all live in
`scripts/measure-self-test-floor.mjs`.

The docblock there still tells readers the mechanical probe cannot read that
file, which this branch makes false. That correction is deferred to #15553 rather
than dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 900334a Sep 4, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15441-not-runnable-defaults branch September 4, 2026 22:36
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

2 participants