feat(pm): derive check:dispatcher-error-vocabulary from a diff's CONTENT, the gate no path can name - #12875
Merged
Conversation
…ENT, the gate no path can name The vocabulary gate computes its own population by walking a bare top-level root, so no path-derived trigger ever names it in a dispatch prompt's gate list. A dev introducing a new error/notice code runs the derived union green locally and reds on CI — measured once, at the price of a full round trip. Adds a CONTENT-shaped CHANGE_KIND_GATES entry: two limbs over the masked source (a `code` token bound to a quoted literal, a SCREAMING_SNAKE constant or a `typeof` reference to one; and a SCREAMING_SNAKE constant bound to a SCREAMING_SNAKE string), deliberately broader than the gate's own SHAPES table so it cannot go stale in the silent direction. Measured on this tree: 196 of 2281 non-test TS files (8.6%), against the 39% a path spelling would have named — the spelling the bare-root ledger already records as REFUSE-WIDE for this gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
This was referenced Aug 28, 2026
os-elon
marked this pull request as ready for review
August 28, 2026 06:51
os-elon
enabled auto-merge
August 28, 2026 06:51
This was referenced Aug 28, 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 #12850
check:dispatcher-error-vocabularycomputes its own population by walking a baretop-level root, so no path-derived trigger ever names it in a dispatch prompt's gate
list. A dev who introduces a new error/notice code runs the derived union green
locally and reds on CI's
Lint & Repo Gates. That silence cost one full CI round tripon PR #12843, which is where this card came from.
This adds a CONTENT-shaped
CHANGE_KIND_GATESentry — the first one in the tablejudged from a file's text rather than from its path.
Why not a path-shaped trigger
Not a judgment call: the tree has already adjudicated this exact spelling.
scripts/pm/bare-root-worklist.mjscarries the rowand the sibling row it is grouped with spells the trade out: such a declaration
"would name this gate for every card in the repo that touches a package". Inventing a
path prefix here would mirror, inside this file, a population the ledger next door has
already refused to spell — and a lead that fires on every card is one a reader learns
to skip. Recognising that the gate is content-shaped is the whole of this change.
The predicate
Two limbs over the comment-masked source, deliberately broader than the gate's own
SHAPEStable:codetoken bound to a quoted literal, a SCREAMING_SNAKEidentifier, or a
typeofreference to one. Thetypeofhalf is load-bearing: thespecimen that cost the round trip is
code: typeof CONVERSION_NOTICE_CODE.SCREAMING_SNAKE string, which is how a code is declared before any
codetoken isnear it.
It is not a copy of
SHAPESand must not become one: a copy would go stale in thesilent direction the day that table grows an indirection (it has grown twice for
exactly that reason), whereas a superset cannot.
Measured on this tree (7169 tracked files)
Those 2 outside files are the entire cost of carrying no population literal — and
carrying none is what keeps this file from spelling a pathy string it would then match
cards through. Verified: watch hints on this file are 9 before and 9 after, none
added, none removed.
A third limb (a SCREAMING_SNAKE string-literal type) was drafted from the card's
wording and dropped on measurement: it reached zero files the other two did not,
so it would have been a limb no fixture in the tree could pin.
The false-positive trade, written down
Per the card's ruling, the entry over-matches on purpose: it fires for a file that
merely carries a code, not only one that adds a new one. A false positive costs one
extra gate run (this gate needs no build and answers for the whole tree in one pass);
a false negative costs a CI round trip. The trade is stated in the predicate's
docblock and in the rendered
whyline, and a self-test case fails if that sentenceis silently removed — narrowing this later is a trade against a measured price, not a
tidy-up.
What it deliberately does not close
A file that does not exist has no content, so the trigger fires for the dev's
re-derivation off the merge base (where the file is real, and where the missed gate
actually costs the round trip) and stays quiet for the PM's hypothetical file surface.
Closing that by falling back to the path half would reintroduce the refused trigger
under another name. Stated in the docblock and pinned by its own case.
Verification
All at final head
7e53b389.node scripts/pm/dispatch-gates.mjs --self-test— 834 cases pass (27 new).Ablation. Removing the
typeoflimb reddens exactly one case, the one pinningthe feat(metadata): versioned ADR-0087 forward conversion at the artifact-ingestion door #12843 shape:
1 of 834 case(s) failed. Mutation confirmed on disk before therun (limb occurrences 1 to 0; blob
400f9278to8f3093af), restored withgit checkout HEAD --, restore confirmed by an emptygit diff HEADand the workingblob hashing back to
400f9278. No build ordist/is involved — this script is rundirectly by node — so there is no rebuild leg to report.
Gate family re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths);it returned exactly the 12 families the dispatch brief named, no more. All green,
each exit code captured before any pipe:
check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:cross-package-test-inputs·check:entry-guard·check:parse-guard·check:pm-dispatch-gates·check:pnpm-filter-targets·check:watch-hint-literal·scripts/check-ci-filter-parity.mjs·scripts/check-cross-package-test-inputs.mjs·scripts/check-self-test-wired.mjs· pluscheck:nul-bytes.check-ci-filter-parity.mjsfirst exited 1 withPREREQUISITE NOT MET - the dependency yaml is not installed. That is NOT MEASURED, not a finding; afterpnpm installit re-ran green: "all 109 declared cross-package glob(s) (84 unique)are covered".
This script's own suite is its
--self-test; no vitest file exercises it (the twotest files that name it do so in prose comments only).
Changeset: none, deliberately
Derived, not assumed.
scripts/pm/**is internal PM tooling and ships in no publishedpackage, so this PR releases nothing. Receipt: of the 8 most recent commits touching
scripts/pm/dispatch-gates.mjs, 8 carried zero changesets. The PR carriesskip-changesetinstead, which is a live mechanism here —pr-automation.ymlreadsthat label as the author's explicit opt-out.
Scope
The CONVENTION trigger only. #12856 — the tsc heap ceiling that makes
check:type-check-debt --re-measureOOM only on CI — was split off by triage and isnot addressed here; it remains open.
Generated by Claude Code