Skip to content

Commit f2681c5

Browse files
huangyiireneclaude
andauthored
refactor(pm): extract the clause-② declaration reader into its own module (#19660)
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 ③ (the `needs:contract-review` label) 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 the `Clause-②: yes | no` declaration line — moves out of `scripts/pm/check-clause2-carriers.mjs` into a new `scripts/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 added `export` keyword** on `quoteLine` (proof below). Nothing was rewritten, nothing was copied, and no second reading of the declaration exists anywhere in the tree. | | before | after | |---|---|---| | what `check-changeset-no-major.mjs` and `check-adr-0087-registration.mjs` import the reader from | `scripts/pm/check-clause2-carriers.mjs`, 10,613 lines, first-party closure 9 modules | `scripts/pm/clause2-line.mjs`, 455 lines, first-party closure **0 modules** — it imports nothing at all, not even a node builtin | | `scripts/pm/check-clause2-carriers.mjs` | 10,613 lines | 10,205 lines | | net across the tree | | **+47** (+473 / −426) | The zero-import property is load-bearing twice: `check-changeset-no-major.mjs` states that it still runs before `pnpm 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) · `readClause2Line` It drags no marker stripper and no board reader. `quoteLine` is the only shared helper, so it moves with the reader and is exported; the checker imports it back rather than keeping a copy. `CLAUSE2_VALUES` and `CLAUSE2_ARMS` are 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: its `fw-gate` sandbox asserts the staged first-party closure of `check-adr-0087-registration.mjs` names the declaration reader by path. The extraction changes that path, so the assertion is re-pointed. Its sibling assertion inside `check-adr-0087-registration.mjs` itself is re-pointed in the same stroke — that pair of sites is exactly the two-hand-lists hazard `first-party-closure.mjs` exists 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 the `origin/main` merge. **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 `--ran` carrying each command's captured exit: ``` ✓ dispatch-gates --ran: 37 derived famil(ies) accounted for — 37 run, 0 NOT-MEASURED (a DERIVED zero — all 37 recorded an exit code and none of them is 3). ``` Headline verdict lines: ``` ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path) ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos ✓ check-changeset-no-major --self-test: 365 assertions ✓ check-clause2-carriers self-test: 1140 cases pass ✓ post-stamped self-test: 562 cases pass across 19 batteries — offline, no network, no token. ✓ check-governed-queue-guard self-test: 312 cases pass ✓ objectui-range --self-test: all checks passed (pnpm check:objectui-changeset) ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset ``` **The four workflows the ruling names — which jobs actually execute the module on a PR event:** | workflow | job | PR event? | reading | |---|---|---|---| | `lint.yml` | `lint` / **Lint & Repo Gates** (required context) | **yes** (`pull_request:`, no branch filter) | runs `check:pm-clause2-carriers` (:1268), `check:pm-post-stamped` (:1365), `check:objectui-changeset` (:3425), `check:changeset-gate-self-tests` (:3489) — all four green locally on `6aac105` | | `pr-automation.yml` | `changeset-check` / **Check Changeset** | **yes** (`pull_request:`) | runs `check-adr-0087-registration.mjs --self-test` and `--base MERGE_BASE` (:939-940) and `check-changeset-no-major.mjs --base MERGE_BASE` (:1075) — both run locally with `--base origin/main`, exit 0 | | `cut-rc.yml` | `cut` | **no** — `workflow_dispatch:` only, deliberately no `push:` and no `schedule:` | its exact step command lines were run locally with `SNAPSHOT_SHA=eff0a9622340f59068651f2b82d4afcd177546a8`: the ADR-0087 pair exit 0, the no-major pair exit 0 (printing its `LEVEL AXIS: NOT APPLICABLE` line — correct, a cut is not a PR and has no declaration to read), the empty-changeset pair exit 0 | | `release.yml` | — | **no** — `push: main` / `schedule` / `workflow_dispatch` | it names these gates in **prose only** (`:358`, `:590`, `:1115` are comments explaining that a major cannot reach the lane while `check-changeset-no-major.mjs` holds). No `run:` step in this workflow invokes either gate, so this workflow never loads the extracted module at all | **Ablation — 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 and `git hash-object` blob) and proves the restore against `HEAD`. Expected direction: turns red. Observed: turns red, both legs. ``` ① anchor CLAUSE2_VALUES ['yes','no'] -> ['yes'] blob 0b23b13 -> 9ababd2ce610 (mutation landed on disk) node scripts/check-changeset-no-major.mjs --self-test ✗ check-changeset-no-major --self-test — 21 failure(s) • control: the bare line IS a declaration — got {"kind":"malformed","value":"no", …} restore: blob == HEAD (0b23b13) and `git diff HEAD` is empty ② anchor CLAUSE2_ARMS ['widening','narrowing'] -> ['widening'] blob 0b23b13 -> bca974b5d407 (mutation landed on disk) node scripts/check-adr-0087-registration.mjs --self-test ✗ check-adr-0087-registration --self-test -- 6 failure(s) restore: blob == HEAD (0b23b13) and `git diff HEAD` is empty ``` Both restore legs verified by `git diff HEAD` being empty against the absolute path, not by an exit code. No ablation artefact remains: `git status --porcelain` is empty on `6aac105`. **Move byte-identity.** The 412 moved lines were compared line-for-line against `git show HEAD:scripts/pm/check-clause2-carriers.mjs` at the pre-move commit: 412 lines compared, exactly 1 differs, and it is the added `export` on `quoteLine`. **Patch round 1** (`cf3d884`, merged forward over `origin/main` as `5f926f8`): the header at :20–:39 struck `release.yml` from 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 the `needs:contract-review` label, `--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-config` is CI's run. Run here: the six changed files, `--no-inline-config --format json`, exit 0, **6 files judged, 0 errors, 0 warnings**, at head `6aac105`. The three readings the narrowing owes: 1. **Population from eslint's own config**, not a guess: the config object at `eslint.config.mjs:971` declares `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`, so `.mjs` is inside the judged population and all six files were judged rather than ignored. 2. **Count read from `--format json`**: the result array has length 6, one entry per changed file. 3. **Invariance for untouched files**, stated by the config itself at `eslint.config.mjs:327-329`: this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file". No verdict on a file this diff does not touch can move because of this diff. **Control bytes.** `pnpm check:nul-bytes` exit 0, plus a direct scan of the six files for the wider control-byte class: no match. ## Changeset — `skip-changeset`, measured `scripts/**` publishes nothing. Measured rather than assumed, with a positive control: - the repo-root `package.json` is `private: true` and declares no `files[]`; - every `package.json` under `packages/`, `apps/` and `examples/` was read: **none** has a `files[]` entry that escapes its own directory, so no published tarball can reach repo-root `scripts/`; - probe: `git grep -l 'readClause2Line|clause2-line|check-clause2-carriers' -- packages/` returns **0** files; - positive control on the same pathspec and the same instrument: `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 is `matchValueToken`'s sibling, and leaving it in the checker would have cost the checker an import for an unused function. Carrier: step ③ of #19061, which the ruling says may delete `check-clause2-carriers.mjs` entirely once nothing but the extracted module is imported from it. - The `check-adr-0087-registration.mjs` and `objectui-changeset-digest.mjs` docblocks narrate the #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. - ⛔ Not touched, by the ruling's own sequencing: `.claude/**`, `AGENTS.md`, `packages/**`, `.github/workflows/**`, the `needs:contract-review` label, `--pair`, `CONTRACT_REVIEW_TIER` in `dispatch-gates.mjs`, and the `## Contract review` record recognisers that `check-governed-queue-guard.mjs` lazy-loads from `check-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 reads `locateReviewOfRecord`, `deliveredCardNumber`, `servedTierStands`, `isModelIdentifierToken`, `REVIEW_OF_RECORD_THREADS` and `REVIEW_OF_RECORD_LOCATION`, none of which is in the moved block. ## Governance `scripts/**` is not a governed surface — `node scripts/pm/dispatch-gates.mjs --tier` printed, 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](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 93cdc43 commit f2681c5

6 files changed

Lines changed: 484 additions & 426 deletions

‎scripts/check-adr-0087-registration.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ import { maskComments, maskCommentsAndLiterals } from './js-comment-mask.mjs';
371371
// #16421 — the DIRECTION ARM, read through the fleet's one declaration reader.
372372
// ⛔ Not re-implemented here: the ruling's condition on the arm is that it has a
373373
// single legal spelling read in a single place, and every reader imports it.
374-
import { readClause2Line } from './pm/check-clause2-carriers.mjs';
374+
import { readClause2Line } from './pm/clause2-line.mjs';
375375
// #16421 — the staging manifest for this gate's own I1/I2 fixture, derived from
376376
// the module graph rather than typed out. Shared with the OTHER site that copies
377377
// this gate into a sandbox (`objectui-changeset-digest.mjs`), because two hand
@@ -7024,8 +7024,8 @@ function selfTest() {
70247024
// ⭐ DERIVED, not listed, and derived by a module BOTH staging sites of this
70257025
// gate import (#16421). It was a two-name list here and the same two-name
70267026
// list in `objectui-changeset-digest.mjs`'s `fw-gate` sandbox. Signal (4)
7027-
// added an edge to `pm/check-clause2-carriers.mjs`, whose own closure is nine
7028-
// modules deep; this site was updated in the same edit and the other was not,
7027+
// added an edge to the clause-② declaration reader, then ten thousand lines
7028+
// of sweep behind it; this site was updated in the same edit and the other was not,
70297029
// and `check:objectui-changeset` went red in CI with an error naming neither
70307030
// the import nor the manifest. One in two is the measured hit rate of "the
70317031
// next author remembers", so neither site holds an opinion about the graph
@@ -7037,7 +7037,7 @@ function selfTest() {
70377037
// moment signal (4) is reached, and the two I-cases below would again report
70387038
// about the wrong thing. This names the one edge that made the list a graph.
70397039
assert(
7040-
staged.includes(copy) && staged.includes('scripts/pm/check-clause2-carriers.mjs'),
7040+
staged.includes(copy) && staged.includes('scripts/pm/clause2-line.mjs'),
70417041
`I1/I2: the staged closure must carry the gate AND the declaration reader it imports — got ${staged.length} file(s): ${staged.join(', ')}`,
70427042
);
70437043
w(

‎scripts/check-changeset-no-major.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ import { isEntrypoint } from './invoked-as.mjs';
391391
// from the gate the PM protocol actually runs. Both modules import node
392392
// builtins only, so this file still runs before `pnpm install`.
393393
import { CONTRACT_REVIEW_LABEL } from './pm/check-half-states.mjs';
394-
import { readClause2Line } from './pm/check-clause2-carriers.mjs';
394+
import { readClause2Line } from './pm/clause2-line.mjs';
395395

396396
const __dirname = dirname(fileURLToPath(import.meta.url));
397397
const REPO_ROOT = resolve(__dirname, '..');
@@ -885,7 +885,7 @@ export function render(result) {
885885
// ① the CLAUSE-② declaration — "this PR puts a new key on a published
886886
// payload" — carried by the `needs:contract-review` gate label and/or by
887887
// the fixed `Clause-②: yes` line the PM protocol spells (read here through
888-
// `check-clause2-carriers.mjs`'s own `readClause2Line`, imported rather
888+
// `clause2-line.mjs`'s own `readClause2Line`, imported rather
889889
// than restated, so the two readers cannot drift);
890890
// ② the CHANGESET LEVEL for the package whose `packages/**/src/**` the diff
891891
// moves.
@@ -1383,7 +1383,7 @@ export function packagesTouched({ cwd, from, head }) {
13831383
// `readClause2Line` returns a REASON with every near miss, and this gate used
13841384
// to print the offending line without it: the reader knew which of three
13851385
// different mistakes the author had made, and the refusal said only "a near
1386-
// miss". The three owe different remedies — `check-clause2-carriers.mjs` says
1386+
// miss". The three owe different remedies — `clause2-line.mjs` says
13871387
// so in its own words ("⛔ The reason changes the sentence, never the state") —
13881388
// so the sentence is what this map supplies.
13891389
//
@@ -1433,7 +1433,7 @@ export function clause2NearMissRemedy(reason, line) {
14331433
}
14341434
return (
14351435
`this gate carries no remedy sentence for a near miss of reason ${JSON.stringify(reason ?? null)} — ` +
1436-
'`readClause2Line` (scripts/pm/check-clause2-carriers.mjs) reports a reason this file has not been taught, ' +
1436+
'`readClause2Line` (scripts/pm/clause2-line.mjs) reports a reason this file has not been taught, ' +
14371437
`and the reason plus the line are printed rather than swallowed. The offending line is: ${line}. Whatever the ` +
14381438
'reason, a declaration is `Clause-②: yes` or `Clause-②: no`, bare, at the start of a line of its own. Add the ' +
14391439
'sentence for this reason to CLAUSE2_NEAR_MISS_REMEDIES in scripts/check-changeset-no-major.mjs.'

‎scripts/objectui-changeset-digest.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,8 +2321,8 @@ function selfTest() {
23212321
// moved.
23222322
//
23232323
// ⭐ It was a hand MANIFEST, and the manifest is what broke (#16421). That
2324-
// gate gained one import — `pm/check-clause2-carriers.mjs`, the fleet's one
2325-
// clause-② declaration reader, whose own closure is nine modules deep — and
2324+
// gate gained one import — the fleet's one clause-② declaration reader, then
2325+
// living inside ten thousand lines of sweep with a nine-module closure — and
23262326
// the author updated the gate's OWN I1/I2 staging site in the same edit and
23272327
// not this one. CI went red HERE, on a gate about objectui changesets, with
23282328
// an error naming neither the new import nor this list. Both sites now DERIVE
@@ -2346,7 +2346,7 @@ function selfTest() {
23462346
for (const rel of gateDeps) gw(rel, readFileSync(join(__dirname, '..', rel), 'utf8'));
23472347
check(
23482348
'#6494 the staged gate carries its whole first-party closure — DERIVED, not a hand manifest',
2349-
gateDeps.includes('scripts/pm/check-clause2-carriers.mjs') && gateDeps.length >= 2,
2349+
gateDeps.includes('scripts/pm/clause2-line.mjs') && gateDeps.length >= 2,
23502350
`${gateDeps.length} dependenc(ies): ${gateDeps.join(', ')}`,
23512351
);
23522352
gg('add', '-A');

0 commit comments

Comments
 (0)