Repository navigation
Commit 300f740
fix(pm): dispatch-gates derives the published-README export gate from the claims a diff takes away (#31314)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X3mVDakofKE6MBihrPXQcML)_
Fixes #31292
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`dispatch-gates --commands` 新增一条派生行,把
`check:published-readme-exports`(已发布 README 里写的 import
必须在包的已构建类型里存在)派给「真的可能让它变红」的改动:README 新写了 import 或成员调用、`package.json`
的发布字段(`exports`/`types`/`files` 等)变了、或某个被 README 引用的名字在源码里被删除或改名。
- 为什么改:这道门禁在 CI 每个 PR 都跑,但本地派生清单从不列出它(#31257 唯一剩下的一族);它读 68
个包的构建产物,按路径派生会派给几乎每张卡,所以改为按「README 声称的名字」与「这次改动删掉的声明」比对。
- 风险与回滚:只动 `scripts/pm/` 下的派生工具与自测,不改任何门禁检查什么,不动
workflow。最坏情况是多派一次(需要先构建)或漏派(即现状)。回滚即还原本 PR。
- 先看:`scripts/pm/dispatch-gates.mjs` 的 `takesAReadmeClaim` 与
`publishedReadmeReach`;`scripts/pm/dispatch-gates.data.mjs`
新增的那一行(含构建前置命令)。
- 两分钟验证:在本分支把 `packages/core/src` 里 `ObjectKernel` 类改名后跑 `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`,预期清单出现 `pnpm
check:published-readme-exports`;还原后不再出现。
- 需要你做的:无,席位落地。
## What changes
`check:published-readme-exports` runs on every PR (`lint.yml`, after the
workspace build) and `--commands` never handed it to a card: it sat in
the artifact-roster block, and the seat ruling on #30763 keeps that
block out of `--commands` wholesale. It is the one family #31257 left
(PR #31291 closed the other seventeen), because its reach is the BUILT
type entries of 68 packages that 129 published documents import from, so
a path net would name it for nearly every card.
This PR adds one change-kind row, `published-readme-claim`, bound to a
**claim net**: it reads what the published documents claim and asks
whether the diff takes one of those claims away. Three limbs, each
judged against the blob at the change set's merge base (the working tree
is the head):
| Limb | Hit when | Not a hit |
|---|---|---|
| DOCUMENT | a published document (the gate's own definition) whose
fenced code makes a claim the base did not: a new scoped or workspace
import, a new name on one, a new `receiver.path.member(` site | a
prose-only edit, a foreign-scope import, a deleted document |
| MANIFEST | a workspace member's `package.json` whose `name`,
`private`, `files`, `exports`, `types` or `typings` changed; the
workspace file's `packages:` globs | a version bump, a dependency edit,
workspace `overrides` |
| MEMBER | a non-test TypeScript source in a reached package (the
imported package or its workspace dependency closure) that declares a
claimed name FEWER times than the base did, or drops an `export * from`
line | an addition, an edit around a claimed name, a comment, an
unclaimed name, a new file, a test or `dist` file |
The names are the gate's own, read by copies of its extraction
(`readFences`, `extractImports`, `memberCallSites`, the one-hop `const x
= [await] [new] Name(` receivers). Where a copy must choose, it is
WIDER: every candidate receiver is kept where the gate drops a shadowed
one. The gate's module loads `typescript`, so it is held to the copy
through a spawned child in the self-test rather than imported (the
no-install rule). The workspace parse is imported from
`scripts/workspace-enumerator.mjs`, the module the gate itself uses
(dependency-free, no path population).
The row's lead names the build prerequisite runnably, as `lint.yml`
spells it (`pnpm exec turbo run build --filter=./packages/*
--filter=./packages/*/*`), and says that the gate's exit 3 on an unbuilt
tree is NOT MEASURED, never green, so a `--ran` record carrying `:: exit
3` reconciles it as NOT-MEASURED.
### Recall, against the gate's own reader (at `7c523aa1`)
- Documents: the net reads exactly the gate's 129
(`outsideUiTree(publishedDocs())`), none differing.
- Claims: the gate's two halves check 364 (package, name) claims; the
net reads 377, and every one of the 364 is among them.
- Declarations: each of the 364 names is declared, as the net's
declaration reader sees it, by a non-test source in its package's
closure (377 of 377 net claims too), so removing or renaming that
declaration is a hit.
### Precision, replayed over the last 500 first-parent commits on `main`
303 of them touched a reached package's TypeScript; **17 would derive
the gate** (10 by a removed claimed name, 7 by a manifest field, 3 by a
new document claim). For comparison, a path net over the reached
packages would have derived it for every one of the 303.
## Tests
New cases in `scripts/pm/dispatch-gates.self-test.mjs`:
- **Limbs, through injected base/head readers:** renaming a claimed
class, removing a claimed method, deleting a file that declared one, and
dropping an `export * from` line are hits; adding a method, a new file,
removing an unclaimed name, a claimed name dropped from a comment only,
a test file, a `dist` file and a path in no reached package are misses.
A new scoped import and a new member call on a built receiver in a live
published document are hits; a prose-only edit, a foreign-scope import
and a deleted document are not. A changed `exports` and changed
workspace globs are hits; a version bump and changed `overrides` are
not. Each declaration shape the reader counts is pinned.
- **The reader held to the gate's own (spawned child):** the same
document set; every claim the gate checks is a claim the net reads; the
gate's source still spells what the net copies (the excluded markdown,
the skip dirs, the call-site matcher, the receiver declaration, the
published-package test, the `ui` tree) and its fence languages are
exactly the net's. A tree with no published document throws rather than
reaching nothing.
- **The row:** exactly one entry names the gate, bound to the claim net;
the rendered lead names the gate runnably, its build prerequisite and
exit 3 as NOT MEASURED; the build filter covers every package directory
the documents reach; a docs page and an unrelated script derive no
README gate through the whole table.
- **Slow tier:** each of the gate's claims has a declaration the net
reads in its package closure.
- **Pin moved:** the inherited-population census row 748 → 751 (the
import line and its two comment lines).
**Ablation** (one fast-tier run, four mutations through nested
`scripts/ablation-replace.mjs`; restore proven four times: blob == HEAD
`e7178e75c1fe`, `git diff HEAD` empty), 6 of 2205 cases red, each named:
- the claimed-name filter replaced by "any removed name" ⇒ "MEMBER:
removing an unclaimed name is not a hit";
- the removal comparison `<` widened to `<=` ⇒ "MEMBER: adding a method
is not a hit", "MEMBER: a claimed name dropped from a comment only is
not a hit";
- the document claim diff replaced by "any claim" ⇒ "DOCUMENT: a
prose-only edit is not a hit", "DOCUMENT: a foreign-scope import is not
a hit";
- the manifest field comparison replaced by "any byte" ⇒ "MANIFEST: a
version bump is not a hit".
## Acceptance notes
- **Dispatch time.** On a dispatch-time surface (no diff yet) the
document and member limbs find nothing to compare and stay quiet; the
gate is owed on the dev's re-derivation over the real diff. A path net
would have answered the PM's question at the price of naming a
build-requiring gate on nearly every card.
- **What the net cannot see:** a file MOVED within a package reads as a
removal from its old path (a wasted run, the safe direction); a claimed
member a type inherits from outside the workspace has no workspace
declaration to remove; a build config that stops emitting an entry is
neither a manifest field nor a declaration.
- **Declaration reader is textual,** so it over-counts (a line-leading
call reads like a method), which only widens the net. The replay above
is the price.
- No `.github/**` edit; no change to what any gate checks.
## Verification
Gate union from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on this diff (3 paths vs merge base
`7c523aa10`): 30 families, all run on head `aa386612`, each exit 0,
reconciled with `--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)". The derivation does not name
`check:published-readme-exports` for this diff, as intended: it touches
no reached package, document or manifest.
<details><summary>30 gates, exit codes captured before any
pipe</summary>
- `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`),
`check-comment-mask-corpus.mjs`, `check-declaration-mirrors.mjs` (and
`--self-test`), `check-self-test-wired.mjs` (and `--self-test`),
`check-self-test-workflow-commands.mjs` (and `--self-test`),
`check-whole-set-label-write.mjs` (and `--self-test`),
`ci/plan-test-core-shards.mjs --self-test`: exit 0 each
- `pnpm check:agent-test-spelling`, `check:bash32-floor`,
`check:ci-filter-parity`, `check:cli-command-ids`,
`check:cross-package-test-inputs`, `check:declared-population-live`,
`check:driver-memory-census`, `check:entry-guard`,
`check:gitlink-declared`, `check:nul-bytes`, `check:parse-guard`,
`check:pnpm-filter-targets`, `check:pnpm-flag-spellings`,
`check:ratchet-remedy-authority`, `check:refd-timer-probe`,
`check:scripts-symbol-anchors`, `check:watch-hint-literal`: exit 0 each
- `pnpm check:pm-dispatch-gates`: exit 0, "✓ dispatch-gates self-test
(fast tier): 2205 cases pass; 9 slow section(s) deferred." (394 s)
</details>
- End to end on the real CLI: with `export class ObjectKernel` renamed
in `packages/core/src/kernel.ts` (uncommitted), `--commands` lists `pnpm
check:published-readme-exports`; after `git checkout HEAD --
packages/core/src/kernel.ts` it does not.
- Full battery (`node scripts/pm/dispatch-gates.mjs --self-test`, slow
tier included) was running on `aa386612` when this PR opened; its
verdict is in the os-dev report on #31292. The slow section's check
(every gate claim declared in its closure) was measured standalone at
377 of 377 net claims, none undeclared.
- `npx eslint --no-inline-config` on the three files: exit 0. No package
touched, so no package `lint`/`test`/`typecheck` is owed.
- CI families are read from this PR's check-runs, not re-run here.
`skip-changeset`: `scripts/pm/**` ships in no package.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the three files passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. Publishes nothing (`scripts/pm/**`), so `skip-changeset`; both
declaration lines are filled in.
- [x] 6. `packages/spec` not touched.
- [x] 7. No hand edits to generated docs, changelogs or release notes.
- [x] 8. Title, body and comments are in English (the maintainer brief
excepted, per the dispatch contract).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nVzx9yUpEnQaYZcEg)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 518d1bd commit 300f740
3 files changed
Lines changed: 524 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1277 | 1277 | | |
1278 | 1278 | | |
1279 | 1279 | | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
1280 | 1290 | | |
1281 | 1291 | | |
1282 | 1292 | | |
| |||
0 commit comments