fix(pm): dispatch-gates names the type-check lanes it cannot measure - #19636
Conversation
`dispatch-gates --commands` emitted `check:type-check-coverage` and `check:type-check-debt` for a TypeScript-touching path and nothing else carrying the word, so a reader who grepped the output for `typecheck` found something and could conclude the surface was covered. Those two ratchet a ledger; the required `TypeScript Type Check` context goes red on per-package `tsc` programs that sat in no bucket at all. Add a type-check lane block, derived from the same parsed workflow entries the always-runs tail and the path-scheduled job block read: every pull-request-workflow step whose `run:` invokes a type-check program, recognised on argv tokens rather than substrings. It renders on every plain run above the tail, prints its rows as `⊘ NOT MEASURED` on the `--commands`/`--json` stderr accounting, carries a `typeCheckLanes` key in `--json`, and is named unconditionally in `outsideBlockNames` so an empty walk cannot hide it. An empty walk renders loud instead. Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
The lane fixture replaced a literal that only exists inside a block scalar, so its unconditional row never appeared. Anchor it on the step whose `run:` is a compact one-liner instead. The four verbatim pins on the outside-blocks enumeration are doing their job: they spell the whole phrase in print order, so the new unconditional name belongs in each. Updated in place, including the two negatives that assert the names printed at zero rows. Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsDiff line numbers below are lines of (a) (b) Same entries — HOLDS. (c) (d) Empty walk (diff 111-117, 211-213) — unreachable today, correctly loud. 4 rows on this tree, so the branch is dead until the recogniser or (e) stdout — confirmed unchanged. No (f) Prohibition honoured. Neither (g) Budget. 207 insertions, ~95 of them prose. "Two families carry the word, neither is a lane" is restated in six comment sites (diff 21-23, 45-53, 99-107, 166-168, 195-198, 243-245) and twice more in rendered output (diff 121-123, 207-210). That matches this file's convention of a why-here note per call site, so I do not refuse it — but it is the first thing a budget would cut. ⛔ The one factual error. Diff 43-44 states the lanes "were in NO bucket this tool had." Measured on this head: all four rows are already rows of the always-runs tail — same workflow, job, step and command, 4 of its 33 rows, printed on every plain run. The dev's own derive-site comment (diff 242-244) says exactly that ("the tail is where these steps otherwise dissolve… one row among thirty-three"), so the diff contradicts itself about the tool's prior behaviour. The real defect is narrower and still real: no name, and no disclosure at all on ② Semver levelNone owed. ③ Boundary flags
What must change
Implemented-by: VERDICT: FAIL Generated by Claude Code |
Review of record on PR #19636 returned FAIL with five owed changes. - The live control is pinned to the lanes behind the required aggregate BY NAME (`Type Check · source gates` / `· workspace` / `· consumer gates`) instead of a bare row count, so a partial recogniser miss reds `check:pm-dispatch-gates` rather than dipping silently. `Type Check · debt ledger` stays absent on purpose: its only `run:` IS the ledger family. - `counts` now sizes the walk — command-carrying steps and spliced `run:` lines — and both rendered lanes print that denominator, the discipline the two neighbouring blocks already keep. - Line continuations are spliced with `joinLineContinuations` before the token split, as `jobFilteredSteps` does; an unspliced split drops a continued invocation silently. - The header no longer claims the lanes "were in NO bucket this tool had". Measured: all four rows were already rows of the always-runs tail. What was absent is a NAME and any `--commands` disclosure. - The three human-lane enumeration pins are extended to the new name, matching what the `--ran` and `--commands` lanes already carry. Two nits from the record: the stderr row now elides rather than truncating silently, and the lane walk moved below the `--ran` return, which renders no block of it. Paid for inside the +200 budget by cutting the restated "neither of those two is a lane" note down to the header and one line in each rendered lane. Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsLine numbers are lines of The five owed changes of record 5770500900 — all five MET. (1) Live control pinned BY NAME — MET. Head 27118-27125: the control now asserts (2) Walk sized, both lanes render it — MET. (3) Continuations spliced — MET. 2932-2935 is byte-identical to (4) Header corrected — MET, and the replacement is true. 2892-2899 now reads "What was missing was NOT the steps… all four rows this walk returns were already rows of the always-runs tail — same workflow, job, step and command, 4 of its 33. Missing was a NAME for them and any disclosure at all on (5) Three human-lane pins extended — MET. Name-by-name battery 27281, the phrase pin 27306 ("naming the three blocks that print unconditionally", over three names), zero-total battery 27327. The round-0 repairs at 27292 / 28370 / 28383 / 28460 are untouched and still correct; occurrences of the name rise 6 → 9. Case count 1903 → 1904 is consistent: only the name-by-name loop adds a Both nits — MET. Stderr row elides with Reverse verification holds as a claim about the code as landed. I ablated one spelling from the recogniser (2885) and re-walked the live workflows: rows 4 → 1, the old Nothing narrowed or widened beyond round 0. Predicate 2881-2886 is unchanged; the only behavioural changes are the splice, the two counters and the empty-line filter (inert — the empty string never matched). Prose cuts removed nothing the code relies on. The cut lines are restatements: "neither of those two is a lane" survives in the header (2896-2899), the human block (11993-11995) and the stderr line (14724-14725); "running every command on stdout does NOT cover it" is carried by "OUTSIDE the runnable total" at 11995. No self-test pin greps a cut phrase — the loud-empty pin at 27113-27114 still matches ( The merge brings nothing into this file. Residuals, not defects. (i) The denominator it installed is itself unpinned — no case asserts the ② Semver levelNone owed, unchanged from round 0. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19172
Clause-②: no
The defect, restated from the measurement
scripts/pm/dispatch-gates.mjs --commandsis what a dispatch order is built from. For a TypeScript-touching path it emitspnpm check:type-check-coverageandpnpm check:type-check-debt— and nothing else carrying the word. Those two ratchet a LEDGER. The requiredTypeScript Type Checkcontext goes red on four CI jobs running per-packagetscprograms, and the four steps that run them had no NAME anywhere in the output and no disclosure at all on--commands— they were already rows of the always-runs tail, 4 of its 33, printed unnamed and unclassified on every plain run. So a reader who greps the output for the one word they would grep for FINDS something and can reasonably conclude the surface is accounted for. That is a false reassurance, not a silence they could notice.Re-measured on the base of this branch (
744a0a3) with the filer's own paths,packages/spec/scripts/sharded-artifacts.test.ts scripts/git-env.d.mts:Premise live on this base, three file moves after the card was graded.
What landed
A type-check lane block, derived from the same parsed workflow entries the always-runs tail and the path-scheduled job block already read — never a hardcoded roster:
isTypeCheckInvocation(command)recognises a type-check program on argv tokens, never substrings:tscas a token together with--noEmit/-p/--project, orrunfollowed bytypecheck/type-check. The substring reading over-matches on this very tree (anechoabout atsc-built package, the lane aggregator's ownconsole.logabout atype-check lane), and both over-matches are pinned as negatives.check:type-check-coverageandcheck:type-check-debtmatch neither, deliberately.typeCheckLaneSteps(entries)walks every pull-request workflow's jobs and steps. A job or step carrying anif:is kept and marked, never excluded — the two neighbouring blocks drop a conditional because each claims CI definitely runs the step, and this one claims only that the lane exists.countsreturns the command-carrying steps and the splicedrun:lines examined, and both rendered lanes print that denominator, so a recogniser that stops spelling a lane shows as a dip rather than as silence.joinLineContinuationsbefore the token split, the readingjobFilteredStepstakes; an unspliced split drops a continued invocation silently.typeCheckLaneLines(rows, counts)renders it on every plain run, directly above the always-runs tail, with the prescription a card owes:pnpm --filter PKG run typecheckper package whose TypeScript the diff changes what a program can see.THE SOURCE OF TRUTH CAME BACK EMPTYwith the workflow count it read, rather than dropping the heading — a missing block looks exactly like a covered surface.--commands/--jsonprint the rows on stderr as⊘ NOT MEASUREDlines, plus one line saying the two ledger families are not these.--jsoncarries atypeCheckLaneskey. The block is named unconditionally inoutsideBlockNames, so an empty walk cannot hide it from the closing disclaimer on any of the three lanes that read that list.After, same paths, at
872e04b:The derivation for this PR now names its own former blind spot.
Reverse verification — measured, not designed
The first full battery run with the block wired in went RED: 6 of 1903 cases failed. Four of those six are pre-existing pins that spell the outside-blocks enumeration verbatim — the human reconciliation lane's phrase pin, the
--ranlane's phrase pin, and both zero-row negatives ("while still naming the two that print unconditionally"). That red is the proof the wiring is observable rather than decorative: adding one unconditional name tooutsideBlockNamesreddened every lane that reads that list. The other two were my own fixture anchoring on a literal that only exists inside a YAML block scalar. Both halves repaired in the second commit; the re-run is green below.Second, for the by-name control (patch round 1): ablating one spelling out of the recogniser (
joinLineContinuationsuntouched) drops the live rows 4 to 1 — the oldrows.lengthgreater than zero assertion stays GREEN while the by-name assertion goes RED. On-disk proof: anchor 1 to 0, blob9c8d18aa52d8toc3acc8e3a937; restored blob equals HEAD andgit diff HEADis empty.Tests and gates, at
3b533c1(patch round 1; the round-0 rows held at872e04b)pnpm check:pm-dispatch-gates(detached, 655.0 s)✓ dispatch-gates self-test: 1904 cases pass.and✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.:: exit 0in the run recordnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran✓ dispatch-gates --ran: 29 derived famil(ies) accounted for — 29 run, 0 NOT-MEASUREDpnpm lint(repo-wide,eslint . --no-inline-config)Ten new self-test cases, all green: the two positive spellings, the two ledger families as negatives, both live over-matches as negatives, the conditional-kept-and-marked row, the non-pull-request workflow sized rather than dropped, the heading and prescription, the loud-empty rendering, the unconditional enumeration name, and a LIVE control asserting the per-package
tsclanes are found on this tree with no ledger family mistaken for one.No build or per-package
typecheckis owed by this diff and none is claimed: it touches one.mjsfile and no workspace package, and the tool's own change-kind derivation scores it0 by change KIND— a.mjsis outside the root tsc program, the root config setting noallowJs.Line budget: 208 insertions / 8 deletions = net +200, self-test cases included (patch round 1 was +76 / −76: the new checks paid for line-for-line out of the restated prose).
Acceptance notes
pnpm --filter PKG run typecheckas real commands) is measured NOT cheap, and is not taken here. Two readings: the tool has no path-to-package map at all (the only package resolution in it runs the other way, from a--filtername to a directory); and a lane is not a discovered check family, so putting lane commands into the runnable union would breakfamilyReconciliation's own arithmetic invariant, which assertsmatched + convention − both + alwaysRunsOnly === totaland throws otherwise. The prescription is therefore carried as text in the block rather than as stdout commands. Candidate 3's prohibition is honoured: neither ledger family is renamed or dropped.Type Check · debt ledgerrunspnpm check:type-check-debt, which IS a discovered family, so it is correctly absent from the lane rows and present in the matched list — the two blocks stay disjoint..github/workflows/**is read and never edited, as the claim's file surface declares.pnpm typecheck,pnpm -r typecheck,node --run typecheck,tsc --build,tsc -b,vue-tsc,tsgo). They are declared in the predicate header, the walk is sized so a miss is visible as a dip, and the live control pins the required aggregate lanes by name so a miss on THOSE reds.Generated by Claude Code