Skip to content

fix(pm): read the compact - run: step form in dispatch-gates (#9203) - #9259

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9203-dispatch-gates-compact-run-step
Aug 17, 2026
Merged

fix(pm): read the compact - run: step form in dispatch-gates (#9203)#9259
os-steve merged 2 commits into
mainfrom
claude/issue-9203-dispatch-gates-compact-run-step

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9203

runCommandTexts in scripts/pm/dispatch-gates.mjs matched a run: step only when the key was preceded by whitespace alone, so the compact block-sequence form — the list dash and the key on one line — never matched and the step contributed nothing to the gate derivation.

- run: pnpm check:something

This is a latent gap, not an active one

Both compact steps in this tree are in showcase-smoke.yml and both are non-check steps (pnpm install --frozen-lockfile, pnpm turbo run build --filter=...). No check:* family is hidden today. Measured over all 25 workflow files, the set of discovered check invocations is byte-identical before and after this change — 130 both ways. The shape was one workflow edit away from hiding a gate; it was not hiding one.

The substance of the card was not the regex — it was what indent counts

Widening to (?:- )? is the easy half. indent is also what the block-scalar body walk uses to decide where a body ends, and a compact step puts its own sibling keys in the columns the dash occupies:

- run: | pnpm check:realenv:
NOTE: "... pnpm check:not-run-here ..."

Counting only the leading whitespace makes env: look deeper than the key, so the walk swallows the rest of the mapping into the command text — and every gate named in a swallowed env: / with: value is then discovered as one the step runs. That trades a missing lead for a fabricated one, which is the strictly worse direction: a gap costs one dev one CI round, an invention is pasted into every dispatch whose surface brushes it.

Settled by measurement against a real YAML parser (PyYAML), not by inspection. One fixture, three parsers, compared against the parser's own answer:

parsersteps foundcheck families discovered
today (^[ \t]*run:)3 of 64 — the three compact steps invisible
widened, indent = leading whitespace6 of 69 — one fabricated, from a swallowed env: value
widened, indent = column of the run key6 of 68 — exactly what YAML says

The truncation direction was probed too, since a walk that ends a body too early is the mirror failure. Under - run: | with the run key at column 8, a body at column 7 or 8 is a YAML error (ParserError / ScannerError); only column 9 and deeper parse. So > keyColumn is the YAML rule itself rather than an approximation of it, and cannot cut a valid body short.

indent counts the whole prefix — leading whitespace plus the - marker. That also makes the two step forms behave identically, which is the point: the key sits at the column run starts on in both, and the walk has always been right for the name: / run: form.

Reverse verification — both legs, direction predicted in advance

Run from committed state, restored with git checkout from the branch each time.

Leg 1 — the new form is genuinely reached (ablation: restore the pre-fix regex, keep the new cases):

✗ a compact `- run:` one-liner is discovered, with its filter
✗ a compact `- run: |` block body is discovered
✗ a direct script in a compact block body is discovered
✗ a compact step after a compact block body still parses
✗ one command text per compact step too
✗ a compact block body keeps both of its lines
✗ a compact one-liner yields its command verbatim
✗ dispatch-gates self-test: 7 of 260 case(s) failed.

The two over-consumption cases stayed green under this ablation, which is the honest asymmetry: a form that is never matched cannot over-consume.

Leg 2 — the widening bought no regression (ablation: keep the widened regex, count leading whitespace only — the regex the card suggested):

✗ a compact block body ends at the `env:` key of its own step
✗ a compact block body keeps both of its lines
✗ dispatch-gates self-test: 2 of 260 case(s) failed.

The first of those is the fabricated gate. The classic-form twin case stayed green, since without a dash the two readings coincide — which is what makes the fixture discriminating rather than decorative.

Existing forms, measured against the real tree rather than only a fixture. All 25 workflow files parsed before and after: check invocations identical in every file (130 → 130), 95 block-scalar bodies parsed, nothing lost. The only difference anywhere is showcase-smoke.yml, 4 → 6 step texts, the two compact steps now read.

Consumers

runCommandTexts has exactly one production consumer, extractCheckInvocations, which takes plain command strings and needed no change. extractWatchHints reads check-script sources, not workflow text, and is not a consumer at all. No consumer makes its own shape assumption.

One thing found while verifying, fixed in place

The first ablation run threw out of the self-test instead of listing failures — compactTexts[2].split(...) dereferences an element that does not exist when the parser drops the compact form. A gate that cannot say which case broke is a worse gate even when it is correctly red, so the indexed read is defaulted. The comment records why. The pre-existing block-scalar cases above it have the same shape and are left alone as out of scope; they fail loudly with a stack trace naming the exact assertion line, so the signal is correct-signed, just less readable.

Verification

Gate union re-derived from the actual changed path (node scripts/pm/dispatch-gates.mjs scripts/pm/dispatch-gates.mjs), which named pnpm check:pm-dispatch-gates and nothing else; check:nul-bytes added for the file edit. Both run at 054991a94, the final commit:

check:pm-dispatch-gates exit=0 (✓ dispatch-gates self-test: 260 cases pass)
check:nul-bytes exit=0 (scanned 6054 text files; no raw ASCII control bytes)

Self-test grew 246 → 260 cases.

skip-changeset: the diff is one internal PM tool under scripts/, published in no package and with no user-visible behaviour — it releases nothing.

Scope held to the parsing gap. A filename-convention gap was a different defect class, already handled elsewhere and not touched here.


Generated by Claude Code

@os-steveos-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 17, 2026 — with Claude
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reconciliation — re-measured from scratch; the delta is not in this branch

Bottom line: this branch produces zero derivation delta. Your 103 is a real number measured on a stale main worktree. No numbers adjusted in either direction, and no code change is needed — but you were right to stop and ask, because three of your four observations are genuinely impossible under the change as I described it, and that is exactly what a stale tree looks like.

The measurement

Two fresh detached worktrees cut from a just-fetched origin/main (739fe5b79) and from the branch tip (054991a94), your exact command in each:

mainref 739fe5b79: dispatch-gates: 104 check famil(ies) discovered across 25 workflow file(s)
branchref 054991a94: dispatch-gates: 104 check famil(ies) discovered across 25 workflow file(s)

Diffing the two complete --residue listings: identical, byte for byte.docs-drift-check.yml is attributed 1 family on both sides, not 0 then 1.

Where your 103 comes from

All four of your observations reproduce simultaneously at 9e0d9640a, an ancestor of this branch's base:

your observationat 9e0d9640aat origin/main739fe5b79
family count103104
ls scripts/docs-audit/README.md, affected-docs.mjs, check-audit-scope.mjs+ check-affected-docs.mjs
families from docs-drift-check.yml01
residue wording⇢ 'scripts/pm/dispatch-gates.mjs'⇢ gate source '...'

Two commits account for all of it, both landed 2026-08-17, both ancestors of 739fe5b79 and therefore of this branch:

git merge-base --is-ancestor confirms both are ancestors of the branch, and git diff origin/main against the branch ref, grepped for gate source / matched via / residueLines, returns no hits: none of that is in my diff. The diff is 120 insertions / 3 deletions in one file.

The two metrics are different quantities

130 is invocation occurrences — one per (workflow, command) match, duplicates included. 104 is distinct family names, deduplicated across all workflows. Both were already equal across main and branch; here they are side by side on the clean worktrees, so the claim is now checkable on your metric too:

DISTINCT FAMILIES main: 104 branch: 104 gained: [] lost: []
INVOCATIONS main: 130 branch: 130
per-workflow checks identical? YES
docs-drift-check.yml families main: 1 branch: 1

The only quantity that moves anywhere is step texts, 289 to 291 — the two compact showcase-smoke.yml steps, both non-check.

check-affected-docs.mjs — real file, real family, no fabrication

It exists on origin/main; your ls predates it. It is a deliberate thin wrapper whose own header states why it exists:

check-affected-docs (#9187) — the discoverable name for affected-docs.mjs's own --self-test … the mapper's own filename, affected-docs.mjs, carries no check- segment, so its self-test contributed zero discovered families despite being a real, one-second-local verification with a verdict. This thin wrapper is the fix.

So the check- prefix is not synthesised by the tool — it is a real filename, created precisely so a real gate stops being invisible. That is the filename-convention gap the card names as the adjacent-but-different class, and it landed as a wrapper file rather than as tool logic. The names: scripts/docs-audit/affected-docs.mjs annotation is the watch-hint — the path literal the gate's own source mentions — not the family name; the wrapper names the mapper because it wraps it. Nothing is invented, and the tool is not naming a nonexistent file.

The latent framing stands

Since the branch moves no family and no invocation, no check:* family was previously hidden by the compact-form gap. Ruling 2 was correct and the PR body's "latent, not active" wording is accurate as written. Had the measurement gone the other way I would have said so plainly — it did not.

What I changed

Nothing. The PR body and the report are accurate as written, so there is no commit to push and no number to correct. If you want the reconciliation on the record beyond this comment, say so and I will add a short note to the PR body — but I would rather not restate another card's history in this PR's body without you asking for it.

To re-verify: re-cut your main worktree from a fetched origin/main and re-run. The staleness is the whole finding.


Generated by Claude Code


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatch-gates.mjs's runCommandTexts does not recognize the compact - run: cmd single-line step form at all — zero commands discovered for that step

2 participants

@os-steve@claude