Repository navigation
Commit f2681c5
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
374 | | - | |
| 374 | + | |
375 | 375 | | |
376 | 376 | | |
377 | 377 | | |
| |||
7024 | 7024 | | |
7025 | 7025 | | |
7026 | 7026 | | |
7027 | | - | |
7028 | | - | |
| 7027 | + | |
| 7028 | + | |
7029 | 7029 | | |
7030 | 7030 | | |
7031 | 7031 | | |
| |||
7037 | 7037 | | |
7038 | 7038 | | |
7039 | 7039 | | |
7040 | | - | |
| 7040 | + | |
7041 | 7041 | | |
7042 | 7042 | | |
7043 | 7043 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
394 | | - | |
| 394 | + | |
395 | 395 | | |
396 | 396 | | |
397 | 397 | | |
| |||
885 | 885 | | |
886 | 886 | | |
887 | 887 | | |
888 | | - | |
| 888 | + | |
889 | 889 | | |
890 | 890 | | |
891 | 891 | | |
| |||
1383 | 1383 | | |
1384 | 1384 | | |
1385 | 1385 | | |
1386 | | - | |
| 1386 | + | |
1387 | 1387 | | |
1388 | 1388 | | |
1389 | 1389 | | |
| |||
1433 | 1433 | | |
1434 | 1434 | | |
1435 | 1435 | | |
1436 | | - | |
| 1436 | + | |
1437 | 1437 | | |
1438 | 1438 | | |
1439 | 1439 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2321 | 2321 | | |
2322 | 2322 | | |
2323 | 2323 | | |
2324 | | - | |
2325 | | - | |
| 2324 | + | |
| 2325 | + | |
2326 | 2326 | | |
2327 | 2327 | | |
2328 | 2328 | | |
| |||
2346 | 2346 | | |
2347 | 2347 | | |
2348 | 2348 | | |
2349 | | - | |
| 2349 | + | |
2350 | 2350 | | |
2351 | 2351 | | |
2352 | 2352 | | |
| |||
0 commit comments