Skip to content

Commit 300f740

Browse files
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

File tree

‎scripts/pm/dispatch-gates.data.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,16 @@ export const CHANGE_KIND_ROWS = [
12771277
},
12781278
],
12791279
},
1280+
{
1281+
kind: "takes away a claim a published README makes (a fenced import or member call newly written in a published document, a manifest field or workspace glob that moves the documents or their type entries, or a claimed name a reached package's source stops declaring — read from the DIFF against the merge base)",
1282+
matches: 'published-readme-claim',
1283+
gates: [
1284+
{
1285+
name: 'check:published-readme-exports',
1286+
why: "every name a published README's fenced code imports from this repo's scope, and every member it calls on a value built from one, must exist in the BUILT type entry that import resolves to. So a README edit that writes a new claim moves it, and so does a removed or renamed declaration of a claimed name in the imported package or its workspace dependencies, or a manifest that changes what is published or where the types are. It printed in the artifact-roster block and no card was handed it (#31292, the family #31257 left): its reach is the built types of 68 packages, so a path net would name it for nearly every card. This row reaches it through the names the 129 documents claim, matched against the declarations the diff removes, measured against the gate's own reader (364 claims, none missed; 17 of the last 500 commits on main derive it, against 303 that touched a reached package's TypeScript). ⚠ It needs the packages BUILT: on an unbuilt tree it exits 3, PREREQUISITE NOT MET, and that exit means NOT MEASURED, never green. Build first, exactly as lint.yml does: pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* (quote the filter values for your shell). Repair the README to name what the package really exports, or keep the export; the baseline is MAINTAINER-ONLY",
1287+
},
1288+
],
1289+
},
12801290
];
12811291

12821292
/**

0 commit comments

Comments
 (0)