Skip to content

Commit 3d18877

Browse files
os-teslaclaude
andauthored
fix(pm): the dispatch-gates exclusion vocabulary recognises DEFERRED, so a deferred table stops deriving as a scan surface (#19308)
Fixes #19260 Clause-②: no `EXCLUSION_DECL_NAME` in `scripts/pm/dispatch-gates.mjs` listed no `DEFERRED`, while `scripts/check-issue-citations.mjs` declares its exclusion table under exactly that word — `DEFERRED_SURFACES` / `DEFERRED_GLOBS` — and applies it as an exclusion: `surfaceFor` opens by returning `null` for every deferred glob. So the derivation read an **exclusion** table as a **scan** surface, and a `.changeset/**` path was told it triggers a gate that looks at nothing there. That derivation is the one PR #19259 (card #18224) trips on, and the assertion it reds — 「a changeset path alone reaches NO value-bearing family any more」 — is correct and is untouched here. One `DEFERRED` branch in the predicate; the measured census in the docblock above it re-measured and rewritten in the same stroke; one self-test case beside the existing named-spelling cases. ⛔ Nothing is renamed in `check-issue-citations.mjs` — `DEFERRED_SURFACES` and `DEFERRED_GLOBS` are exported and pinned by that gate's own self-test, and this docblock says the fix belongs on the derivation side: 「an author's next spelling should be met by this predicate rather than by a rediscovery of this card」. ## Acceptance notes ### Firing pair, both directions, on PR #19259's head The script derives from the tree it LIVES in — `ROOT` comes from `import.meta.url`, and `trackedFiles()` reads that root — so a `cwd` does **not** redirect it at another tree. The fixed file was therefore copied into a detached worktree of PR #19259's head (`6d1272b3850e5c0e756745dc6486d20aa330653d`, blob `5d78406d6` before, `8216826a5` while mutated) and run from there; the probe was restored with `git checkout HEAD -- scripts/pm/dispatch-gates.mjs` back to blob `5d78406d6`, `git diff HEAD` empty. `.changeset/test-abc.md` did not need to exist. | probe tree `6d1272b385` | `--commands` lines | `pnpm check:issue-citations` rows | |---|---|---| | subject `.changeset/test-abc.md`, before | 19 | **1** | | subject `.changeset/test-abc.md`, after | 18 | **0** | | control `packages/types/src/index.ts`, before | 44 | **1** | | control `packages/types/src/index.ts`, after | 44 | **1** | The subject's two command lists differ on exactly one line — `diff` reports hunk `13d12`, deleting the `pnpm check:issue-citations` line and nothing else — the control's two lists are byte-identical. Command: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack PATH`, exit 0 on all four runs. ### The census: the method it never stated, and what it now reads The block named its method only in prose and named no command, so the method is now spelled out in the block itself: enumerate `topLevelDecls` over every tracked JS/TS file under `scripts/`, keep the non-callable declarations, test each name against the predicate, and price the arm by diffing `extractWatchHints` against a build of this module whose predicate matches nothing. Attribution is the regex engine's own — leftmost position first, then alternation order (the old text said "first-match attribution" without saying which of the two, and they disagree on a multi-word name such as `EXCLUDED_SKIP`). Measured over the objectstack-ai/objectstack tree at `e6a03e6491`, with the export-visibility of six internal bindings as the only difference from the shipped file (a throwaway copy, never committed): | reading | docblock before | re-measured, no `DEFERRED` | re-measured, with `DEFERRED` | |---|---|---|---| | tracked JS/TS files under `scripts/` | 230 | 287 | 287 | | top-level VALUE declarations | 3,513 | 4,697 | 4,697 | | identifiers matching the predicate | 64 | 73 | **75** | | of those, carrying a string literal | 58 | 65 | **66** | | hints the set moves | 14 | 16 | **24** | | files it moves them on | 6 | 7 | **8** | | change no derivation / really leave one | 8 / 6 | — | **13 / 11** | Per-word tally, same ordering as the block: `SKIP 54, EXCLUDED 10, EXCLUSION 4, NOISE 2, SKIPPED 2, DEFERRED 2, EXCLUSIONS 1, and EXCLUDE / EXCLUDES / IGNORE / IGNORED 0` — four zero-scoring arms now, not three, because `EXCLUDES` went 1 → 0 on its own. ⚠️ **Every one of those numbers was already stale before this card**: the middle column is the same tree with the OLD predicate, so the drift from `230 / 3,513 / 64 / 58 / 14` is the tree moving, not the arm. Nothing reds when it does, which is why the block now carries the tree ref it was read on. `DEFERRED` matches exactly two declarations on this tree, both in `check-issue-citations.mjs`, and both are that gate's own exclusion table. It moves 8 hints off that gate, 5 of which cost nothing — the test globs sit under the gate's own `packages/**` inclusion, so `hintCovers` still reaches a test path — and **3 of which were fabricated leads**: `.changeset/**` (the one the card measured), plus `scripts/**` and `docs/adr/**`, which nobody had named. The gate stays reachable through `packages/**`, `packages/**/src/**/*.ts` and its siblings, which is what keeps the control lit. Re-verified after the merge of `origin/main` `e6a03e6491`: every figure above reproduces, and the edit adds no top-level VALUE declaration to this file (112 before, 112 after), so the 4,697 holds for the delivered tree. ### Gates — every exit code captured before any pipe | command | exit | verdict line | |---|---|---| | `node scripts/pm/dispatch-gates.mjs --self-test` (this branch) | 0 | `✓ dispatch-gates self-test: 1867 cases pass.` | | `node scripts/pm/dispatch-gates.mjs --self-test` (pristine `e6a03e6491`) | 0 | `✓ dispatch-gates self-test: 1866 cases pass.` | | `pnpm check:pm-dispatch-gates` (detached, under the shared lock) | 0 | `check:pm-dispatch-gates: the battery took 1012.6s on this box.` | | `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | 0 | no output | | the other 27 derived families | 0 each | listed below | Case count 1866 → 1867: the one new case is the `DEFERRED` entry in the named-spelling loop beside the existing exclusion-vocabulary cases, ⛔ not at the tail of `selfTest()` — the tail regions that PR #19162 (`:22173`–`:22203`, now landed as `e6a03e6`) and PR #19024 (`:23763`) touch are untouched here, and the merge of `origin/main` carrying #19162 was clean. Inside the battery the case 「⭐ a changeset path alone reaches NO value-bearing family any more」 — the one red on PR #19259's head — now reads green. Families derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (28, recomputed post-merge and identical to the pre-merge list) and reconciled with `--ran`: `28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of them is 3)`. The 27 besides the battery: `check-ci-filter-parity`, `check-closing-keyword-parity` (+ self-test), `check-comment-mask-corpus`, `check-declaration-mirrors` (+ self-test), `check-scripts-symbol-anchors` (+ self-test), `check-self-test-wired` (+ self-test), `check-self-test-workflow-commands` (+ self-test), `check-whole-set-label-write` (+ self-test), `check:agent-test-spelling`, `check:bash32-floor`, `check:cli-command-ids`, `check:cross-package-test-inputs`, `check:declared-population-live`, `check:driver-memory-census`, `check:entry-guard`, `check:nul-bytes`, `check:parse-guard`, `check:pnpm-filter-targets`, `check:ratchet-remedy-authority`, `check:refd-timer-probe`, `check:watch-hint-literal` — all exit 0. Control-byte self-scan beyond the gate, exit captured before any pipe: `grep -naP` over the changed file for the C0 range plus DEL exits 1 — none present. ### Line budget and shape `scripts/pm/dispatch-gates.mjs` 28,345 → 28,371 lines against `origin/main` `e6a03e6491`: **net +26** (+64 / −38), inside the +40 the dispatch set. Measured against the 28,337 the dispatch quoted at `7d0f911`/`7ec8534`, the file reads +34, of which +8 are PR #19162's own. `git diff --stat` shows one file. No changeset: `scripts/` ships in no published package's `files[]` (checked across every non-private manifest in the tree), and `scripts/pm/**` is on the non-publishing fast track. ⚠️ So `Check Changeset` needs the `skip-changeset` label, and this dispatch forbids label writes to the dev — **that one write is the seat's**, not left undone by accident. ### Noted, not filed **The census in this docblock was already stale before this card, and nothing reds when it goes stale.** The middle column of the census table above is the same tree read with the OLD predicate: `230 → 287` files, `3,513 → 4,697` declarations, `64 → 73` matches, `14 → 16` moved hints, and the tally's `EXCLUDES 1 → 0`. The sibling gate keeps its own census as code with a `measuredOn` ref (`CENSUS_17512` in `check-issue-citations.mjs`, marked 「⛔ Readings, not a budget」); this one is prose in a comment, so it rots silently. Not filed — it is an observation about a missing guard, not a reproducible defect, a broken declared contract or an authoring trap. 承接者: the standing queue on this same file (#19070 → #19104 → #19105 → #19106 → #19172), any of which reads this block. The mitigation this PR does ship is the tree ref and the spelled-out method, so the next reader can tell a stale number from a current one. ## 维护者速读(草稿) **改了什么** — 派单工具 `dispatch-gates` 里那张「哪些常量名代表『这个门禁不看这里』」的词表,补上了 `DEFERRED` 这个拼写。顺带把该处注释里那份实测普查重新测了一遍并改写,因为它早已过期。 **为什么改** — `check-issue-citations` 这个门禁用 `DEFERRED_SURFACES` 声明它**故意不看**的路径。 词表不认这个词,于是工具把「不看的清单」读成了「要看的清单」,反过来告诉开发者:改一个 changeset 文件会触发这个门禁 —— 而该门禁对 changeset 路径其实什么都不做。后果不止是一条假线索:它还让 PR #19259 在 `lint.yml` 第 32 步整条中止,#18224 自己新加的两步从未执行,卡住了一张 p2。实测这条修法还顺手消掉 另外两条没人发现的假线索(`scripts/**` 与 `docs/adr/**`)。 **风险与代价(含回滚)** — 风险很低:改动是一个正则分支加注释,只影响派单提示里「你该跑哪些门禁」这份 清单,不影响任何门禁自身的判定,也不改任何对外发布的包。方向上只会**少给**一条线索、不会多给,而这一侧 的失效代价是「一张卡多跑一轮 CI」,比反方向「每张卡都被塞一条假线索」便宜得多 —— 这个不对称是该文件自己 写下的判据。回滚 = revert 这一个提交,单文件、无生成物、无迁移。 **席位意见** **你要做的** — ① 这是 draft PR,按席位流程补 `## Contract review` 记录后再走 ready + auto-merge; ② `skip-changeset` 标签需要席位来打(本单禁止开发侧写标签),否则 `Check Changeset` 会红; ③ 落地后 PR #19259 / 卡 #18224 即可重跑,它那条断言本身是对的、本 PR 未动。 --- _Generated by [Claude Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 84a7e9e commit 3d18877

1 file changed

Lines changed: 64 additions & 38 deletions

File tree

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5886,57 +5886,81 @@ export function packageRootAnchoredHint(hint, base, tree, files) {
58865886
*
58875887
* ## The census, measured on this tree
58885888
*
5889-
* 3,513 top-level VALUE declarations over the 230 tracked JS/TS files under
5890-
* `scripts/`. 64 identifiers match the predicate; 58 carry at least one string
5891-
* literal; the whole set moves 14 hints, across 6 files:
5892-
*
5893-
* scripts/check-doc-authoring.mjs SKIP_PATHS, SKIP_FILES,
5889+
* Re-measured with the `DEFERRED` arm in place, over the objectstack-ai/objectstack
5890+
* tree at `e6a03e6491`. The METHOD, which this block used to leave implicit: run
5891+
* `topLevelDecls` over every tracked JS/TS file under `scripts/`, keep the
5892+
* non-callable declarations, test each name against the predicate, and price
5893+
* the arm by diffing `extractWatchHints` against a build of this module whose
5894+
* predicate matches nothing. Attribution is the regex engine's own — leftmost
5895+
* position first, then alternation order.
5896+
*
5897+
* 4,697 top-level VALUE declarations over the 287 tracked JS/TS files under
5898+
* `scripts/`. 75 identifiers match the predicate; 66 carry at least one string
5899+
* literal; the whole set moves 24 hints, across 8 files:
5900+
*
5901+
* scripts/check-issue-citations.mjs DEFERRED_SURFACES 8
5902+
* scripts/check-doc-authoring.mjs SKIP_DIRS, SKIP_PATHS, SKIP_FILES,
58945903
* PACKAGES_PROSE_EXCLUDED 6
58955904
* scripts/check-refd-timer-probe.mjs EXCLUDED_DIRS 4
5896-
* scripts/check-corpus-claim-drift.mjs SKIP_SUBTREES 1
5897-
* scripts/check-role-word.mjs SKIP_SUBTREES 1
5905+
* scripts/pm/measurement-claim-triage.mjs EXCLUDED, SKIP_DIRS 2
5906+
* scripts/check-corpus-claim-drift.mjs SKIP_DIRS, SKIP_SUBTREES 1
5907+
* scripts/check-role-word.mjs SKIP_DIRS, SKIP_SUBTREES 1
58985908
* scripts/check-keyed-text-bounds.mjs SKIP_DIRS 1
5899-
* scripts/pm/check-half-states.mjs H36_SHARED_PREFIX_NOISE 1
5909+
* scripts/pm/check-half-states.mjs H36_SHARED_PATH_NOISE,
5910+
* H36_SHARED_PREFIX_NOISE 1
59005911
*
59015912
* Each was read against the gate that declares it, and each is an exclusion in
59025913
* that gate's own words: "whole subtrees skipped by path", "generated
59035914
* subtrees, excluded by PATH under ROOTS", "generated from spec/frontmatter —
59045915
* not hand-authored, don't police", "directories `git ls-files` can still name
5905-
* that hold no authored source", and — for `PACKAGES_PROSE_EXCLUDED`, the one
5906-
* that is a bare string rather than a list — the `continue` in the gate's own
5907-
* `descend` that skips it.
5908-
*
5909-
* ## What the 14 cost, which is not 14
5910-
*
5911-
* EIGHT of them change no derivation at all, because the gate ALSO declares the
5912-
* containing root as an inclusion population and `hintCovers` still reaches the
5913-
* path through that. Measured per hint, probing under each dropped hint against
5914-
* the surviving set: all six of check-doc-authoring's (`.claude/**`, `docs/**`,
5915-
* `content/**` and `packages/**` are its `ROOT_WATCH_HINTS`) and both
5916-
* `content/docs/references` (covered by `content/docs`). That gate's own
5917-
* self-test already said so from the other side — every `SKIP_PATHS` entry must
5918-
* sit UNDER a declared root — so the exclusion hints were pure duplication.
5919-
*
5920-
* SIX really leave a derivation, and every one of them is a lead that was
5916+
* that hold no authored source", "deliberately OUT, each with the reading that
5917+
* put it out", and — for `PACKAGES_PROSE_EXCLUDED`, the one that is a bare
5918+
* string rather than a list — the `continue` in the gate's own `descend` that
5919+
* skips it.
5920+
*
5921+
* ## What the 24 cost, which is not 24
5922+
*
5923+
* THIRTEEN of them change no derivation at all, because the gate ALSO declares
5924+
* the containing root as an inclusion population and `hintCovers` still reaches
5925+
* the path through that. Measured per hint, probing under each dropped hint
5926+
* against the surviving set: all six of check-doc-authoring's (`.claude/**`,
5927+
* `docs/**`, `content/**` and `packages/**` are its `ROOT_WATCH_HINTS`), both
5928+
* `content/docs/references` (covered by `content/docs`), and the five test
5929+
* globs of the deferred table (covered by check-issue-citations' own
5930+
* `packages/**`). That gate's own self-test already said so from the other
5931+
* side — every `SKIP_PATHS` entry must sit UNDER a declared root — so those
5932+
* exclusion hints were pure duplication.
5933+
*
5934+
* ELEVEN really leave a derivation, and every one of them is a lead that was
59215935
* false: `node_modules`, `dist`, `coverage` and `.turbo` off
5922-
* check-refd-timer-probe's skip set, and `.changeset` twice — off
5936+
* check-refd-timer-probe's skip set; `.changeset` twice — off
59235937
* check-keyed-text-bounds' `SKIP_DIRS` and off the noise floor the card was
5924-
* filed on. The changeset pair is what a dev actually saw: a card that has not
5925-
* written its changeset yet is told which families it will owe once it does,
5926-
* and that projection carried FOUR fabricated rows, 16 -> 12 — including
5938+
* filed on; the two gate paths measurement-claim-triage declares it skips; and
5939+
* `scripts/**`, `docs/adr/**` and `.changeset/**` off `DEFERRED_SURFACES`,
5940+
* which is what the `DEFERRED` arm retired. The changeset pair is what a dev
5941+
* actually saw when #15753 was filed: a card that has not written its
5942+
* changeset yet is told which families it will owe once it does, and that
5943+
* projection carried FOUR fabricated rows, 16 -> 12 — including
59275944
* `check-half-states.mjs --format=markdown --provenance="$PROVENANCE"`, the
59285945
* networked half-state-patrol sweep, advertised to every card in the tree as a
5929-
* gate its changeset would trigger.
5946+
* gate its changeset would trigger. The deferred table is that same reading
5947+
* one gate over, and it is why this arm exists: `surfaceFor` opens by
5948+
* returning `null` for every deferred glob, so `check:issue-citations` was
5949+
* offered to a changeset path as a gate it triggers while the gate looks at
5950+
* nothing there.
59305951
*
59315952
* ## The predicate: what the census kept, and what it retired
59325953
*
59335954
* `DENY`/`DENIED` was measured and REMOVED. It matched exactly one declaration
59345955
* on this tree and that one is a false positive — an HTTP fixture, not an
59355956
* exclusion list — and "deny" in this tree names AUTHORIZATION vocabulary
5936-
* (`DENY_CODE`), never a path skip list. Matches for the surviving
5937-
* alternatives, first-match attribution: SKIP 47, EXCLUDED 9, NOISE 2,
5938-
* SKIPPED 2, EXCLUSION 2, EXCLUSIONS 1, EXCLUDES 1, and EXCLUDE / IGNORE /
5939-
* IGNORED 0. The three zero-scoring arms are kept deliberately and the reason is
5957+
* (`DENY_CODE`), never a path skip list. `DEFERRED` was measured and ADDED: it
5958+
* matches two declarations on this tree, `DEFERRED_SURFACES` and
5959+
* `DEFERRED_GLOBS` in `check-issue-citations.mjs`, and both are that gate's
5960+
* own exclusion table. Matches for the surviving alternatives, first-match
5961+
* attribution: SKIP 54, EXCLUDED 10, EXCLUSION 4, NOISE 2, SKIPPED 2,
5962+
* DEFERRED 2, EXCLUSIONS 1, and EXCLUDE / EXCLUDES / IGNORE / IGNORED 0. The
5963+
* four zero-scoring arms are kept deliberately and the reason is
59405964
* the direction this predicate fails in: over-matching DROPS a hint (a missing
59415965
* lead — one card, one CI round), while under-matching KEEPS a wrong one (a
59425966
* fabricated lead pasted into every dispatch prompt whose surface brushes it).
@@ -5959,14 +5983,16 @@ export function packageRootAnchoredHint(hint, base, tree, files) {
59595983
* is not reached — `check-test-completeness.mjs` has the one instance on this
59605984
* tree, and it costs nothing today because every literal in it is a bare
59615985
* directory word the admission rule already refuses. camelCase spellings are
5962-
* not reached either: the anchor is the SCREAMING_SNAKE segment, and the four
5963-
* camelCase near-misses on this tree (`scripts/docs-audit/affected-docs.mjs`)
5964-
* are counters and note strings, not populations. Both are the direction that
5986+
* not reached either: the anchor is the SCREAMING_SNAKE segment, and the seven
5987+
* camelCase near-misses on this tree (five in
5988+
* `scripts/docs-audit/affected-docs.mjs`, one in
5989+
* `scripts/check-type-check-coverage.mjs`, one here) are counters, note strings
5990+
* and memo caches, not populations. Both are the direction that
59655991
* drops LESS, which is the direction a widening of this rule may not silently
59665992
* take.
59675993
*/
59685994
const EXCLUSION_DECL_NAME =
5969-
/(?:^|_)(?:NOISE|SKIP|SKIPPED|EXCLUDE|EXCLUDED|EXCLUDES|EXCLUSION|EXCLUSIONS|IGNORE|IGNORED)(?:_|$)/;
5995+
/(?:^|_)(?:NOISE|SKIP|SKIPPED|DEFERRED|EXCLUDE|EXCLUDED|EXCLUDES|EXCLUSION|EXCLUSIONS|IGNORE|IGNORED)(?:_|$)/;
59705996

59715997
/** `topLevelDecls` classifies self-tests for its OTHER caller; this one has no stake in it. */
59725998
const NO_SELF_TEST_STARTS = new Set();
@@ -17145,7 +17171,7 @@ function selfTest() {
1714517171
t('and a declaration AFTER it is unaffected — the span closes where the statement does', multilineHints.includes('packages/core/src'));
1714617172
// The named spellings, one case each, so a narrowing of the predicate is
1714717173
// visible here rather than only in the live census.
17148-
for (const word of ['SKIP', 'EXCLUDE', 'EXCLUDED', 'EXCLUSIONS', 'IGNORE']) {
17174+
for (const word of ['SKIP', 'EXCLUDE', 'EXCLUDED', 'EXCLUSIONS', 'IGNORE', 'DEFERRED']) {
1714917175
const named = `const ${word}_PATHS = ['packages/skipped/src'];`;
1715017176
t(`\`${word}\` names an exclusion too — the predicate is the convention, not one constant`, !extractWatchHints(named).includes('packages/skipped/src'));
1715117177
}

0 commit comments

Comments
 (0)