You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c739de7
Browse filesBrowse the repository at this point in the historyBrowse files
authored
tooling(dispatch-gates): the STALE TREE reading keeps only the moved paths the derivation actually opened (#26585)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ87iti5Co7zHGEHxwKnxJVDN)_Fixes#26334
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`scripts/pm/dispatch-gates.mjs` 的 STALE TREE
读数只再数派生真正打开过的文件(`derivationInputs`,从 discovery 实测:全部 workflow、跟过的
composite action、根
`package.json`、每个门禁族的脚本/import/spawn/按路径读取/包清单,加引擎自身闭包);`scripts/`
下没人读的生成记录(sdui 清单记录、ADR 锚点)移动时不再误报,安静行点名被排除的路径。
- 为什么改:ui/重构落地每次都动 `scripts/` 下的生成记录,pm-only 树因此每次都读到假 STALE
TREE(卡上实测派生集 52 不变),警告被忽视就失去意义。
- 风险与回滚:只改读数的过滤,不改派生族、不改任何门禁命令;一个不该排除的输入会漏报——自检用实测集钉住闭包(清单、全部
workflow、每个门禁文件、引擎与其数据模块);回滚即 revert 本 PR 的合并提交。
- 先看:`scripts/pm/dispatch-gates.mjs` 的 `derivationInputs` /
`liveDerivationInputs`(排除规则与降级方向)、`baseDrift` 的 `narrowed`
hunk、`driftLines` 安静分支;然后 `dispatch-gates.self-test.mjs` 的 base drift 段。
- 两分钟验证:在落后 `origin/main` 且只差一次 ui 落地的 pm 检出跑 `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`,预期 stderr 的 banner 是 "none of the commit(s)
this tree can SEE touched … 1 path(s) under the surface moved too, none
of them an input of this answer: scripts/sdui-manifest.record.json",不再出现
STALE TREE(`--tier` 不做派生,仍列原始清单并注明未收窄)。
- 需要你做的:无,席位落地。
## What changes
`DERIVATION_SURFACE` stays the pathspec roots the staleness diff is
scoped to (`.github/workflows`, `.github/actions`, `package.json`,
`scripts`). What is new is that the moved paths under those roots are
**narrowed to the files the derivation actually opened**, measured
rather than listed — the "narrowed surface" option of the two the order
named, chosen because its self-test can prove the negative from the
discovery itself instead of from a hand list that would rot the next
time a generated record lands under `scripts/`.
- `derivationInputs()` (new, exported): the measured set — every
workflow file, every composite action the follow opened, the root
`package.json`, and for every discovered family its gate files,
first-party imports, spawned programs, program-text reads and package
manifests, plus the engine's own module closure (this file, its
first-party imports — the data module two levels down from every family
— and the battery it loads by `import()`). Memoised per discovery, which
is memoised per tree, so a derivation that already discovered pays
nothing more.
- `narrowToDerivationInputs(moved, inputs)` (new, exported): the pure
split into `changed` (inputs) and `ignored` (under the roots, not
inputs). `inputs` null keeps everything — the superset reading.
- `baseDrift({ cwd, inputs })`: reads the same three-dot diff, then
narrows it by the `inputs` its CALLER names — a set, or a function asked
only once something moved under the roots. The reading gains an
`ignored` field beside `changed` (`null` = not narrowed), and only
`changed` decides loud vs quiet. The CLI's deriving modes (`--commands`,
`--ran`, the human list) hand in `liveDerivationInputs`, which costs
nothing extra because the derivation discovers the same tree through the
same memo; `--tier` reads no workflow by contract, so it hands in
nothing and its loud reading says it was not narrowed. A first draft let
`baseDrift` discover on its own: a battery of `--tier` children on a
stale tree then paid a ~14 s discovery each and the full self-test ran
24 minutes — so the choice moved to the caller. A discovery that throws
degrades to no narrowing (over-report, never under-report).
- `driftLines`: the quiet branch names what it set aside (`N path(s)
under the surface moved too, none of them an input of this answer: …`,
first three named), so the quiet reads as a measurement and not as the
silence a level tree gets. A reading that set nothing aside renders
byte-identically to before.
- Derived families and gate commands: untouched — no discovery code
changed; `--commands` derives the same 30 families on a51cd08 and on
4b203a9.
The card's named specimen, `scripts/sdui-manifest.sources/**`, had
already been retired on `main` by e931621 (2026-10-27T01:15Z, #26367)
before the seat's premise check — see Acceptance notes — so the live
specimens are `scripts/sdui-manifest.record.json` (moves on every ui
landing that regenerates the manifest) and `scripts/adr-anchors/*.json`
(moves on refactor landings); the fix closes the class rather than the
one path.
## Tests
`node scripts/pm/dispatch-gates.mjs --self-test` (both tiers) on head
4b203a9: `✓ dispatch-gates self-test: 2224 cases pass.` (`fast 1935
case(s) in 353.9s · slow 289 case(s) in 974.3s`). ⚠️ The base-drift
section is in the SLOW tier, and CI runs only the fast tier on
`pull_request` / `merge_group` (`scripts/pm/check-dispatch-gates.mjs`,
#22991) — so this PR's check-runs do not exercise the new
temporary-repository and LIVE cases; this local two-tier run is their
evidence, and the hourly `schedule` run is the first CI run that will.
The full run also caught two slow-tier-only ordering bugs in the first
draft of these cases (a case reading a fixture above its declaration);
fixed in 9a12ebe and 4b203a9, and every identifier the new cases read
was then checked declared-before-read. New cases in the base-drift
section, one per direction the order asked for and the live pins the
measured set needs:
- FRESH — temporary repository: upstream regenerates
`scripts/sdui-manifest.sources/core.json` under the roots; the raw
reading reports it (the departure pin: what used to go loud); narrowed
by the inputs the same tree reads `changed: []`, `ignored: [that
record]`, and renders the quiet two-line shape naming the set-aside
record, no `STALE TREE`; a foreign checkout handed no inputs is not
narrowed.
- STALE — same repository: a changed `scripts/check-thing.mjs` still
goes loud and is named alone (`Stale here: scripts/check-thing.mjs`)
with the record still set aside; a workflow
(`.github/workflows/lint.yml`) and the root `package.json` moving
upstream go loud, both named.
- LIVE pins on this tree's measured set: every input is a tracked file;
it holds the root manifest and every workflow file; this engine, its
data module and its battery; every gate file of every discovered family;
it is a strict subset of the tracked files under `scripts/` (265 of 442
on 4b203a9), so the narrowing is not vacuous; `liveDerivationInputs()`
answers that same set; and `scripts/sdui-manifest.record.json` is
tracked and NOT an input.
- Mode split: a level tree never asks for the inputs (a throwing
function proves it is not called); inputs handed in as a function read
identically to the set; an un-narrowed loud reading says `Not narrowed
to what the answer is made of … A --commands or --ran run measures the
inputs and lists only those.`, a narrowed one does not.
- Renderings: the quiet line with set-aside paths (counted past the
third), byte-identity when nothing was set aside, the pure split's
order.
## Acceptance notes
- **Premise, measured** (os-dev rule 6): the card's trigger directory
`scripts/sdui-manifest.sources/` does not exist on `main` — retired by
e931621 (2026-10-27T01:15Z, "retire the per-package SDUI manifest
records", #26367), after the card was filed (2026-10-26T21:5xZ) and
before the seat's read-only premise check at b53322d (`git ls-tree
b53322d scripts/` lists `scripts/sdui-manifest.record.json`,
`check-sdui-manifest.mjs`, `gen-sdui-manifest-node.mjs` and no
`sdui-manifest.sources`). The mechanism the card names holds unchanged:
the whole `scripts/` root was counted, and generated records under it
still move on landings in other lanes —
`scripts/sdui-manifest.record.json` at 1bbb7bf (ui),
`scripts/adr-anchors/*.json` at b7f8a2e, 2e7127b (refactor) — and the
derivation opens none of them (measured: 265 of 442 tracked `scripts/**`
files are inputs on 4b203a9). The card's "Wanted" is met for the class;
`premise_still_valid: true` with that correction.
- **Inputs outside the roots** (observation, not fixed here): the
measured set holds 43 files outside `DERIVATION_SURFACE` — gate scripts
resolved through package manifests (`packages/spec/scripts/check-*.ts`,
`packages/lint/scripts/*.mjs`), `packages/spec/package.json`, and
program-text reads such as `.claude/skills/pm-dispatch/SKILL.md`. The
diff stays scoped to the roots, as the ruling's "nothing wider" asks, so
an upstream change to one of those still reads as off-surface, exactly
as before this PR. Widening the diff to the measured set would be the
honest next step; it is a separate decision.
- The `fix:` remedy in the third unmeasured door still prints the raw
`git diff --name-only … -- ROOTS` command: it is the superset a reader
can run by hand; the narrowing is applied on top of it and is not
something a shell one-liner can reproduce.
- A changed `dispatch-gates.self-test.mjs` upstream counts as an input
(the engine closure includes the battery) — a conservative choice: it
does not move the answer, but `check:pm-dispatch-gates` runs it, and
over-reporting is this file's degraded direction.
## Verification
Three real landings replayed on throwaway checkouts (a `--shared` clone
of this worktree with `refs/remotes/origin/main` pinned at the landing
and HEAD at its parent; "before" = that tree's own engine, `--tier`;
"after" = this PR's engine module closure committed on top, run in a
deriving mode, `--commands`):
<details><summary>1bbb7bf4 — a ui landing whose only movement under the
roots is <code>scripts/sdui-manifest.record.json</code> (the card's
class)</summary>
Before:
```
⚠️ STALE TREE — this answer is derived from a tree at least 1 commit(s) behind origin/main, and 1 file(s) it derives from CHANGED across that range.
Stale here: scripts/sdui-manifest.record.json
```
After:
```
At least 1 commit(s) behind origin/main, and none of the commit(s) this tree can SEE touched what this answer derives from — HEAD 2026-10-26T12:40:29+00:00 vs origin/main 1bbb7bf 2026-10-26T12:45:21+00:00. 1 path(s) under the surface moved too, none of them an input of this answer: scripts/sdui-manifest.record.json.
```
</details>
<details><summary>b7f8a2eb — a refactor landing whose only movement
under the roots is an ADR anchor record</summary>
Before: `STALE TREE … 1 file(s) it derives from CHANGED` / `Stale here:
scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json`.
After: `none of the commit(s) this tree can SEE touched what this answer
derives from … 1 path(s) under the surface moved too, none of them an
input of this answer:
scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json.`
</details>
<details><summary>e931621a — the sentinel retirement itself: two
workflows and three gate scripts moved beside 27 records (the control
that must stay STALE)</summary>
Before: `STALE TREE … 32 file(s) it derives from CHANGED` / `Stale here:
.github/workflows/ci.yml .github/workflows/lint.yml
scripts/check-changeset-no-major.mjs scripts/check-sdui-manifest.mjs
scripts/gen-sdui-manifest-node.mjs scripts/sdui-manifest.record.json …
+26 more`.
After: `STALE TREE … 5 file(s) it derives from CHANGED` / `Stale here:
.github/workflows/ci.yml .github/workflows/lint.yml
scripts/check-changeset-no-major.mjs scripts/check-sdui-manifest.mjs
scripts/gen-sdui-manifest-node.mjs` — the verdict for a real
derivation-input change is unchanged; only the records left the list.
</details>
The `--ran` reconciliation on this branch, head 4b203a9 (no drift line:
the tree was level with the fetched `origin/main` when it ran):
<details><summary>Derived gates — 30 families, run locally on 4b203a9,
reconciled with <code>--ran</code></summary>
```
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:declared-population-live :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
Run reconciliation — 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero — all 30 recorded an exit code and none of them is 3).
```
</details>
`check:dual-build-cjs-loads` is not in this card's derived list.
`--tier` on each throwaway tree now prints the raw list plus the
not-narrowed line (e.g. 32 files on e931621), by design. No package is
touched (no ① closure, no ② package suite); `pnpm lint` as a repo-wide
sweep is CI's run — `eslint scripts/pm/dispatch-gates.mjs
scripts/pm/dispatch-gates.self-test.mjs` exits 0 locally.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(n/a — no schema touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the two files,
exit 0; the repo sweep is CI's)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
(`scripts/pm/**` publishes nothing — `skip-changeset`)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (n/a)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbZ5vD35MzQ8UqhhTJB
---------
Co-authored-by: os-warren <warren@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
? ` ${ignored.length} path(s) under the surface moved too, none of them an input of this answer: ${ignored.slice(0, 3).join(' ')}${ignored.length > 3 ? ` … +${ignored.length - 3} more` : ''}.`
15834
+
: '';
15694
15835
return [
15695
-
` At least ${behind} commit(s) behind ${DEFAULT_BASE_REF}, and none of the commit(s) this tree can SEE touched what this answer derives from — ${span}.`,
15836
+
` At least ${behind} commit(s) behind ${DEFAULT_BASE_REF}, and none of the commit(s) this tree can SEE touched what this answer derives from — ${span}.${setAside}`,
15696
15837
` Whether UNSEEN upstream work did, this run cannot tell: the range above ends at ${DEFAULT_BASE_REF}, a LOCAL snapshot only a fetch moves — not at upstream. Run 'git fetch ${DEFAULT_BASE_REMOTE} ${DEFAULT_BASE_BRANCH}' and derive again for the strongest reading a checkout can take.`,
15697
15838
];
15698
15839
}
@@ -15702,6 +15843,14 @@ export function driftLines(drift) {
` Those files ARE the families printed below, so this run read their old copies and still exited 0 — a well-formed answer about a tree nobody is on.`,
15704
15845
` "At least": ${DEFAULT_BASE_REF} is a LOCAL ref only a fetch moves. Run 'git fetch ${DEFAULT_BASE_REMOTE} ${DEFAULT_BASE_BRANCH}' and derive again from a tree at ${DEFAULT_BASE_REF}.`,
15846
+
// A list nobody narrowed may name paths the answer is not made of — a
15847
+
// generated record under `scripts/` — and a reader cannot tell which from
15848
+
// the list alone, so the reading says so and names the run that measures
15849
+
// it (#26334). Absent from a narrowed reading: there the list IS the
15850
+
// inputs.
15851
+
...(Array.isArray(ignored) ? [] : [
15852
+
` Not narrowed to what the answer is made of: this run measured no derivation, so every moved path under the surface is listed, generated records included. A --commands or --ran run measures the inputs and lists only those.`,
15853
+
]),
15705
15854
];
15706
15855
}
15707
15856
@@ -16225,7 +16374,12 @@ if (invokedDirectly) {
16225
16374
for (const line of sisterTier.stdout) console.log(line);
16226
16375
process.exit(0);
16227
16376
}
16228
-
for (const line of bannerLines({ identity, paths: declaredPaths, drift: baseDrift() })) console.error(line);
16377
+
// `--tier` reads no workflow and no check script by contract (its branch
16378
+
// below), so it hands the drift no inputs to narrow by and its loud reading
16379
+
// says so; every deriving mode hands in the measured set, which it is about
0 commit comments