fix(pm): dispatch-gates and check-self-test-wired discover package-local gate invocations by their real path (#15342) - #15415
Merged
Conversation
`check-self-test-wired` anchored `INVOCATION_RE` on the bare literal
`scripts/`, so lint.yml's `node packages/lint/scripts/check-reference-
carrier-shape.mjs --self-test` matched as a SUBSTRING and was filed under
`scripts/check-reference-carrier-shape.mjs` — a key with no file behind it.
Both directions were silent: the real file sits outside the root walk, so it
was in no population and never audited, and the phantom key could never be
reconciled against a carrier either.
Two anchors move, and only these:
- `INVOCATION_RE` consumes the directory prefix into the key and gains a
left boundary, so a path is keyed WHOLE. A leading `./` is stripped
(cut-rc.yml and release.yml import from `"./scripts/…"`, and keying those
elsewhere would drop two live invocations); `..` is not a prefix segment,
because a path this ROOT cannot resolve is how the phantom was minted.
- the population gains a second source: a script CI NAMES that lives outside
the root walk, admitted on the root walk's own terms (must exist, must
carry the literal in code). Not a second walk — see the comment in main().
Measured on the live corpus: 186 named paths, 0 without a file on disk
(before: exactly 1). Carriers 182 -> 183, members 168 -> 169, wired 164 -> 165.
Self-test grows two batteries — `left boundary` (6) and `live corpus` (3);
50 cases -> 59, floor 7 -> 9. No existing case is weakened.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…eal path (#15342) `lint.yml` runs one gate by a package-local path — `node packages/lint/ scripts/check-reference-carrier-shape.mjs --self-test`, plus the bare production invocation on the next line. Both tools that read that corpus anchored on the literal `scripts/`, and failed in opposite ways. `dispatch-gates` required `node ` to be followed IMMEDIATELY by `scripts/`, so neither `DIRECT_CHECK_INVOCATION` nor `SELF_TEST_INVOCATION` matched the step at all: the gate was in NO family, and `--residue` could not report it either — it is absent from the universe the matched / silent / undetermined buckets partition. Measured on `65846bc46` for a card editing that gate's own file: `--commands` named it zero times, all 27 `--residue` mentions were the dev's own changed path echoed back, and the step surfaced only in the always-runs STEP tail, as one of the 33 CI steps the derivation names no family for. Both patterns gain the same directory prefix, spelled identically. A segment must start with an alphanumeric or `_`, so `..` is refused — a path this ROOT cannot resolve is how a phantom identity key gets minted. A `scripts/` segment is still required, so no new species is admitted (`node packages/cli/bin/ run.js` stays out). Over 7472 tracked files: check families discovered 252 -> 254 (+2, ZERO lost) gate files 201 -> 202 (+1, ZERO lost) watch-hint (gate, file) pairs 240947 -> 254181 (+13234, ZERO lost) existing families re-attributed 0 — every pre-existing family's pair count is byte-identical before and after always-runs steps naming NO family 33 -> 32 The gate declares four subtree hints, so it lands in the derivation for cards touching `packages/**`, `examples/**`, `scripts/**` and `apps/**`, and by identity for a card editing its own file — not in the residue. Two existing tail cases pinned the defect as a property. Neither is weakened: each is inverted and gains a positive control, so the step's absence from the tail cannot go green through a tail that stopped walking. check-self-test-wired keyed the same path by SUBSTRING, filing it under `scripts/check-reference-carrier-shape.mjs` — a key with no file behind it, audited by nobody. `INVOCATION_RE` consumes the prefix and gains a left boundary; the population gains a second source, admitted on the root walk's own terms. Its two new skip predicates are membership in the walk's OWN OUTPUT rather than `startsWith('scripts/')`: that re-spelling reaches the dispatch derivation as the bare literal `scripts` and joined the shrink-only escapable-literal species FRESH on both of this gate's families. Self-tests grow four batteries: `left boundary` (6) and `live corpus` (3) in check-self-test-wired (50 -> 59 cases, floor 7 -> 9), and eight fixture cases plus two live pins in dispatch-gates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
This was referenced Sep 4, 2026
baozhoutao
marked this pull request as ready for review
September 4, 2026 15:00
baozhoutao
enabled auto-merge
September 4, 2026 15:00
baozhoutao
deleted the
claude/issue-15342-package-local-gate-anchoring
branch
September 4, 2026 15:40
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15342
lint.ymlruns one gate by a package-local path. Two tools read that corpus and both anchored on the literalscripts/; they failed in opposite directions, and both silently.Measured first — every workflow step invoking a gate by a path NOT under
scripts/Extracted with
check-self-test-wired.mjs's owncollectInvocations(the extractioncheck-self-test-workflow-commands.mjsalready imports), never a fourth parser. Over 30 workflow files and 186 named paths, the population is exactly one step:lint.yml— step Relationship carriers are spelled as the string the spec declares:node packages/lint/scripts/check-reference-carrier-shape.mjs --self-testnode packages/lint/scripts/check-reference-carrier-shape.mjsThe only other non-root path any workflow invokes with
nodeispackages/cli/bin/run.js, which carries noscripts/segment and is not a gate.(a)
dispatch-gates.mjsnever discovered it, and--residuecould not report it eitherBoth
DIRECT_CHECK_INVOCATIONandSELF_TEST_INVOCATIONrequirednodeto be followed immediately byscripts/, so neither matched the step at all. A family that is never discovered is absent from the universe the matched / silent / undetermined buckets partition — the #11397 state, one lane over. Measured on65846bc46, for a card editing that gate's own file:--commandsnames it 0 times (26 commands derived, none of them this gate);--residuementions are the dev's own changed path echoed back as an INPUT (matched via packages/lint/… ⇢ gate source 'packages/**'), never as a family;(b)
check-self-test-wired.mjskeyed it to a root path with no file behind itIts
INVOCATION_REopened on the bare literalscripts/with nothing to its left, so it matched the package-local path as a substring and filed the gate underscripts/check-reference-carrier-shape.mjs— a key with no file behind it. Both directions were silent: the real file is outside the root walk so it was in no population and never audited, and the phantom key could never be reconciled against a carrier either, so neitherauditPopulationnorauditLedgerhad anything to say. Measured before: 186 named paths, exactly 1 with no file on disk.The fix — minimal and additive, at both anchors
scripts/pm/dispatch-gates.mjs. Both patterns gain the same directory prefix, spelled identically. A prefix segment must start with an alphanumeric or_, so..is refused: a path this ROOT cannot resolve is how a phantom identity key gets minted, andresolveCheckToFiles' docblock prices that outcome as strictly worse than the bug being fixed. Ascripts/segment is still required, so the prefix widens the DIRECTORY a gate may sit under and never the species of file.scripts/check-self-test-wired.mjs.INVOCATION_REconsumes the prefix into the key and gains a left boundary, so a path is keyed WHOLE; a leading./is stripped (cut-rc.ymlandrelease.ymlimport from"./scripts/…", and keying those elsewhere would drop two live invocations). The population gains a second source — a script CI NAMES that lives outside the root walk — admitted on the root walk's own terms: the file must exist and its CODE must carry the literal. Fixing the key alone would have left that half exactly as silent as before. After: 186 named paths, zero without a file on disk; carriers 182 → 183, members 168 → 169, wired 164 → 165.Its two skip predicates are membership in the root walk's own output, not
startsWith('scripts/'). That re-spelling is a bare top-level word wearing a separator: it reaches the dispatch derivation's hint set as the plain literalscriptsand joined the shrink-only escapable-literal species FRESH on both of this gate's families (caught bydispatch-gates --self-test, and confirmed by diffing the extracted hint sets base vs branch). The set form says what the predicate means and declares nothing.The A/B
For a card touching
packages/lint/scripts/check-reference-carrier-shape.mjs,--commands --repo objectstack-ai/objectstack:The gate declares a population —
ROOT_DIR_WATCH_HINTS = ['packages/**', 'examples/**', 'scripts/**', 'apps/**']— so it does not land in the residue. It reaches cards by that declared population as well as by identity: a card touchingpackages/spec/src/data/field.zod.tsnow derives both of its invocations too.Priced in both directions over 7472 tracked files:
The +13234 is exactly 2 x 6617: four subtree hints, two invocations, two keys — the #14880 split working as designed. Becoming a GATE FILE is the one direction that could SUBTRACT (
discoverFamiliesexcludes gate files from import-following), so it is measured rather than argued: no existing family's pair count moved either way.SELF_TEST_INVOCATIONgains zero recall today — the one specimen carries acheck-basename, so the direct matcher takes it and the self-test matcher skips it by design. It is widened anyway because the two matchers' coordination contract is that they mint byte-identical keys for one script; a package-local gate not namedcheck-*would otherwise be discovered by neither. Same standard as the extension right-boundary inresolveCheckToFiles: zero recall today, zero cost today, class closed before the first specimen arrives.Self-tests
check-self-test-wired: two new batteries,left boundary(6) andlive corpus(3) — 50 → 59 cases, battery floor 7 → 9. No existing case weakened.dispatch-gates: eight fixture cases plus two live pins. Two existing always-runs-tail cases pinned the defect as a property (a package-local gate invoked by path IS in the tail, and its live twin). Neither is weakened: each is inverted and given a positive control, so the step's absence from the tail cannot go green through a tail that stopped walking. The tail's own class assertion — a step the derivation names nothing for IS listed — is untouched and still held by the other-interpreter member, on the fixture and on the live tree.Ablation, restore proven
The mutation put both patterns back to the pre-#15342 spelling and was confirmed on disk before any reading (widened-prefix occurrences 2 → 0; base-spelled 3, being the two patterns plus one prose mention in the new battery comment). Restore is
git checkout HEAD -- ABSOLUTE_PATHunder anEXIT INT TERMtrap, verified by blob hash againstgit rev-parse HEAD:PATHand an emptygit diff HEAD, not by an exit code.Each case's own predicate, same inputs and same operators as the shipped assertions, against the base anchoring — 7 red, 6 controls green:
The same probe against the shipped tree is 13/13, matching the shipped self-test's own verdict. Case 10 passing on both is correct and stated: on the base tree nothing package-local is discovered, so there is no phantom to catch — it is a class pin, not an instrument for this anchor.
Verification
Exit codes captured before any pipe (
cmd > file 2>&1; EXIT=$?), verdict lines quoted from the tools' own output. Run at2c22f5716.node scripts/pm/dispatch-gates.mjs --self-test(foreground, shared verify lock) —✓ dispatch-gates self-test: 1402 cases pass.,os-verify-lock: VERDICT command-exit 0 · held the lock 385s.pnpm check:pm-dispatch-gatesis the wrapper that spawns exactly this command.node scripts/check-self-test-wired.mjs—✓ … every one of the 169 script(s) CI runs that ship a --self-test has that self-test run by CI(scope: 183 carriers,1 of them package-local gate(s) CI names by path).node scripts/check-self-test-wired.mjs --self-test—9 declared batteries, 59 cases registered, every battery at or above its pinned floor.--changed --commands --repo objectstack-ai/objectstack— 25 commands, every one run, all exit 0.check-self-test-workflow-commands.mjsand--self-test,check:declared-population-live(203 of 254 famil(ies) declare a path population, and every one of them reaches this tree's 7472 tracked file(s)),check:ratchet-remedy-authority,check:nul-bytes,check:watch-hint-literal— all exit 0.pnpm lint(eslint . --no-inline-config) — exit 0.Out-of-scope finding filed while measuring: #15414 — check-self-test-workflow-commands builds its own root-only population, so the same package-local gate is in no sweep there either. Not touched here.
No changeset: this publishes nothing from any released package (
scripts/tooling only), so theskip-changesetlabel carries it.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code