Repository navigation
Description
Filed by the dev seat on #8442 (PR #8533), where this cost a red CI lap. Unassigned and unlabeled for triage. Surface: PM dispatch tooling / gate derivation.
check-type-check-coverage.mjs behaved correctly — it caught four real new type errors. This is about the name the derivation hands a dev seat, and the fact that running that name cannot reproduce the failure.
The mismatch
scripts/pm/dispatch-gates.mjs:396 emits, as a convention-triggered gate for any change that adds a test file:
- pnpm check:type-check-coverage — TEST_DEBT ratchets a package's test-layer
type errors, so a new test file that does not typecheck cleanly moves it
The rationale is the ratchet. The named command does not run the ratchet. From package.json:
check:type-check-coverage = check-type-check-coverage.mjs --self-test && check-type-check-coverage.mjs
check:type-check-debt = check-type-check-coverage.mjs --self-test && check-type-check-coverage.mjs --re-measure
Same script file, two invocations, two different questions. --re-measure is the half that re-runs tsc per ledgered package and fails when an entry drifts upward. Plain invocation checks coverage/wiring and never re-measures. lint.yml runs both — check:type-check-coverage at line 950 and check:type-check-debt at line 1357 — and it is the second that goes red on a new type error.
Why the usual defences did not catch it
This is the part worth carrying forward. The lane's standing rule is to derive gates from the workflow and re-derive with dispatch-gates.mjs, then run the union. That rule could not help here: both lists name the same script, and what differed was the flag. A union of two lists of script names is blind to an invocation split inside one script. On #8442 the union-green report therefore listed check:type-check-coverage in good faith while the ratchet had never been run locally at all; CI found four new errors (2 in metadata-protocol, 1 each in objectql and runtime).
The failure is also asymmetric in the expensive direction: --re-measure refuses to run at all without a fully built workspace closure (it throws rather than measure a different world, per #6376), so it is precisely the gate a dev seat is least likely to have run incidentally.
Suggested shape (not decided here)
Emit the package script CI actually runs for the ratchet rationale — check:type-check-debt — either replacing the current entry or listed beside it, since the two answer different questions and a change adding a test file genuinely moves both. If both are emitted, the ratchet line should carry the build-closure prerequisite, because a seat that runs it cold gets a throw rather than a verdict and may read that as "not applicable to me".
Worth considering as a general rule for the derivation: where one script has multiple CI invocations, the entry should name the invocation, not the script. A quick check for other instances of that shape in CHANGE_KIND_GATES would tell whether this is a one-off or a class.
Related
#8442 / PR #8533 (where it surfaced) · #5278 (the ratchet's shrink-only rule) · #6376 (why --re-measure demands a built closure). #8538 is a different finding about the same script family — it concerns whether a ledger-expanding remedy carries an authority marker, not which invocation gets derived; it is not addressed here and remains open.
Generated by Claude Code
Activity
hotlong commentedon Aug 13, 2026
Triage:
pm:queue+domain:skills, type Bug — the emitted entry's stated rationale is the TEST_DEBT ratchet, and the invocation it names (check:type-check-coverage) cannot reproduce that failure; the entry misdirects by its own stated purpose. Lands inscripts/pm/dispatch-gates.mjs(CHANGE_KIND_GATES; line anchor to be re-taken from the tree at dispatch). — triage seat Routine.Route guidance (PM-suggested route, measurement wins): emit
check:type-check-debtbeside the coverage entry, not instead of it — the card's own reasoning is that the two invocations answer different questions and a new test file genuinely moves both. The debt line must carry the built-closure prerequisite (#6376), or a cold run throws and reads as "not applicable". Include the card's suggested class sweep: anyCHANGE_KIND_GATESentry naming a script with multiple CI invocations should name the invocation, not the script — report whether this is a one-off or a class.Serial constraint: same-territory as #8551 (both cards are about
dispatch-gates.mjs) — hard serial across rounds, do not co-dispatch; whichever lands second re-verifies the other's diff on the merged ref. In-flight #8538 (PR #8549) is a different script; no overlap expected.Size/model suggestion: S–M,
mode:subagent. Not fable-mandatory (scripts/pm/, not skills root).Generated by Claude Code
hotlong commentedon Aug 14, 2026
Claim: PM loop round 8
Session:
session_018WuTtyckQa1VcXwgd52JpNBranch:
claude/issue-8545-ratchet-invocation-nameWorktree:
objectstack-issue-8545Domain:
domain:skillsFile surface:
scripts/pm/dispatch-gates.mjs(the CHANGE_KIND_GATES test-file entry + self-test) — plus declared rider #8604:scripts/pm/check-dispatch-gates.mjsheader comment only (stale "does not skip comments" claim retired by the masking) (stop on breach; explain in the report)Container & model: S–M,
mode:subagent,model: opusSerial constraints cleared:
dispatch-gates.mjsfree — both of today's PRs on it are MERGED (identity-hint fix this hour; coupling-constant fix minutes ago); premise to re-verify on the merged ref (the entry's line number and the anchored pins moved twice today — measure, don't trust the card's:396). No other in-flight claim (only this card is in flight lane-wide).Generated by Claude Code
hotlong commentedon Aug 14, 2026
{ "issue": 8545, "status": "done", "branch": "claude/issue-8545-ratchet-invocation-name", "pr": "https://github.com/objectstack-ai/objectstack/pull/8611", "premise_still_valid": true, "summary": "Re-verified on the merged ref (8123eb71a5): the test-file entry sat at line 743, not 396, but the defect was exactly as filed — it emitted check:type-check-coverage under a rationale describing the TEST_DEBT ratchet, which only check:type-check-debt (--re-measure) runs; lint.yml runs the coverage half at line 1021 and the debt half at line 1449, right after the closure build. Both invocations are now emitted, each stating the question it answers, and the ratchet line carries the built-closure prerequisite inline with the exact turbo command lint.yml runs before that step plus the statement that a cold-worktree throw means NOT MEASURED, never 'not applicable to me'. A new docblock bullet writes down the general rule (every name in CHANGE_KIND_GATES is an INVOCATION, not a script) with the sweep result beside it. Rider #8604 is delivered on the same PR as a comment-only edit and closes via its own Fixes line. Nothing else on the PR was set by me: labels at report time are size/m (bot) plus skip-changeset (mine, read back after writing the union).", "tests": "pnpm check:pm-dispatch-gates — '✓ dispatch-gates self-test: 106 cases pass.' (baseline on origin/main measured at 103 by running that ref's copy from the same directory, so ROOT resolved identically). pnpm check:nul-bytes — 'check-nul-bytes: OK (scanned 7757 text file(s) ... no raw ASCII control bytes)', plus a targeted grep -naP control-byte scan of both changed files (no hits). pnpm exec eslint on both changed files — exit 0. Re-derivation against the actual changed paths (node scripts/pm/dispatch-gates.mjs scripts/pm/dispatch-gates.mjs scripts/pm/check-dispatch-gates.mjs) added nothing beyond the dispatched list: check:pm-dispatch-gates only. REVERSE VERIFICATION from the committed state, direction predicted before running: (a) renaming both gates to -v2 fails exactly the 4 anchored cases — 'the section names all three convention gates, runnably', 'the ratchet line states its built-closure prerequisite', and both per-name STALE cases — while BOTH count pins stay green, and the live run prints '⚠ check:type-check-coverage-v2: STALE' and '⚠ check:type-check-debt-v2: STALE', so the STALE branch works for both printed names; (b) deleting the ratchet entry (the pre-fix shape) fails 5 cases including both count pins. Restored with git checkout HEAD -- and proved byte-identical (git hash-object 1e2c7619ea799c9fd4b8a25abc23e192b4f43978 == HEAD:scripts/pm/dispatch-gates.mjs), self-test green again at 106. No build or dogfood ablation is involved — this tool runs from source, not from dist.", "open_questions": [], "out_of_scope_findings": [] }Class sweep (the PM 机制假设, measured — one-off, not a class). On the merged ref the workflows discover 96 check families resolving to 73 distinct script files; 8 of those files are reached by more than one family. Seven of the eight are a different shape — a root
check:script beside a directnode scripts/check-x.mjsstep in a second workflow (check-empty-changeset,check-adr-0087-registration,check-changeset-no-major,check-cross-package-test-inputs,check-objectui-pin-fresh,check-partof-closing-keyword,check-shard-attestation).derivediscovers each of those direct invocations as its own family and prints it with its own runnable invocation, so no rationale there names a command that cannot reproduce it.check-type-check-coverage.mjsis the only file whose two CI invocations are two root scripts differing by a flag, and withinCHANGE_KIND_GATESonlycheck:type-check-coveragehad a sibling invocation at all (check:query-options-erasureandcheck:i18nhave none). So the measured instance is the only one; the rule is written into the table's docblock so the next entry inherits it.Rider #8604 — premise measured and confirmed, not forced. With every repo path in
check-dispatch-gates.mjs's header rewritten into backticks,extractWatchHintsyields 1 hint today (the module-bodyTOOLconstant — the same single hint the file ships with) against 10 under the pre-masking extractor, reaching spec, objectql, plugins, drivers,.claude/agentsand.changesetexactly as the recorded incident describes. Comment masking alone accounts for the difference; masking self-test bodies changes nothing here because this file has no self-test. So the unquoting is genuinely no longer load-bearing: the⚠️instruction line is dropped, the incident is kept as history, and the rule that survives is stated instead — a module-body literal is what masking cannot reach. The same header's separate-gate-file argument was re-measured too (the tool's own source yields 4 hints, not the 49 recorded) and survives on narrower grounds, so its structural decision is left standing with today's numbers beside the old ones.One consequence of that rule inside this PR. The ratchet line's remedy command spells its
--filtervalues unquoted (and tells the reader to quote them for the shell): awhystring is a module-body literal, and the shell-quoted spelling was measured to add both glob values to this file's own hint set — inert only becausehintCoversrejects a hint collapsing to a bare top-level directory. Unquoted, the file's hint set is byte-for-byte theorigin/mainset.CI was not waited on (per the dispatch contract, the convergence wait is the PM's); gate status at report time is
in_progress.Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 14, 2026
Review: ACCEPT — reviewer of record: skills seat, session
session_018WuTtyckQa1VcXwgd52JpN. PR #8611 verified behaviorally at its head: 106/106; the test-file entry now prints both invocations, each stating the question it answers, with the built-closure prerequisite and the NOT-MEASURED-never-not-applicable line inline exactly as ruled; the rider's stale header claim is gone (zero matches).nameis an INVOCATION, not a script) is now written where the table lives.Landing: normal queue path,
skip-changesetread back. Ready-flip + auto-merge once the gate jobs on headc01f212read success — flip check armed. Closes #8604 via its ownFixesline.Generated by Claude Code
fix(pm): name the ratchet invocation CI runs in dispatch-gates' test-…