Repository navigation
refactor(pm): extract the clause-② declaration reader into its own module - #19660
Conversation
…dule The two CI gates that read `Clause-②: yes | no` imported `readClause2Line` from `scripts/pm/check-clause2-carriers.mjs` — ten thousand lines of sweep machinery, a nine-module first-party closure, reached for forty lines of line reading. The reader, its two closed sets and the quoting helper move verbatim into `scripts/pm/clause2-line.mjs`, which imports nothing at all; the sweep imports them back rather than keeping a second copy. `check-changeset-no-major.mjs`, `check-adr-0087-registration.mjs` and `pm/post-stamped.mjs` re-point one import line each. The two derived first-party-closure assertions and the refusal sentences that name the reader's home follow it. Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone, and ③ Boundary flags
What must change for PASS: one sentence in Implemented-by: VERDICT: FAIL Generated by Claude Code |
… step ① Two header facts were wrong for a reader of this tree. `release.yml` was listed among the workflows the two gates run in; it names them in prose only and runs neither, so it never loads this module. And the paragraph describing what the ruling retired read as a description of the tree: the label, `--pair`, the double-carrier discipline and the independence pair are retired by the ruling and still run here until steps ② and ③ land. Comment-only. No executable line moves. Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsRendered in-seat by the skills seat 1 session, whose served tier reads as the fable tier word at
② Semver levelNone — no release. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… double carrier, independence requirement (step ②) (objectstack-ai#19694) Part of objectstack-ai#19061 Clause-②: no Step ② of the ruled three-step order on the clause-② contract review (ruling 5770886272, letter B; scope narrowed by the maintainer's word of 2026-09-22, 5772289798: 「降档保险丝 不留」). Step ① landed as PR objectstack-ai#19660. Step ③ (the `needs:contract-review` label itself and every script that names it) is a later round and is NOT in this PR — objectstack-ai#19061 remains open. ## 维护者速读(草稿) **改了什么**:按裁决 B 的第 ② 步,把技能文本里守「条款②契约复核」的三层机器删掉两层半:`needs:contract-review` 标签的挂与清纪律、`--pair` 入队前检、双载体、以及「独立性对」作为硬性要求;`references/contract-review.md` 的〈载体纪律〉与〈降档保险丝〉两节整节删除(您的原话「降档保险丝 不留」),该文件 60 行缩到 24 行;SKILL.md、core-rules.md、platform-readings.md 各删或修剪对应句子(净减 44 行);`packages/spec` 错误码台账源文件顶部注释里那句话去掉标签名(「登记新码是条款②改动」的表述保留),生成的参考页同步重生成,并附一条 `@objectstack/spec` 的 patch changeset(这段注释随 `.d.ts` 与源码一起发布)。`platform-readings.md` 里「强制档不因临缺授权降档」这条原则按复核要求以独立一行恢复(不再提保险丝)。`scripts/pm/dispatch-gates.mjs` 自检里原来钉住〈降档保险丝〉两句话的两条断言,换成一条「这两句话不得回流」的守卫。留下的只有一条 `## Contract review` 记录作为 Tier S(`.claude/**`)的落地记录,队列守卫继续读它。 **为什么改**:您 9-18 说「不需要 AI 不能自审契约改动 这种」,9-22 批准了 B 方案(三层收成一层):标签、`--pair`、记录三层守的是同一件事,多出来的两层是纯成本;席内达档复核就是 Tier S 的落地方式。 **风险与代价(含回滚)**:规则文本删除加一处自检断言改写,不改任何门禁的判定逻辑;自检在本分支最终 head 上实测 1905/1905 通过。回滚 = revert 本 PR(纯文本加一条自检守卫,无迁移)。仍有六处 `.claude/` 散文与 AGENTS.md 一处仍点名旧机器,列在下文,归步骤 ③。 **席位意见**:技能席 1(`session_01Wnstp2kTth7sGXfr8fXypc`)已按 GitHub 与拉取的 head `c7ea483b8` 复核两轮。第 0 轮记录 FAIL 5773967201 只欠两件:`platform-readings.md` 被删的原则行须以不带保险丝名的独立规则补回(≤120 字节),以及 `@objectstack/spec` 的 patch changeset(docblock 会进发布的 `.d.ts`)。补丁轮 1 恰好只做了这两件——PR 路径上两轮之间的 diff = +6(一行 119 字节的补回行、五行 changeset),`origin/main` 的合并未触及 PR 路径,`dispatch-gates.mjs` 与第 0 轮逐字节相同。本席在独立 worktree 复跑 ratchet(SKILL.md 815/819 · contract-review.md 24/60 · platform-readings.md 468/469 · core-rules.md 151/151)、skill-id-lint、frame-sync、governed-prose、doc-authoring、nul-bytes、三个 changeset 门禁、token ratchet 均 exit 0;第 0 轮头上 1905 例 dispatch-gates 自测 exit 0(脚本两轮未变),本头由本席另起后台复跑。`skip-changeset` 已由本席摘掉,`Check Changeset` 随即跑绿,CI 35 项全绿。复核记录 PASS 5774400593(席内 fable),`--pair` 0,ACCEPT 5774446589 在卡上。登记簿读 Tier S,但裁决 B 写的是 Tier H——以您的裁决为准,PR 留 draft 等您的手;若您要本席按 Tier S 入队,一句话即可。步骤 ③(删标签、队列卫士的标签腿、六处散文残留与指针)另开一轮。 **你要做的**:合并这个 draft PR(受管面,按您定的方式落地)。 ## What this PR does - `.claude/skills/pm-dispatch/references/contract-review.md` (60 → 24 lines): the 〈载体纪律(挂与清)〉 and 〈降档保险丝(机读)〉 sections are deleted whole. 〈复核归属与资格(按面)〉 keeps who renders the record, its shape (`Served-tier:` = the constant name, the head sha, `Implemented-by:`/`Reviewed-by:` as record lines, PASS/FAIL) and the landing pre-check, which is now two items (① the at-tier record on file, ② every check green); the pure-regeneration exception line moves under ①. The title and the pointer line are reworded to what remains. No new sentence was added: the current-head binding of the record is stated by AGENTS.md Prime Directive objectstack-ai#14 and enforced by the queue guard, so the moved exception line stands on that. - `.claude/skills/pm-dispatch/SKILL.md` (819 → 815): in 〈入队与落地〉 the bullets that hang the label, run `--pair N` on PR open, park the PR behind the label and point at the retired sections go; the Tier S landing sentence keeps 「PASS ⇒ ready、auto-merge;FAIL ⇒ 补丁轮;⛔ 免复核不放行」. Elsewhere only in-line trims: the label example and the gate-label clause in the label-write rules, the self-review-ban half of the PM tooling exception (the authorization-quote condition stays), the machine-read note on the record lines, the triage 「不挂」 clause, and the dangling 「细则见」 pointer on the subagent-verdict rule. - `references/core-rules.md` (151 → 151): the two mirrored clauses trimmed (自审自合; 闸门标签同此). - `references/platform-readings.md` (469 → 468): the `--pair` proxy reading and the fuse-named rule go; the three readings whose subject was the fuse keep their platform fact (get_session semantics, the minute-wall example). The principle the fuse rule carried comes back as a standalone line without the fuse name (patch round 1, the review's ①(b)): 「强制档不因临缺授权降档:临缺非退役,退役唯维护者裁;不达档起隔离达档子代理或等档。」 (119 bytes, at :393 in the served-tier readings block). The `needs:contract-review` mergeable-state reading at :26 stays this round — it is a reading about a label that still exists until step ③. - `packages/spec/src/api/error-code-ledger.zod.ts` docblock :102: 「Registering a code widens this face and is therefore a Clause-② change, door or no door」 — the label name is gone, the Clause-② statement stays. `content/docs/references/api/error-code-ledger.mdx` regenerated by `gen:docs` (the diff is exactly that sentence). `.changeset/19061-error-code-ledger-docblock-label.md` declares a `patch` for `@objectstack/spec` (patch round 1, the review's ①(e)/②): the docblock ships in the `.d.ts` declaration chunks and the `.zod.ts` source is in the package's `files[]`, so the diff publishes bytes from a released package — `AGENTS.md`'s changeset clause is the stricter one over the comment fast lane. `Clause-②: no` stands (no accept / reject behaviour, no public-surface change). - `scripts/pm/dispatch-gates.mjs` (the coordinator's mid-run increment; the file is a self-test, not a gate's logic): its self-test read `references/contract-review.md` and pinned two sentences of the deleted fuse section in two `t(…)` cases — 「…and its fuse keeps the quota exemption pointed at DISPATCH, never at the review」 and 「…and carries the case it never had: a RETIRED tier is not an exhausted one, and it is a maintainer ruling, ⛔ never a seat's reading」. They are replaced by ONE retirement guard in the file's own pattern — 「the rulebook no longer carries the retired 降档保险丝 lines (maintainer 2026-09-22 「降档保险丝 不留」, objectstack-ai#19061 comment 5772289798)」 — which asserts the new truth and reds if the text creeps back; the `fuseRules` read and the `CONTRACT_REVIEW_TIER` case stay as they were. A plain removal of the two cases was made and measured first (1904/1904 on the merged head) but committing it was refused by the harness's action policy as a test removal; the guard form is what landed. ## Inventory — every grep hit on the base (`97f4f8c828`), its disposition and where it lands | base hit | what | disposition | after | |---|---|---|---| | SKILL.md:25 | 自审自合 on the PM tooling exception | TRIMMED (independence half goes; authorization-quote clause stays) | 25 | | SKILL.md:155 | gate-label hang/clear example (`needs:contract-review`) | DELETED (+ :154 gate-label clause trimmed) | — | | SKILL.md:239 | record lines `Implemented-by:`/`Reviewed-by:`, 自审机读可见 | TRIMMED (machine-read note goes; record lines stay) | 238 | | SKILL.md:342 | ⛔ 不挂 `needs:contract-review` (triage) | TRIMMED (label clause goes; 裁定写方向 stays) | 341 | | SKILL.md:536 | 条款②自审资格按车道 (at-tier in-seat / isolated subagent) | KEPT (who renders the record) | 535 | | SKILL.md:635 | skills-lane own PR at-tier 自审 | KEPT (who renders) | 634 | | SKILL.md:656 | label hung by the seat on PR/card | DELETED | — | | SKILL.md:657 | `--pair N` on PR open, C5 | DELETED | — | | SKILL.md:659 | pointer naming 载体纪律 / 降档保险丝 | TRIMMED (pointer reworded to what remains) | 655 | | SKILL.md:773 | director duty title names the label | DEFERRED to step ③ (twin lanes/director.md:27 is off this surface) | 769 | | contract-review.md:3 | pointer: 载体纪律、归属资格与降档保险丝 | REWORDED to what remains | 3 | | contract-review.md:5–:22 | 〈载体纪律(挂与清)〉 | DELETED whole; :21 (pure-regen exception) MOVED | 22 | | contract-review.md:30 | record shape lists 独立性对 | KEPT (shape) | 12 | | contract-review.md:31 | `Served-tier:` = constant name | KEPT (shape) | 13 | | contract-review.md:35 | 独立性件 + 保险丝 ⇒ isolated subagent | DELETED (independence requirement + fuse) | — | | contract-review.md:38 | 独立性对(机读) values | TRIMMED (「(机读)」 goes; record-line values stay) | 19 | | contract-review.md:39 | SELF-REVIEW verdict | DELETED | — | | contract-review.md:40 | 清标即落地 / 剥双载体 | DELETED | — | | contract-review.md:43 | ② 双载体已清 via `--pair N` | DELETED (pre-check renumbered 三条 → 两条, ③ → ②) | — | | contract-review.md:44 | 确定性行 / C5 | DELETED | — | | contract-review.md:48–:60 | 〈降档保险丝(机读)〉 | DELETED whole (maintainer 「不留」) | — | | core-rules.md:23 | 自审自合 mirror | TRIMMED (mirror of SKILL.md:25) | 23 | | core-rules.md:40 | 闸门标签同此 (mirror of SKILL.md:155) | TRIMMED | 40 | | platform-readings.md:26 | label is a merge gate (mergeable reading) | KEPT this round — a reading about the label object; goes at step ③ with the label | 26 | | platform-readings.md:106 | 双载体清标 as a rate-limit example | TRIMMED (example goes; reading stays) | 106 | | platform-readings.md:120 | `--pair` proxy reading | DELETED (its general fact stays at :117–:119) | — | | platform-readings.md:385 | ⛔ 不作保险丝输入 | TRIMMED (fuse clause goes; reading stays) | 384 | | platform-readings.md:386 | ⇒ 保险丝无输入 | TRIMMED (fuse clause goes; reading stays) | 385 | | platform-readings.md:394 | 强制档不得因不可用而降档 (fuse rule) | REPLACED — the fuse-named sentence goes; the principle returns as a standalone line without the fuse name (patch round 1) | 393 | | AGENTS.md:278 | PD#14 `--pair N` / `Served-tier:` | OUTSIDE (not edited; step ③, the maintainer's file) | 278 | | error-code-ledger.zod.ts:102 | docblock names the label | REWORDED (Clause-② statement stays) | 102 | Two non-keyword neighbours were trimmed with their hits because they are the same discipline: SKILL.md:154's 「闸门标签被剥不是红灯是放行」 and SKILL.md:76's dangling pointer 「细则见 references/contract-review.md」 (the details it pointed at were the fuse's subagent-adoption lines). ## Line budget (ratchet ceilings untouched; every file at or under) | file | before | after | ceiling | |---|---|---|---| | `.claude/skills/pm-dispatch/SKILL.md` | 819 | 815 | 819 | | `references/contract-review.md` | 60 | 24 | 60 | | `references/core-rules.md` | 151 | 151 | 151 | | `references/platform-readings.md` | 469 | 468 | 469 | | `packages/spec/src/api/error-code-ledger.zod.ts` | 1679 | 1678 | — | | `scripts/pm/dispatch-gates.mjs` | +4 / −4 (comment lines and the one guard) | | — | | `.changeset/19061-error-code-ledger-docblock-label.md` | — | new, 6 lines (`@objectstack/spec`: patch) | — | Diff against `origin/main` (`7e1b048`, merged in at patch round 1; head `c7ea483`): 8 files, +32 / −70. ## File-surface increments beyond the claim (declared) 1. `content/docs/references/api/error-code-ledger.mdx` — the generated reference page carried the sentence; regenerated by `pnpm --filter @objectstack/spec gen:docs`, `check:docs` and `check:generated` (15/15) green. 2. `scripts/pm/dispatch-gates.mjs` — the two fuse-pin cases of its self-test, replaced by one retirement guard (described above). PR objectstack-ai#19684 (objectstack-ai#19680) landed on `main` and edited the same file in a different region; it is merged in. ## Verification (final head `c7ea483`, patch round 1) - Derivation: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths; the changed set read from the merge-base) → 132 families (the changeset gates joined: `check-empty-changeset` `--base` and `--self-test`, `check:objectui-changeset`, `check:pm-changeset-deadline-census`, `release-rehearsal-clone --self-test`). Reconciliation with `--ran`: 132 derived, 129 run, 3 NOT-MEASURED, 0 UNRUN. Every exit captured before any pipe. - The 3 NOT-MEASURED, each a prerequisite refusal (exit 3), none touched by this diff: `pnpm --filter @objectstack/spec run check:skill-examples` (reads `packages/client-react/dist`, not built), `pnpm check:dual-build-cjs-loads` (needs every package's dist), `pnpm check:lean-entry-closure` (needs `packages/objectql/dist`). A whole-repo build is CI's run, not this docblock-only spec change's. - `pnpm check:pm-dispatch-gates` at `c7ea483`, run detached with the exit recorded: exit 0, 「✓ dispatch-gates self-test: 1905 cases pass」, 709.7 s; the retirement guard and the 「spells NO model id」 walk over the 28 rulebook files (which read the restored line) are among them. Without the script change the same gate read 「✗ dispatch-gates self-test: 2 of 1904 case(s) failed」, exit 1 — the expected direction for prose a self-test had pinned. - Changeset gates at this head, all exit 0: `check-changeset-no-major --base origin/main` 「✓ This diff introduces no `major` bump.」; `check-adr-0087-registration --base origin/main` 「this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)」; `check-empty-changeset --base origin/main` 「No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)」 and 「No changeset from the merge base modified or deleted」; `check-empty-changeset --self-test` (159 assertions); `check:changeset-gate-self-tests`; `check:pm-changeset-deadline-census`; `check:objectui-changeset`. - Named gates, all exit 0 at this head: `check:pm-skill-ratchet` (SKILL.md 815 / 819, contract-review.md 24 / 60, platform-readings.md 468 / 469, core-rules.md 151 / 151; every line within the 120-byte cap), `check:pm-skill-id-lint` (30 files clean), `check:skill-frame-sync`, `check:pm-governed-prose`, `check:doc-authoring`, `check:nul-bytes` (plus a direct grep of the seven changed files for the wider control-byte class: no match), `check-skills-token-ratchet` (34 files within ceilings), `check-skill-frame-freshness`, `check:pm-clause2-carriers` (1140), `check-governed-queue-guard --self-test` (312), `check:pm-half-states` (5160), `check:pm-expected-skips`, `check-published-list-mirrors`, `check-self-test-wired`, `check-scripts-symbol-anchors`, `check-declaration-mirrors`. - `packages/spec`: `build` exit 0 (four times — base and after each of the three `origin/main` merges); `gen:docs`; `check:docs`; `check:generated` 「All 15 generated artifacts are up to date」 (at `c7ea483` too); `typecheck` exit 0 and `vitest run src/api/error-code-ledger.test.ts` 20/20 at `c7ea483` (declared narrowing: the diff in this package is a comment plus a changeset; the whole suite is CI's). - eslint narrowing: `pnpm exec eslint --no-inline-config --format json` over the two lintable changed files (`error-code-ledger.zod.ts`, `dispatch-gates.mjs`): exit 0, 2 judged, 0 errors, 0 warnings. Population: `eslint.config.mjs:971` (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`); invariance: `eslint.config.mjs:327–:329` — type-aware linting is never enabled, so no untouched file's verdict can move because of this diff. - `node scripts/pm/check-governed-merges.mjs --pr 19694`: GOVERNED — Tier S (4 of 6 paths hit the register, all under `.claude/**`; no Tier H path), 88 changed lines at that reading, under the human-merge threshold. ## Changeset — measured, not assumed The dispatch's assumption 「the docblock sentence changes no emitted dist byte」 is FALSIFIED on the dist half: the file-level docblock of `error-code-ledger.zod.ts` IS emitted into the declaration bundle. Base build vs post-edit build of `packages/spec/dist` (216 files each): 12 differ — the two `.d.ts`/`.d.mts` chunks that carry the docblock (`automation-api.zod-*`, renamed by content hash), the four entry `.d.ts` files that import them, four sourcemaps (line shift) and the two build-input stamps. Runtime `.js`/`.mjs`/`.cjs`: 0 differ. `needs:contract-review` in dist after: 0 hits (base: 2). `files[]` of `@objectstack/spec` also ships `src/**/*.zod.ts`, so the source comment ships too. So the diff publishes bytes from a released package and takes a `patch` changeset for `@objectstack/spec` (added in patch round 1 on the review's word; `AGENTS.md`'s clause is the stricter one over the comment fast lane). The `skip-changeset` label applied at round 0 is the wrong carrier and the seat removes it at the landing act; this dev did not touch it. ## Acceptance notes — riders off this surface (not filed; prose drift is outside the fileable classes) - Six `.claude/` prose sites still name the retired machinery and are off this step's claimed surface: `.claude/agents/os-dev.md:303` (the label + `--pair PR-NUMBER` reading), `references/dispatch-runbook.md:161` (独立性件), `references/lanes/director.md:27` (duty title names the label) and `:35` (pointer to 载体纪律), `references/lanes/ui.md:38` (独立性件与降档保险丝), `references/state-machine.md:10` (the label's card-side hang). Carrier: step ③ of objectstack-ai#19061 (the label), or a sub-issue of it. - `references/lanes/director.md:33` points at 「`../contract-review.md` 落地前检③」; after the renumbering here that item is ②. Same carrier. - `scripts/pm/dispatch-gates.mjs` docblock line 「Rule text: `references/contract-review.md` 「降档保险丝」」 (near the `CONTRACT_REVIEW_TIER` constant) names a section that no longer exists; outside the two cases this PR was allowed to touch — step ③'s territory. - `AGENTS.md:278` (Prime Directive objectstack-ai#14) still names `check-clause2-carriers.mjs --pair N` and the label — Tier H, the maintainer's own edit at step ③. - The `--template` pointers kept in `contract-review.md` :13 and :19 point at `check-clause2-carriers.mjs --template`; step ③ decides that file's fate. - The derivation did not list `check:pm-dispatch-gates` for a diff touching only `references/contract-review.md`, although that gate reads the file at runtime; it appeared once `dispatch-gates.mjs` itself was in the diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #19061 — STEP ① of the ruling's three-step order (record 5770886272, batch #211 item 3, letter B). Steps ② (the rule text,
--pair, the double-carrier discipline, the independence-pair requirement) and ③ (theneeds:contract-reviewlabel) are later rounds on the same card and are not addressed here; #19061 remains open.Clause-②: no
What this does
readClause2Line— the fleet's one reader of theClause-②: yes | nodeclaration line — moves out ofscripts/pm/check-clause2-carriers.mjsinto a newscripts/pm/clause2-line.mjs. The two CI gates the ruling names, plus a third importer it does not, re-point one import line each.The move is mechanical: 412 lines lifted from the checker's
Limb ②section, byte-identical except one addedexportkeyword onquoteLine(proof below). Nothing was rewritten, nothing was copied, and no second reading of the declaration exists anywhere in the tree.check-changeset-no-major.mjsandcheck-adr-0087-registration.mjsimport the reader fromscripts/pm/check-clause2-carriers.mjs, 10,613 lines, first-party closure 9 modulesscripts/pm/clause2-line.mjs, 455 lines, first-party closure 0 modules — it imports nothing at all, not even a node builtinscripts/pm/check-clause2-carriers.mjsThe zero-import property is load-bearing twice:
check-changeset-no-major.mjsstates that it still runs beforepnpm install, and both gates stage their own first-party closure into a throwaway git repo for their I1/I2 fixtures.Measured closure — why the module is 455 lines and not the ruling's "~40"
The ruling's estimate is the CODE size and it is accurate: the reader and its helpers are about forty lines of executable text. The other ~370 are the docblocks that already sat on them — the #16421 arm rationale, #17098's two structural tells, #13914's trailing-reasoning calibration, the four-axis note on the alternation refusal. They moved with the code rather than being dropped, because they are the authority on why each boundary sits where it does.
The dependency closure was measured, not assumed, and it is clean — every symbol below is used only by the reader, except the one marked:
CLAUSE2_VALUES·CLAUSE2_ARMS·CLAUSE2_KEY_LINE·CLAUSE2_NEAR_MISS_LINE·CLAUSE2_KEY_TEXT·CLAUSE2_KEY_COLON·CLAUSE2_LINE_START_DECORATION·hasInlineClause2Key·matchValueToken·readValueToken·readArmToken·clause2LineDescribes·quoteLine(shared — five call sites remain in the checker: :3257, :3824, :6150, :6152, :6165) ·readClause2LineIt drags no marker stripper and no board reader.
quoteLineis the only shared helper, so it moves with the reader and is exported; the checker imports it back rather than keeping a copy.CLAUSE2_VALUESandCLAUSE2_ARMSare imported back for the checker's own self-test pins.Files changed, and why each one had to move
scripts/pm/clause2-line.mjs— new. The reader, its two closed sets, the quoting helper, and a header stating what ruling 5770886272 retired and kept, so a reader of the small file knows why it exists.scripts/pm/check-clause2-carriers.mjs— the moved block leaves; one import block arrives. Its own--self-test(1140 cases) still drives the imported symbols.scripts/check-changeset-no-major.mjs,scripts/check-adr-0087-registration.mjs— one import line each. These are the two CI gates.scripts/pm/post-stamped.mjs— one import line. A third importer the ruling does not name; measured at claim time and re-pointed so no second edge to the big file survives.scripts/objectui-changeset-digest.mjs— not on the claim's declared file surface, and required by the move: itsfw-gatesandbox asserts the staged first-party closure ofcheck-adr-0087-registration.mjsnames the declaration reader by path. The extraction changes that path, so the assertion is re-pointed. Its sibling assertion insidecheck-adr-0087-registration.mjsitself is re-pointed in the same stroke — that pair of sites is exactly the two-hand-lists hazardfirst-party-closure.mjsexists for, and re-pointing one and not the other would have reproduced it.Four refusal / docblock sentences that named the reader's old home follow it, so no prescription points at a file the reader no longer lives in.
Verification
Every exit code was captured before any pipe. Final head
6aac105; the whole union below was re-run on that head after theorigin/mainmerge.Gate union — 37 derived, 37 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) and reconciled with--rancarrying each command's captured exit:Headline verdict lines:
The four workflows the ruling names — which jobs actually execute the module on a PR event:
lint.ymllint/ Lint & Repo Gates (required context)pull_request:, no branch filter)check:pm-clause2-carriers(:1268),check:pm-post-stamped(:1365),check:objectui-changeset(:3425),check:changeset-gate-self-tests(:3489) — all four green locally on6aac105pr-automation.ymlchangeset-check/ Check Changesetpull_request:)check-adr-0087-registration.mjs --self-testand--base MERGE_BASE(:939-940) andcheck-changeset-no-major.mjs --base MERGE_BASE(:1075) — both run locally with--base origin/main, exit 0cut-rc.ymlcutworkflow_dispatch:only, deliberately nopush:and noschedule:SNAPSHOT_SHA=eff0a9622340f59068651f2b82d4afcd177546a8: the ADR-0087 pair exit 0, the no-major pair exit 0 (printing itsLEVEL AXIS: NOT APPLICABLEline — correct, a cut is not a PR and has no declaration to read), the empty-changeset pair exit 0release.ymlpush: main/schedule/workflow_dispatch:358,:590,:1115are comments explaining that a major cannot reach the lane whilecheck-changeset-no-major.mjsholds). Norun:step in this workflow invokes either gate, so this workflow never loads the extracted module at allAblation — a red-first proof that the extracted module is the live reader for both CI gates. Run through
scripts/ablation-replace.mjs, which proves the mutation landed on disk (anchor count andgit hash-objectblob) and proves the restore againstHEAD. Expected direction: turns red. Observed: turns red, both legs.Both restore legs verified by
git diff HEADbeing empty against the absolute path, not by an exit code. No ablation artefact remains:git status --porcelainis empty on6aac105.Move byte-identity. The 412 moved lines were compared line-for-line against
git show HEAD:scripts/pm/check-clause2-carriers.mjsat the pre-move commit: 412 lines compared, exactly 1 differs, and it is the addedexportonquoteLine.Patch round 1 (
cf3d884, merged forward overorigin/mainas5f926f8): the header at :20–:39 struckrelease.ymlfrom the list of workflows the two gates run in — it names them in prose only (:358, :590) and never loads this module — and gained the step ① warning that theneeds:contract-reviewlabel,--pair, the double-carrier discipline and the independence pair are retired BY THE RULING and still RUN in this tree until steps ② and ③ land. 27 docblock lines changed, 0 executable lines (the file stripped of docblocks is byte-identical on both heads); the review of record went FAIL 5771683235 → PASS 5771857466.Lint — a proven narrowing, not a skipped scan. The repo-wide
eslint . --no-inline-configis CI's run. Run here: the six changed files,--no-inline-config --format json, exit 0, 6 files judged, 0 errors, 0 warnings, at head6aac105. The three readings the narrowing owes:eslint.config.mjs:971declaresfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], so.mjsis inside the judged population and all six files were judged rather than ignored.--format json: the result array has length 6, one entry per changed file.eslint.config.mjs:327-329: this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file". No verdict on a file this diff does not touch can move because of this diff.Control bytes.
pnpm check:nul-bytesexit 0, plus a direct scan of the six files for the wider control-byte class: no match.Changeset —
skip-changeset, measuredscripts/**publishes nothing. Measured rather than assumed, with a positive control:package.jsonisprivate: trueand declares nofiles[];package.jsonunderpackages/,apps/andexamples/was read: none has afiles[]entry that escapes its own directory, so no published tarball can reach repo-rootscripts/;git grep -l 'readClause2Line|clause2-line|check-clause2-carriers' -- packages/returns 0 files;git grep -l 'defineStack' -- packages/returns 405 files. The instrument finds what is there.Acceptance notes
readValueToken(moved with the block, unchanged) is defined and never called — its every remaining reference is docblock prose in four files. Dead code, so ⛔ not filed as a card; noted here. It moved rather than staying behind because it ismatchValueToken's sibling, and leaving it in the checker would have cost the checker an import for an unused function. Carrier: step ③ of Retire the in-seat clause-② contract review — the rule, its gate label, and the 9k-line checker's gate role #19061, which the ruling says may deletecheck-clause2-carriers.mjsentirely once nothing but the extracted module is imported from it.check-adr-0087-registration.mjsandobjectui-changeset-digest.mjsdocblocks narrate the [finding] An accept-set narrowing owes a**BREAKING**banner incorebut not inplatform-objects— and the ADR-0087 classifier reads the banner #16421 incident as "that gate gained one import, whose own closure is nine modules deep". After this PR that closure is zero modules deep, so the two sentences were adjusted to describe the incident without asserting a depth that is no longer true. The derivation rule they justify is untouched: two sites, one graph, neither holding an opinion about it..claude/**,AGENTS.md,packages/**,.github/workflows/**, theneeds:contract-reviewlabel,--pair,CONTRACT_REVIEW_TIERindispatch-gates.mjs, and the## Contract reviewrecord recognisers thatcheck-governed-queue-guard.mjslazy-loads fromcheck-clause2-carriers.mjs(RECOGNISER_SOURCES.tier, pinned at the checker's own :4407 equivalent). The ruling KEEPS the record, so those recognisers stay in the checker this round — verified: the queue guard readslocateReviewOfRecord,deliveredCardNumber,servedTierStands,isModelIdentifierToken,REVIEW_OF_RECORD_THREADSandREVIEW_OF_RECORD_LOCATION, none of which is in the moved block.Governance
scripts/**is not a governed surface —node scripts/pm/dispatch-gates.mjs --tierprinted, verbatim, "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled." Opened as draft. The card's later steps do touch Tier H surfaces; this one does not.维护者速读(草稿)
改了什么 —— 把那条
Clause-②: yes | no声明行的读取器,从一万行的check-clause2-carriers.mjs里原样搬进一个新的小文件scripts/pm/clause2-line.mjs。代码一个字没改写(412 行逐行比对,只多了一个export关键字),只是换了个住处。两个每次 PR 都跑的 CI 门禁,加上第三个调用方,各改一行 import。为什么改 —— 裁决 5770886272 的第 ① 步。这两个门禁要的只是「这张 PR 有没有放宽公开面」这个 semver 判断,和「谁复核了这份 diff」毫无关系;但它们为了拿到四十行的读行逻辑,过去要把一万行、九个模块深的巡检机器整个拖进来。搬完之后它们的依赖闭包是零个模块——新文件连一个 node 内置模块都不 import。这件事有实际后果:
check-changeset-no-major.mjs声称自己能在pnpm install之前就跑起来,两个门禁还要把自己的依赖闭包复制进临时 git 仓做自检,闭包小了这两件事都更稳。风险与代价(含回滚) —— 风险很低:纯搬家,无行为变化,全树净增 47 行。为了证明「搬完之后门禁读的确实是新文件」,做了两次消融:把新文件里的常量改坏,两个 CI 门禁分别转红(21 个失败 / 6 个失败),再还原并用 blob 哈希与
git diff HEAD证明树干净。37 个派生门禁族全绿、零未测量。回滚就是 revert 这一个 PR,没有数据迁移、没有发布面变化(scripts/**不进任何 npm 包,已用正控实测)。唯一超出认领申报文件面的是objectui-changeset-digest.mjs:它断言那个门禁的依赖闭包里有声明读取器的路径,不跟着改就必然变红。席位意见 ——
你要做的 —— 这一步不碰
.claude/**也不碰AGENTS.md,所以不是 Tier H,按常规队列落地即可;但它是三步裁决的第 ① 步,第 ② 步(删规则文本、--pair、双载体、独立性对)和第 ③ 步(删标签)会碰 Tier H 面,那两轮才需要您的手。如果您认同「读取器搬家」这件事本身,这个 PR 就可以走。Generated by Claude Code