Skip to content

skills(pm-dispatch): key the clause-② contract review by lane — spec and skills owe it on every round, other lanes owe none - #18903

Merged
os-elon-musk merged 3 commits into
mainfrom
claude/issue-18536-lane-keyed-contract-review
Sep 19, 2026
Merged

os-elon-musk merged 3 commits into
mainfrom
claude/issue-18536-lane-keyed-contract-review

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18536

Clause-②: no

What this lands

The maintainer's lane rule, restated by the director record on the card (comment 5717169811), quoted verbatim and untranslated:

「曾经要求只有 spec 和 skills 需要 fable,其他 opus 就够了,理论上其他车道不需要契约复审」「即使项目经理跑在 opus 上,也可以支持起 fable 子 agent 复审,这样最快。除非子 agent 用 fable 启动失败,也就是没有 fable」

carried into the governed text and the two machine readers:

  1. Spec and skills lanes — every delivered round gets the contract review at CONTRACT_REVIEW_TIER: in-seat when the seat's served tier is that tier, otherwise by the at-tier review subagent the seat spawns (the 09-17 route stays). The record is the same-shape comment with Served-tier:, on the PR or the card.
  2. Every other lane — no contract review. The clause-② limbs (path packages/spec/src/**, declaration Clause-②: yes) survive unchanged and now answer the LANE question: a limb hit is spec-lane work and moves there; a Clause-②: no PR outside the contract surface lands on the three pre-checks and the gates. No default-tier self-review record is demanded, and no other lane spawns the at-tier subagent.
  3. Tier unavailable (the at-tier subagent cannot start) — the review cannot be produced: the PR stays draft and out of the queue, and the maintainer's own review is the only bypass, by their word each time.

The lane key is the 2026-09-10 key restored (PR #17294) with the 2026-09-17 subagent route kept (PR #18511); the 2026-09-16 tier key (PR #18363) is superseded. Not a byte-revert: both spec AND skills owe the review, and 「余席默认档自审加门禁」 is gone — other lanes owe nothing.

Charter (commit 1) — net 0 lines per file

file before → after ratchet ceiling
.claude/skills/pm-dispatch/SKILL.md 812 → 812 812
references/contract-review.md 60 → 60 60
references/core-rules.md 151 → 151 151

Every edited line is at most 120 bytes; check:pm-skill-ratchet is green on bc0c2ec41. Rule text carries no issue numbers (check:pm-skill-id-lint green); the four-axis frame block is untouched (check:skill-frame-sync / -freshness green).

SKILL.md — five in-place rewrites, one added line, one retired line:

  • :512 强制条款② — 「达档复核归派发席席内」 → 「命中即 spec 车道的活」.
  • :522 席位档策略 — 「按实测档:达档席内审;未达档席 ⛔ 不自审,起隔离达档子代理转录核档采信」 → 「按车道:spec 与 skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理」 (the transcript-verified tier of the subagent stays stated in contract-review.md :54).
  • :636 入队闸门 — 「无席内条款②复核 PASS 在案」 → 「无达档条款②复核 PASS 在案」; the limbs at :637 / :638 are untouched.
  • :639 交付后复核 — 「归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡」 → 「只 spec 与 skills 车道欠,每轮达档:席内审或起子代理;双肢命中即 spec 车道」 (the record's shape lives in contract-review.md :27–:29).
  • :640 ADDED (rule 3, where the enqueue gate lives) — 「子代理起不来 ⇒ 复核缺席,PR 留 draft 队列外等档;唯一旁路是维护者亲审,逐次为准。」
  • :646 pointer — 「席内复核的适用面」 → 「契约复核的适用面」.
  • RETIRED, paying for :640 — the former :672 「报告席记条款②默认档 FAIL 率入复审清单;超改制前达档史值 ⇒ 决策卡交维护者定回退。」 Reason: it meters the FAIL rate of DEFAULT-TIER clause-② reviews, and under rules 1–2 no lane performs one (spec and skills review at tier; other lanes review nothing), so the metric's population is empty. It sits outside the claim's declared SKILL.md bands; it is the one tier-keyed line in the file the ruling empties, and paying in-file required it. Re-wrap is not currency: no line was re-flowed.

contract-review.md — thirteen in-place rewrites:

  • Title and :23 — 「(席内)」 → 「(按车道)」.
  • :9, :11, :12, :14 — 「席内契约复核 / 席内复核」 → 「达档契约复核 / 达档复核」: the review is at tier wherever it happens, and "in-seat" was the 09-16 key.
  • :25 — 「交付后收集复核当轮席内完成;借复核不移卡,新 spec 工作恒归 spec 席」 → 「交付后收集复核当轮完成;只 spec 与 skills 车道欠,新 spec 工作恒归 spec 席」. The "borrow the isolated review" route is what rule 2 closes for other lanes.
  • :26 — 「按档位:达档席内审契约增量;未达档 ⛔ 不自审,走保险丝路;豁免仅独立性件与保险丝」 → 「按车道:spec 与 skills 席审契约增量;达档席内审,未达档 ⛔ 不自审,起达档子代理」.
  • :27 — 「达档与默认档同形」 → 「席内与子代理同形」: there is no default-tier record any more; one shape holds between in-seat and subagent (the two scripts' quotations of this line are updated in the same PR).
  • :32 — the independence case → 「独立性件(契约真分叉、dev 挂旗)与保险丝只免席内审,不免复核:起隔离达档子代理」. This is the 「豁免仅独立性件与保险丝」 rewrite the ruling asked for: the independence case and the fuse exempt the seat from IN-SEAT review only, never from the review's existence. The former third trigger 「派发后的跨车道面(含 spec)」 is lane routing now (:25).
  • :40 landing pre-check ① — 「席内条款②复核 PASS 在案 …(档位按实测)」 → 「达档条款②复核 PASS 在案 …(spec、skills 每轮)」.
  • :50 the fuse — 「管每个出条款②裁决的席 … 未达档 ⛔ 不自审」 → 「只管 spec 与 skills 席的条款②复核」 (the 不自审 clause stays at :26).
  • :52 — 「至席内复核完成」 → 「至达档复核完成」.
  • :53 — 「标签原样留置,队列外等待是安全态」 → 「起不来即无复核,标签原样、队列外等档」 (rule 3 on the fuse side).

core-rules.md — one rewrite: :112 「契约卡达档复核归派发席」 → 「契约复核只 spec、skills 欠」. Trace by REST GET /pulls/N/files: PR #18363 (09-16) touched SKILL.md, contract-review.md and platform-readings.md only, so the 09-16 re-key never landed in core-rules.md; :112's tier key came with PR #18511 (09-17), replacing PR #17294's 「归 spec 席」. :122 「references/ 席内达档复核后入队」 is left as the mirror of SKILL.md :625 (see acceptance notes).

Machine side

commit 2 — scripts/pm/dispatch-gates.mjs. The clause-② note and the suspect tail that --tier prints (quoted into claim comments) said 「spec seat; default-tier build」 and 「in the spec seat」 — the 09-10 seat key, which PR #18363 never re-keyed. They now name the spec and skills lanes, the in-seat-or-subagent route, and 「a hit outside those lanes is spec-lane work and moves there」; the docblock above MANDATORY_TIER_GLOBS carries the rule. Four self-test pins hold both renderings to the lane key and refuse the two retired spellings. --self-test: 1852 cases pass (baseline on 0b31d90: 1848).

commit 3 — scripts/pm/check-clause2-carriers.mjs. needsRecordRead — C6's population gate and the sweep's read budget — owed a record only in the completed state (a cleared yes), so a spec-lane Clause-②: no round read 0 with no record on its head: exactly PRs #18530 / #18529, the card's measured pair. Now:

  • LANES_OWING_REVIEW (domain:spec, domain:skills, frozen) and laneOwesReview(pair), read off the CARD's domain:* labels; unreadable labels stay UNJUDGED through the existing labels gap.
  • Population: the completed state (unchanged) OR a declared no on a spec/skills card (new). A no anywhere else still owes nothing — pinned.
  • C6's row on a spec/skills no round with no record: exit 4; remedy = the lane's review at tier (in-seat or by the at-tier subagent), with the unavailable-tier state and its one bypass named.
  • A cleared yes OUTSIDE the two lanes keeps its row and its exit 4 (the yes is a limb hit, and limb-hit work is owed), but its remedy is lane ROUTING: re-lane the item to domain:spec (pm:retriage, or split the contract work to a spec-lane card or PR per 「新 packages/spec 工作恒由 domain:spec 席收口」), or correct a false yes with a Clause-②-correction: comment — never a default-tier self-review, never an at-tier subagent from that lane.
  • The C6-RECORD note prescribes the clear-citation only where a clear exists; on a spec/skills no round with its record it says the lane owes the record and it exists.
  • New floored battery, 34 cases (floor 30); roster floor 32 → 33. --self-test: 977 cases pass (baseline 941).

Deviation from the dispatch's suggested case (c) — a yes pair on a domain:cli card as 「not owed as a record, plus a note」 at exit 0 — implemented instead as owed at exit 4 with the routing remedy. Reasons: contract-review.md :42 promises 「0 = … head 上有记录」; a cli seat clearing a yes pair and landing it from that lane is never a legal workflow under rule 2 (the work 「moves there」), so an exit-0 note would be the 0-with-a-message the file's own header bans; and the row's exit is unchanged from today (only the remedy text moves), so no legal workflow is re-blocked. The four-axis reading is in the report's open_questions; the PR is draft for the maintainer's word either way.

Gates (run on bc0c2ec41, the final commit)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 46 families. 45 ran with exit 0 (each captured redirect-then-$?), among them check:pm-skill-ratchet, check:pm-skill-id-lint, check:skill-frame-sync, check:pm-governed-prose, check:pm-clause2-carriers, check:declared-population-live, check:pm-half-states, check:nul-bytes. check:doc-formula-expressions first exited 3 — PREREQUISITE NOT MET (@objectstack/formula / @objectstack/lint not built; nothing measured) — and reruns green after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the verify lock (22 + 9 + 14 examples judged clean). check:pm-dispatch-gates (430–450 s) was still running detached when this body was written; its verdict and the --ran reconciliation are in the os-dev-report comment on #18536. Both edited scripts' own --self-test pass (above). The four package tests that mention dispatch-gates.mjs do so in docblocks only and read nothing from it, so they are not owed. The derivation printed a STALE TREE warning (the tree is behind an origin/main that moved during the run); the PM's re-derivation after the report reads the true list.

skip-changeset: nothing published moves — .claude/** and scripts/pm/** ship in no package's files[].

Acceptance notes

Lines outside this card's declared file surface that carry seat or tier wording — reported, not edited (承接者: the skills seat's next SKILL.md round; the hot-file queue behind this card holds #18743 · #18755 · #18665 · #18469 PR-B · #18489):

  • SKILL.md :231 「派发后发现的跨车道面(含 spec)不移卡,认领席借契约复审档隔离复核」 and core-rules.md :62 (its mirror). The "borrow the isolated at-tier review" route is what rule 2 closes for other lanes (no other lane spawns the at-tier subagent). The ruling's 「moves there」 and SKILL.md :234 「已派发卡 ⛔ 不因触 spec 转席」 reconcile as "the contract WORK moves to the spec lane; the card is not re-seated", which is how contract-review.md :25 now reads. Suggested rewrite of :231, same byte budget: 「认领即跟到 MERGED:派发后发现的跨车道面(含 spec)不移卡,契约面工作交 spec 车道达档复核。」
  • SKILL.md :608 「技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席」, :623 「由本席按达档自审」, :625 「经席内达档复核后 ready → 入队」 and core-rules.md :122 「references/ 席内达档复核后入队」: consistent with rule 1 read as "the skills lane's review at tier, in-seat or by subagent"; the word 席内 there is the 09-16 spelling and could be read as in-seat-only.
  • references/lanes/spec.md :19–:33 and references/lanes/skills.md: no contradiction found; lanes/skills.md :13 「契约复核归派发席席内」 is ownership wording (the seat owns the record it adopts) and stays true.
  • The follow-up the ruling assigns to the lane seats after this lands, not to this card: re-read the needs:contract-review carriers hung outside the spec/skills lanes (the director's ledger ⑫) and re-lane or strip each.
  • Sweep-mode cost: check-clause2-carriers.mjs without --pair now buys the PR thread for every spec/skills-lane no pair too (one read per PR, cached per PR); the budget paragraph in the file's header is unchanged in shape.

维护者速读(草稿)

改了什么:把契约复核的归属重新按「车道」写回章程:只有 spec 与 skills 两条车道的每一轮交付都要过契约复审档的复核——席位达档就席内审,不达档就起达档子代理;其余车道零契约复核,条款②命中即 spec 车道的活;达档子代理起不来时 PR 留 draft、队列外等档,唯一旁路是您亲审。三份章程文本行数不变(812 / 60 / 151),两个机读脚本(--tier 的提示行、--pair 的复核记录人口)同步改成按车道判。

为什么改:9-16 那次把「只在 spec 席」改成了「按席位实测档」,席位名单没了,于是同一张 Clause-②: no 的 spec 车道 PR 在两条细则下答案相反(#18536 的两种读法),而账号级 429 让「等档位」在两种读法里含义完全不同。您 9-17 的裁决把车道规则说回来了,本 PR 只是把它落到文本和脚本上。

风险与代价(含回滚):--pair 从此对 spec/skills 车道的 no 轮也要求 head 上有复核记录,没有就退 4——这正是 #18530 / #18529 该有的读数,但意味着这两条车道的 no 轮在记录落下前都不能入队;其它车道不受影响,yes 挂在别的车道上的旧读数(退 4)不变,只是补救措施从「自审」改成「改道 spec」。回滚 = revert 这三个 commit,文本与脚本一起回到 0b31d90。

席位意见:(留空)

你要做的:确认本 PR 是否如实落了您的车道规则,是则合并;合并后各车道席按裁决去清理挂在 spec/skills 之外的 needs:contract-review 载体。


Generated by Claude Code

… spec and skills owe it on every round, other lanes owe none

The maintainer's lane rule, restated on the card: the contract review at
CONTRACT_REVIEW_TIER is owed in the spec and skills lanes only, on every
delivered round (in-seat when the seat is served at that tier, otherwise by
the at-tier review subagent the seat spawns — the 09-17 route stays); every
other lane lands on the three pre-checks and the gates with no contract
review, and a clause-② limb hit outside those lanes is lane routing (spec-lane
work) rather than a self-review demand. When the subagent cannot start the
review is unavailable: the PR stays draft, out of the queue, and the
maintainer's own review is the only bypass.

Net 0 lines per file (812 / 60 / 151). The one added SKILL.md line (the
unavailable-tier state) is paid by retiring the default-tier clause-② FAIL
rate line, whose population the rule empties.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
… not a seat

The clause-② note and the suspect tail printed by `--tier` (quoted into
claim comments) said "spec seat; default-tier build" and "in the spec seat" —
the 2026-09-10 seat key, never re-keyed by the 09-16 tier PR. They now say
what the charter says: the review is owed in the spec and skills lanes,
in-seat at tier or by the at-tier subagent, and a contract-surface hit
outside those lanes is spec-lane work that moves there. The docblock above
MANDATORY_TIER_GLOBS carries the same rule; four self-test pins hold both
renderings to the lane key and refuse the two retired spellings.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
`needsRecordRead` — C6's population gate, and the sweep's read budget — owed
a record only in the completed state (a cleared `Clause-②: yes`). Under the
lane rule the spec and skills lanes owe the contract review on EVERY round
they deliver, `no` included, and a `no` round hangs no carrier to mark its
review pending: PRs #18530 / #18529 (spec-lane `no` rounds) read 0 here with
no record on either head. The population now adds the `no` rounds of the two
lanes, read off the card's `domain:*` labels (`LANES_OWING_REVIEW`,
`laneOwesReview`; unreadable labels stay UNJUDGED). A `no` elsewhere still
owes nothing. A cleared `yes` outside the two lanes keeps its row and its
exit — the `yes` is a limb hit and limb-hit work is owed — but its remedy is
lane routing (re-lane to spec, or correct a false `yes`), never a
default-tier self-review and never an at-tier subagent from that lane. The
C6-RECORD note prescribes the clear-citation only where a clear exists. New
floored battery, 34 cases; roster floor 32 → 33.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bc0c2ec41e8c93f6ef1a9eefa33a72d393292264

① Derived judgments

  • Surface: .claude/skills/pm-dispatch/SKILL.md (+6 / −6, 812 / 812), references/contract-review.md (+14 / −14, 60 / 60), references/core-rules.md (+1 / −1, 151 / 151), scripts/pm/dispatch-gates.mjs (+34 / −14), scripts/pm/check-clause2-carriers.mjs (+225 / −41). No published package moves; no contract's accept/reject set moves; the public surface is unchanged.
  • Rule 1 (spec and skills owe the at-tier review every round, in-seat or by the at-tier subagent): SKILL.md 「条款②复核按车道:spec 与 skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理。」 and 「交付后复核只 spec 与 skills 车道欠,每轮达档:席内审或起子代理;双肢命中即 spec 车道。」; contract-review.md 「按车道:spec 与 skills 席审契约增量;达档席内审,未达档 ⛔ 不自审,起达档子代理。」 — correct; the transcript-verified tier of the subagent (the finding(pm-dispatch): SKILL.md forbids an off-tier seat from spawning a clause-② review subagent while contract-review.md prescribes exactly that — so a default-tier lane has no legal route to a Clause-② yes verdict at all #18434 guard) stays on contract-review.md's 「子代理档只取其转录 harness 逐请求 model 盖章」 line, unchanged.
  • Rule 2 (other lanes owe none; a limb hit is spec-lane work): SKILL.md 「强制条款②:… 命中即 spec 车道的活。」, the limbs untouched; contract-review.md 「只 spec 与 skills 车道欠」 and 「独立性件(契约真分叉、dev 挂旗)与保险丝只免席内审,不免复核:起隔离达档子代理。」 — the 「豁免仅独立性件与保险丝」 rewrite the ruling asked for, now saying what is exempted from what; core-rules 「契约复核只 spec、skills 欠」 — correct.
  • Rule 3 (tier unavailable ⇒ no review, draft and out of the queue, the maintainer's own review the only bypass): SKILL.md added line 「子代理起不来 ⇒ 复核缺席,PR 留 draft 队列外等档;唯一旁路是维护者亲审,逐次为准。」 and contract-review.md 「起不来即无复核,标签原样、队列外等档」 — correct.
  • Payment: the added SKILL.md line is paid by retiring 「报告席记条款②默认档 FAIL 率入复审清单;超改制前达档史值 ⇒ 决策卡交维护者定回退。」 — provenance read by the seat: PR skills(pm-dispatch): clause-② cards build at the default tier and are gated by the contract review at CONTRACT_REVIEW_TIER (#16905) #16915 (skills(pm-dispatch): 强制条款② mandates CONTRACT_REVIEW_TIER for IMPLEMENTATION of clause-② cards — drop it to TIER_DEFAULT and let the existing contract review be the gate (maintainer-authorised trial already running) #16905, 2026-09-09, the reform that put clause-② builds at the default tier); it metered the FAIL rate of default-tier clause-② reviews as that reform's rollback trigger. Under rules 1–2 no lane performs a default-tier clause-② review, so the metric's population is empty; the retirement is sound, and it is named to the maintainer in the 速读 because it removes a reform-era rollback trigger, not a mere restatement.
  • Machine side: dispatch-gates.mjs tierLines and the suspect tail now key the review by lane (spec and skills, in-seat at tier or by the at-tier subagent; a hit outside is spec-lane work and moves there), with four pins refusing the two retired spellings. check-clause2-carriers.mjs: LANES_OWING_REVIEW frozen at exactly the two lanes; laneOwesReview reads the CARD's domain:* labels and answers null on unreadable labels (already UNJUDGED); needsRecordRead = the completed state OR a declared no on a spec/skills card; C6 carries three remedies keyed by where the pair sits, the exit unchanged (4) in all three; the C6-RECORD note distinguishes the owed-by-lane case; the sweep buys the PR thread for the two lanes' no pairs (cost declared). The seat's spot-check on a scratch worktree at this head: check-clause2-carriers.mjs --self-test exit 0, 977 cases (baseline 941); check-skill-line-ratchet exit 0 at 812 / 60 / 151 / 403; check-skill-id-lint exit 0 (27 files clean); dispatch-gates.mjs --self-test exceeded the seat's 540 s timeout (exit 124, 0 ✗ before the cut) — NOT MEASURED by the seat, the dev's detached run reads 1852 pass, and CI's Lint & Repo Gates measures it on this head.
  • The dev's question (a cleared yes on a card outside the two lanes with no record: exit 4 with a lane-routing remedy, or exit 0 with a note): A, as implemented. Rule 2 says the work moves to the spec lane; an exit 0 would read as landable from the lane that cleared it, the 0-with-a-message the file's own header bans; the exit is what it is today, only the remedy moved, so no legal workflow is newly blocked. Not a maintainer question: gate strength is kept, not weakened, and no accept set moves.
  • Residue outside the card's declared bands, carried as riders on the SKILL.md serial's next card (⛔ not this PR's): SKILL.md 「认领即跟到 MERGED:派发后发现的跨车道面(含 spec)不移卡,认领席借契约复审档隔离复核」 and core-rules' mirror still name the borrowed isolated review for any lane; 「references/ 席内达档复核后入队」 (SKILL.md and core-rules) keeps the 09-16 word 席内. Both reconcile with the ruling as "the contract WORK moves, the card is not re-seated; the skills lane reviews at tier in-seat or by subagent", but the words predate it.

② Semver level

  • skip-changeset is correct: .claude/** and scripts/pm/** ship in no package's files[]. Clause-②: no on the claim and in the body; PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 18903 at 2026-09-18T05:18Z: exit 0, one live claim, no widening tell.

③ Boundary flags

  • Dev flags: one open_questions entry, answered A above. Deviations accepted: the payment line lies outside the four declared SKILL.md bands (declared in the body, reason sound); contract-review.md 「达档与默认档同形」 → 「席内与子代理同形」 with both script quotations updated in the same PR; check:pm-dispatch-gates run detached past the foreground cap with the verdict line read (the standing platform fact); one Bash call moved to the background by the harness with no work lost.
  • CI on this head at 2026-09-18T05:18Z: 22 success · 12 skipped · 1 in_progress (Lint & Repo Gates) — nothing red; the landing check reads it again on the landing act.

Implemented-by: claude/issue-18536-lane-keyed-contract-review
Reviewed-by: session_01BTeBejoPUvRHN8WdAJC6oF

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)— PR #18903 · 席位定稿 2026-09-18T05:30Z(草稿在正文,以本评论为准)

改了什么:把你 9-17 说的车道规则写回章程与两个机读脚本:只有 spec 与 skills 两条车道的每一轮交付(不论 Clause-② 是 yes 还是 no)都要过契约复审档的复核——席位达档就席内审,不达档就起达档子代理;其余车道零契约复核,条款②命中即 spec 车道的活;子代理起不来时 PR 留 draft、队列外等档,唯一旁路是你亲审。三份章程行数不变(812 / 60 / 151);--tier 的提示行与 --pair 的复核记录人口同步改成按车道判。

为什么改:9-16 那次把「只在 spec 席」改成「按席位实测档」,席位名单没了,同一张 spec 车道 no PR 在两条细则下答案相反(#18536 的两种读法);你的裁决把车道规则说回来,本 PR 落到文本和脚本上。

风险与代价(含回滚):① spec 与 skills 车道的 no 轮从此也要 head 上有复核记录,没有就 --pair 退 4——这两条车道现在在飞的 no PR 入队前都要补记录(这正是 #18530 / #18529 该有的读数);其它车道不受影响。② 为了付新增那一行,退掉了 SKILL.md 里「报告席记条款②默认档 FAIL 率…超改制前达档史值 ⇒ 决策卡交维护者定回退」这一行——它是 9-09 改制(PR #16915)留的回退触发器,量的是"默认档自审"的失败率;按新规则没有任何车道再做默认档自审,这个指标没有人口了。请你知道这是删掉一个回退触发器,不只是改措辞。③ 机器侧一处取舍(dev 提问、席位裁 A):别的车道上被清标的 yes 配对仍退 4,但补救从「自审」改为「改道 spec」,退出码与今天相同。回滚 = revert 三个 commit,文本与脚本一起回到 0b31d90。

席位意见:建议批准。三条规则逐字落地,车道键与子代理路线同存;唯一值得你多看一眼的是第②条那行退役。留在下一张 SKILL.md 卡上的两处旧措辞(「借契约复审档隔离复核」与 references/ 落地行的「席内」)已记在 ACCEPT 里,不在本 PR。

你要做的:确认本 PR 如实落了你的车道规则,并接受第②条那行的退役,是则 APPROVE(GOVERNED_APPROVERS 账户);之后由本席按裁决 C 落地,再由各车道席清理挂在 spec/skills 之外的 needs:contract-review 载体。


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 23:50
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit e15870f Sep 19, 2026
44 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18536-lane-keyed-contract-review branch September 19, 2026 00:09
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…E on objectstack-ai#18373) (objectstack-ai#18946)

Fixes objectstack-ai#18373

Clause-②: no

`skip-changeset` — measured, not asserted; see Verification below.

Executes the maintainer ruling of 2026-09-18 on this card (batch objectstack-ai#153
item 3, **letter E**):
retire `check-type-source-resolution`. This PR is EXECUTION — it does
not re-argue A/B/C/D.

## What the ruling ordered, and where each piece landed

| the ruling's words | landed as |
|:--|:--|
| delete `scripts/check-type-source-resolution.mjs` and its self-test |
file deleted. Its "self-test" was the file's OWN `--self-test` dispatch,
not a separate file, so it went with it (`git ls-files` matched exactly
one path for the name). |
| `KNOWN_DIST_RESOLVED_TYPE_IMPORTS` with it | that registry lived
inside the deleted file; the identifier now has **0** occurrences
anywhere in the tree. |
| the `check:type-source-resolution` entry in the root `package.json` |
removed (one line). |
| the 「Type-source resolution gate」 step in `.github/workflows/lint.yml`
| step removed, together with the 23-line comment block that exists only
to explain it. |
| `AGENTS.md` and any doc that names the gate as a standing check |
**`AGENTS.md` names it zero times — re-measured here, see §1. This diff
does not touch the governed surface.** |
| `docs/audits/gate-census-2026-09.md:215` verdict | rewritten to the
ruling's exact text, see §2. |
| PR objectstack-ai#18708 | not touched. |
| `check:test-source-alias` (the vitest-axis sibling) | not touched, and
its ledger is not touched. |

## 1. `AGENTS.md`: the ruling's sentence describes a sentence that does
not exist

Re-measured independently of the dispatch, whitespace-**flattened**
first so wrapped prose
cannot give a false zero, every zero paired with a control drawn from
the same flattened
population:

```
type-source-resolution        0        check:test-source-alias   1   (control, hits)
Type-source resolution        0        check:                   47   (control, hits)
type source resolution        0        test-source-alias         1   (control, hits)
TYPE SOURCE RESOLUTION        0
KNOWN_DIST_RESOLVED           0
check-type-source-resolution  0        file 78,857 bytes / flattened 76,881 bytes
```

My reading **agrees with the dispatch's**. So the ruling's `AGENTS.md`
clause has no
referent, no line was hunted for there, and `AGENTS.md` / `CLAUDE.md` /
`.claude/**` /
`skills/**` / `docs/adr/**` are all absent from this diff.

## 2. The two censuses — deliberately different acts

**`docs/audits/gate-census-2026-09.md` — verdict REWRITTEN.** Its
verdict column is a
forward-looking *disposition* (what should happen to the gate), which is
exactly what a
later ruling can override. The row's verdict now reads
`retire · maintainer ruling 2026-09-18 on objectstack-ai#18373`, the ruling's own
spelling.
Because that is a NEW verdict spelling, the document's own verdict-count
table was kept
arithmetically true in the same edit: `keep` 133 to 132, a new row for
the new spelling at
1, `retire (all spellings)` 59 to 60, `keep (all spellings)` 150 to 149.
The union still
sums to 225 rows. Nothing else on the row moved — class, contract, blast
radius and the
measured catch window are what the census measured, and this PR did not
re-measure them.

**`docs/audits/2026-09-self-test-shape-census.md:341` — deliberately
LEFT ALONE.** The
dispatch flagged it as a second carrier the ruling did not name; it
holds a
`ROSTER | HELD` row for this gate. It gets nothing, for a reason, not by
omission:

- That document's header pins it to a tree — `origin/main` at
`d30ccb9bd`, re-verified at
`1be26b0de`. Its rows are not dispositions; each one is **the result of
a behavioural
probe run against that sha**. Retiring the gate today does not make "at
`d30ccb9bd` this
  script's self-test exited non-zero when it ran zero cases" untrue.
- **Deleting** the row would break the document's own arithmetic — 179
total, 165 HELD, 4
DEFEATED, 1 ACCIDENT, 9 NOT MEASURED — and with it the whole
reconciliation the document
exists to perform against objectstack-ai#15410's competing count of 170. A record you
can subtract
  rows from is not a record.
- **Rewriting** the verdict would assert a measurement nobody took.

Rule applied, and the same rule decides every prose carrier below: **a
sentence that makes
a present-tense claim about the gate acting is now false and is
repaired; a sentence
recording a past measurement or why a past change happened is not.**

## 3. The hard coupling: `check:ratchet-remedy-authority`, measured
before and after

That gate keeps a hand-classified control corpus keyed on gate FILENAME,
and its self-test
asserts the sweep reaches every entry. Both legs, run from this
worktree:

| leg | `--self-test` | main run |
|:--|:--|:--|
| **before** any change (at `02bdeaaf2`) | exit **0** | exit **0** — 257
scripts swept, 15 marked, 6 refused, control corpus 31 |
| **after deleting the file only** | exit **1** — `the sweep still
REACHES every known instance; it no longer reaches:
check-type-source-resolution.mjs` | exit **1** — `STALE: the control
corpus ... covers scripts/check-type-source-resolution.mjs, which is no
longer in the corpus. Drop the entry, or restore the file.` |
| **after the repair in this PR** | exit **0** | exit **0** — 256
scripts swept, 15 marked, 5 refused, control corpus 30 |

**The repair is the gate's own prescribed remedy, and no floor moved.**
What that gate pins
is `SELF_TEST_BATTERIES` — a roster of battery NAMES with a per-battery
count floor and a
pinned roster SIZE, and its own comment at the roster says deleting an
entry silences a
floor as effectively as zeroing it. That roster is a **different
registry** from the
control corpus, and it is untouched in substance:

```
declared batteries: 21      SELF_TEST_BATTERY_FLOOR: 21      sum of counts: 30
battery (12) count: 1   (unchanged)
```

The control corpus (`CONTROL`) has no pinned size — the gate prints
`Object.keys(CONTROL).length`
— and its STALE branch names dropping the entry as the fix. 257 to 256
swept, 6 to 5 refused
and 31 to 30 classified are the mechanical consequence of one file
leaving the corpus, not a
weakened floor.

Three further carriers in that same file, each judged by the rule in §2:

- `:16` "The precedents are ..." — present tense, names four files a
reader is told to open.
  The dead name is dropped; the other three stay.
- `:1221` the author-facing remedy "turn it down outright the way
`check-type-source-resolution.mjs` does" —
present tense, and after this PR it points an author at a file that does
not exist. The
exemplar is swapped to `check-test-source-alias.mjs`, the co-precedent
of the identical
PREDICATION shape that this same file already names at `:16` and in
battery (12).
  ⛔ This names that gate; it does not touch it or its ledger.
- battery (12)'s label and its assertion text ("the shape the two
registry gates use") —
present tense, now one gate. Label renamed, assertion reworded. Roster
size and the
  battery's own count are unchanged, so nothing is unpinned.
- `:662` "…which turned check-type-source-resolution's CORRECT remedy
into a reported
violation" — a record of a measurement that was taken and rejected.
**Historical: kept.**

## 4. The coupling the dispatch did not name:
`check-type-check-coverage.mjs`

Found by re-measuring rather than by the brief. That gate's **live,
author-facing** TEST_DEBT
graduation remedy told an author route (b) was "Available ONLY while
`pnpm check:type-source-resolution` still passes with the tests
re-admitted ... Run it before
you commit". After this PR that is a command that does not exist, in a
message whose whole
job is to tell an author which of two routes is open.

Repaired so it keeps the WARNING and loses the dead instruction: it now
records that the
gate that decided the route was retired under this ruling, that its
silence is ⛔ not a
clearance, that what it measured has not changed (the re-admitted tests
import workspace
packages the src program never held; it read red on 14 of the 18 entries
with an exclusion
to drop), and that (a) is the route to prefer.

**⛔ The self-test that pins that message is NOT weakened.** Its
`present` needles
(`check:type-source-resolution`, `SHRINK-ONLY`, `tsconfig.test.json`)
and the sibling
FUTURE_DEBT case's `absent` needles are left **byte-identical** — the
rewritten message
still carries all three, because it names the retired gate and its
former registry
explicitly. Only the case LABEL and its explanatory `why` changed. `pnpm
check:type-check-coverage`
exits **0** after the edit.

The other five mentions in that file (`:934`, `:986`, `:1099`, `:4462`,
and the `:537` /
`:5629` provenance notes) are records of measurements — "MEASURED as a
red `main`", "SINCE
MEASURED ... by dropping each entry's exclusion and reading
`check:type-source-resolution`",
"measured by doing it". Under the §2 rule the measurements are kept; the
two that also made
a present-tense claim about a live consumer (`:537`, `:5629`) now say
the gate was retired.

## 5. The other repo-root tooling carriers

- `scripts/typecheck-configs.mjs` — this library existed *because* two
gates needed the same
predicate. One is gone. Its self-test does **not** assert a consumer set
(checked: no
consumer array, only prose), so nothing reds; but "Two gates need this
predicate", "both
consumers resolve", "the two callers" and ":201
`check-type-source-resolution.mjs` imports
the predicates" were all present-tense and false. Repaired to name the
one live consumer and
record the retirement. ⛔ Folding the module back into its remaining
caller is explicitly
left as a separate decision — its cases are floored in its own dispatch
(PR objectstack-ai#15327) and a
  fold-in would not inherit that floor.
- `scripts/check-undeclared-dep-imports.mjs:42` — "the two gates that
look adjacent" is now one.
- `scripts/workspace-enumerator.mjs:66` — the `WORKSPACE_PARENT_GLOBS`
declaration list named
the deleted file; it now names the live one and records where the other
went.
- `scripts/pm/dispatch-gates.mjs` — five mentions, **all left alone**:
every one is a recorded
measurement in a docblock (pair counts of a matcher variant that was
measured and refused).
Historical under the §2 rule. The tool derives its families from
`package.json` and the
workflows at run time, so the retired gate simply leaves its output; it
needs no edit and
  reds nothing.
- `scripts/pm/check-clause2-carriers.mjs:8209` / `:8250` — **verified
offline before deciding,
and left alone.** The record is a frozen inline array of comment bodies
passed
`headSha: 'offline'`; nothing in it resolves a remote ref, and the two
mentions are branch
names inside a historical claim-contest fixture about this card,
unrelated to the gate.
- `scripts/typecheck-configs.mjs:202`'s stale comment about its importer
— see above; that is
  the comment the dispatch flagged as pointing the other way.

## 6. Out of scope, on purpose

- ⛔ **`packages/*/CHANGELOG.md` (five files) — untouched.**
`AGENTS.md:686` is unconditional:
a factual error in a released entry is repaired in a dedicated docs-only
PR, ⛔ never as a
rider on code changes. They are also *correct* as historical records of
what those releases did.
Same for `content/docs/releases/**` (which names it zero times anyway).
- **About 30 prose carriers in `packages/**/tsconfig*.json` comments,
test docblocks,
`packages/cli/bin/run-dev.js` and `examples/*/tsconfig.json` —
untouched, and reported to the
PM as a residual.** Boundary applied: the ruling scoped this diff itself
when it moved the lane
to `domain:devx` 「the diff is repo-root tooling, `package.json` and the
lint workflow」.
Editing those comments would pull roughly 18 packages and 3 examples
into the changeset,
change the diff's lane, and multiply the derived gate set — for comments
that mostly explain
  *why a `paths` rule exists*, a reason that outlives the gate.

## 7. The workflow step removal leaves the required context intact

Measured, not asserted. The required context is the JOB's `name:`, and
no context name is
derived from a step:

```
BASE 02bdeaa : lint job `name:` = Lint & Repo Gates   steps = 179   (step present)
HEAD           : lint job `name:` = Lint & Repo Gates   steps = 178   (step absent)
```

The job keeps 178 other steps and its name is byte-identical, so the six
required contexts
are unchanged. `pnpm check:required-contexts` exits 0. ⚠️ One wording
note for the record:
the ruling writes the job as 「Lint and Repo Gates」; the job's actual
`name:` is
`Lint & Repo Gates` (ampersand). Same job, and the ruling's conclusion
holds.

## 8. `skip-changeset`, measured

Criterion: nothing already published moves. Measured against every
workspace manifest's
`files[]`, with a positive control proving the reader works rather than
merely reporting zeros.

```
changed paths (9)                              files[] reaches
  .github/workflows/lint.yml                     NONE
  docs/audits/gate-census-2026-09.md             NONE
  package.json                    (private:true) NONE
  scripts/check-ratchet-remedy-authority.mjs     NONE
  scripts/check-type-check-coverage.mjs          NONE
  scripts/check-type-source-resolution.mjs       NONE   (deleted)
  scripts/check-undeclared-dep-imports.mjs       NONE
  scripts/typecheck-configs.mjs                  NONE
  scripts/workspace-enumerator.mjs               NONE

POSITIVE CONTROL — must be reported as reached
  packages/spec/src/data/query.zod.ts            @objectstack/spec   (glob entry)
  packages/cli/dist/index.js                     @objectstack/cli    (directory entry)
  packages/spec/CHANGELOG.md                     @objectstack/spec   (literal entry)
```

3 of 3 controls hit, across two packages and all three `files[]` entry
kinds, so the zeros
above are readings and not an empty read. The root `package.json` is
`private: true` and is
never published at all.

## 9. Verification

Gate set derived in-worktree with
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths
passed — the tool takes its own change set from the merge base), then
run. Union taken at
`13b2b68ef`, the final commit.

- **69 of 69 derived commands run. 63 exit 0.**
- **6 exit 3 = `PREREQUISITE NOT MET`, NOT MEASURED, declared to CI**:
`check:dts-closure`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:sourcemap-no-sources-content`,
`check:type-check-debt` and `@objectstack/lint
check:doc-formula-expressions`. Every one of
them refuses because this worktree has no build; each prints its own
"this is NOT a pass"
line and exits 3 rather than 1. They are derived from the root
`package.json` edit, and they
read the `dist/` of packages this diff does not touch — this diff
changes no package source,
  so their verdict cannot depend on it. CI's build lanes measure them.
- `pnpm lint` (`eslint . --no-inline-config`, the whole repo, no
narrowing) — exit **0**.
- `pnpm check:ratchet-remedy-authority` — exit 0, before/after table in
§3.
- `pnpm check:type-check-coverage` — exit 0.
- `pnpm check:pm-dispatch-gates` — exit **0**, `dispatch-gates
self-test: 1848 cases pass`
(976.3s on this box; run on its own because it does not fit a ten-minute
foreground window).
- `pnpm check:required-contexts`, `check:step-collectors`,
`check:self-test-wired`,
`check:self-test-workflow-commands`, `check:aggregator-roster`,
`check:scripts-symbol-anchors`,
`check:declaration-mirrors`, `check:ci-filter-parity`,
`check:nul-bytes`,
  `check:workflow-step-name-quoting` — all exit 0.
- Control-byte self-scan over every changed file
  (`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`) — clean.
- ⚠️ `dispatch-gates` prints its own warning that `--commands` is
**not** a complete account
of CI; the artifact-roster, wide-population, workflow-valued and
path-scheduled CI families
  are outside that list by construction.
- ⚠️ `dispatch-gates` also reported a STALE TREE note: `origin/main`
moved 3 commits after this
branch point and `scripts/pm/check-widening-tells.mjs` changed there.
The derivation itself
uses three-dot merge-base semantics, so the change set above is correct;
the note affects only
  that one family's shape. No path of this diff overlaps it.

## 10. Serial constraint with two in-flight PRs

Both **objectstack-ai#18889** (draft) and **objectstack-ai#18414** (open, non-draft) also edit
`.github/workflows/lint.yml`
and the root `package.json`. Re-checked immediately before pushing:
**both are still open and
unmerged**, so neither had landed under this branch. This diff is
written to survive either
landing first — ⛔ no line number was used as a reading:

- the workflow step is located by **its own name**, and the edit
asserted the literal text of
`- name: Type-source resolution gate`, its `run:` line and the first
line of its comment block
before removing anything; a shifted file fails the assertion instead of
deleting the wrong step.
- the `package.json` entry is matched as a **unique exact string**,
never by offset.

The ruling's `.github/workflows/lint.yml:4187` and the dispatch's
`package.json:172` were both
treated as clues; both happened to still be correct at `02bdeaaf2`, but
nothing here depends on that.

Also re-measured against a freshly fetched `origin/main` (`46559f61c`,
five commits past this
branch point): **none of those five commits touches any of this diff's
nine paths**, so no merge
was needed and no line re-derivation was owed.

**objectstack-ai#18708 is closed, unmerged** (2026-09-18T06:37Z) — confirmed here, not
assumed. This PR does not
touch it. **objectstack-ai#18903** is editing `scripts/pm/check-clause2-carriers.mjs`,
the file holding the
frozen objectstack-ai#18708 fixture this PR deliberately leaves alone — adjacent, not
overlapping.

## Acceptance notes

- Noted, not filed: the gate census's inventory counts (182 check files,
225 rows) are pinned
to the census's own tree and were deliberately not re-derived — only the
verdict column and
its roll-up were touched. Carrier for a future re-derivation: whoever
executes the next
  batch of the 58 remaining `retire` rows.
- Noted, not filed: `docs/audits/gate-census-2026-09.md` now carries a
verdict spelling
(`retire · maintainer ruling ...`) that no other row uses, where the
existing convention for a
ruling-driven retirement is the verdict `retire (ruled)` plus `ruled
retire: #NNNNN X` in
column 3. The ruling's literal text was followed rather than the
convention.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…e the 27 signature hashes (objectstack-ai#18971)

Fixes objectstack-ai#16045

Clause-②: yes (widening)

Ruled at `5560224701` (director batch objectstack-ai#60, 2026-09-06, maintainer
verbatim 「同意」), re-affirmed by triage at `5724532096`: option A, a
readable declaration-text snapshot, ⛔ not a hash. The card body's three
mutually exclusive routes predate that ruling and were not re-litigated
here.

`@objectstack/spec` pinned its public surface on one axis.
`api-surface/` records each export as `name (kind)`, and a signature
change, a renamed interface field and a dropped union member move
**none** of those rows. The only shape pin was
`api-surface-signatures.json`: 27 rows, and reference-level even there.
This adds `api-surface-declarations/`, the declaration text the packed
build actually emits for every export of every published entry point,
and retires the 27 hashes it subsumes.

## The counts, re-derived on this head before the first generation

The ruling asks for this by name; the card's own numbers were
self-declared unverified and 12 days old.

| Number | Card | This head (`b33898f5d`) | Unit, and what would make it
something else |
|---|---|---|---|
| entry points | 17 | **17** | type entry points in the `exports` map —
those whose `require.types` ends in `.d.ts`. Adding or removing one such
subpath. |
| `exports` map entries | (not stated) | **19** | every key in the map.
The extra two are `./openapi.json` and `./package.json` — asset subpaths
with no declaration at all, filtered out by the same `.d.ts` test
`build-api-surface.ts` has always applied. ⇒ premise 1 resolved: **17 is
right and the map did not grow**; 19 counts two things that were never
entry points. |
| pinned rows | 5309 | **5336** | `name (kind)` rows summed over the 17
`api-surface/` shards. +27 since the card. Ratio unmoved: 27/5336 =
0.51%, so the headline 99.5% stands. |
| distinct exported names | (not stated) | **5200** | (entry, name)
pairs. The gap to 5336 is dual-declared names, which are two rows by
design. |
| signature hashes | 27 | **27** | top-level keys of
`api-surface-signatures.json`. Bright control: the first value really is
a `sha256:` string, so this counts signature entries and not empty
objects. |

Premise 3 also holds: all 17 packed `.d.ts` files exist and resolve
through the map (3,215,437 bytes for the root entry down to 13,081 for
`./integration`). No entry point lacks a packed declaration, so the gap
the dispatch reserved for itself did not open.

## What the artefact costs — premise 4, which nobody had costed

| | |
|---|---|
| shards | 17, one per entry point |
| declaration blocks | 5336 |
| bytes | **12,661,943 (12.08 MiB)** |
| lines | **237,706** |
| gzipped | **1,071,825 (1.02 MiB)** — against this package's ~17.57 MiB
compressed `dist`, so about **+5.8%** of tarball |
| largest shard | `system.txt`, 3,592,701 bytes / 73,283 lines |
| median declaration | **81 bytes** |
| skew | the 20 largest declarations hold **~65%** of all bytes; four
exceed 20,000 lines each (`EnvironmentArtifactSchema` 21,868,
`ObjectStackDefinitionSchema` and `ObjectStackSchema` 21,851,
`ChangeSetSchema` 20,395) |

Stated plainly, as the dispatch asks, and ⛔ not as a veto: the packed
`.d.ts` is a tsup dts rollup, so a Zod schema's declaration is its
**fully expanded** structural type. That expansion is exactly what makes
an inner field rename visible — and it is also why a single schema can
produce a 21,000-line diff. The ruling's stated reason for choosing text
over a hash is that the contract-review seat reads the diff; that
reasoning holds per declaration and is worth a second look at the top
twenty. One reading, for whoever wants it: 31% of declarations hold
97.7% of the bytes, so nothing cheap is available by trimming the tail.

## Both instruments, measured on one tree at one commit

The card's thesis is that the old pin cannot fail on a shape change. Not
argued — ablated, with the mutation proven on disk by blob hash and the
mutation proven to have reached `dist/` before any verdict was read.

**A. the source-level control — a renamed interface field, the card's
own class.** `JobRunOutcome.reason?` renamed to `degradationReason?` in
`packages/spec/src/contracts/job-service.ts` (blob `363443e2` to
`d4b1520c`), spec rebuilt, `ablation-dist-preflight` exit 0 confirming
the marker reached the built artefact:

```
check:api-surface              exit=0    "public API surface unchanged"      [BLIND]
check:api-surface-declarations exit=1    "~ JobRunOutcome (interface)"       [SEES IT]
```

Restore leg: blob back to `363443e2`, rebuilt, `ablation-dist-preflight
--absent` exit 0 (marker gone from all 214 built files), `git diff HEAD`
clean, gate back to exit 0.

**B. the gate can fail on its own artefact.** One field renamed inside
`qa.txt` by hand (blob `3f5efb04` to `5b86fec2`, injected occurrences 1,
deleted text 0): exit **1**, attributed to `TestSuiteSchema (const)`,
failure text naming the regenerate command. Restored to the HEAD blob,
`git diff HEAD` empty: exit **0**.

## The retirement, and the coverage proof the ruling demands

All **27** signature names resolve to a declaration block in
`api-surface-declarations/root.txt`, **0 missing** — enumerated from
`defineAction` through `defineWebhook`, each as `(function)`.

One honest qualification, because the subsumption is not uniform. For
those 27 factory declarations the text is `declare function
defineAction(config: z.input of ActionSchema): ActionParsed;` — a type
**reference**, exactly as blind to an inner-key narrowing as
`typeToString` was. What is gained is not sharper text on the 27; it is
the **5309 other declarations**, including `ActionSchema` itself, whose
own expanded block is where such a narrowing shows up. So the retirement
is a strict superset of pinned declarations, not an equal trade. Nothing
published read the retired file — it was never in this package's
`files[]`.

## Where it lands, and why there

- Generator: `packages/spec/scripts/build-api-surface-declarations.ts`,
beside the eight sibling artefact generators, reading the same input
through the same `collectEntries` logic. The ruling says "one generator
script under `scripts/`"; this reads that as the directory the whole
family lives in, because the artefact reads the **built dist** and only
the lane that builds spec can run its gate.
- Artefact: `packages/spec/api-surface-declarations/ENTRY.txt`, a
sibling **directory** of `api-surface/`. Not inside it: `listShardNames`
throws on any file in that directory that is not a `NAME.json` shard, so
`api-surface/` is closed by construction. No existing
`api-surface/*.json` is regenerated by this PR (`check:api-surface`
green throughout), which keeps it clear of PR objectstack-ai#18688 and PR objectstack-ai#18319.
- Gate: `check:api-surface-declarations`, a step in lint.yml's `Type
Check · consumer gates` lane after the two build steps, with
`check:api-surface` and the other dist-reading gates. **No new required
context** — a step in an existing lane. Registered in the
`check:generated` ledger, in `REGEN_ARTIFACTS`, and in `.gitattributes`
as `merge=os-regen`.
- Sharded per entry point from day one, for the reason its neighbour is:
the merge queue rebuilds server-side where no custom driver runs, so two
PRs sharing one generated file evict the second. Pit 1 from `5715457322`
is answered by the layout rather than by an assumption — and
`check:merge-driver`, which reconciles `.gitattributes` against
`REGEN_ARTIFACTS` in both directions, is green over the swap.
- Published, with the reason the gate demands. `check:published-files`
refuses a `files[]` entry that carries none; the registered line says
what a consumer does with it — read two published tarballs and see
*which declared shape* moved between releases, the question
`api-surface` cannot answer. If 1.02 MiB of tarball is judged too much,
one line of `files[]` removes it without touching anything else.

Three registries had to learn about the new gate, each because it
discovered the gate on its own rather than because a list named it:

- `check:published-files` — demanded the reason above.
- `scripts/pm/dispatch-gates.mjs` — its live manifest edge gave the new
gate a population before anything listed it, which is the eighth member
of a class whose seventh was recorded the same way. Declared as
`CLASS_EIGHTH`, with a case asserting the edge really reaches it.
- `scripts/pm/check-widening-tells.mjs` — `PUBLISHED_SURFACES` is
derived from `REGEN_ARTIFACTS`, so retiring the signatures row dropped
it off that surface and reddened two self-test cases. Both are
retargeted to state the retirement as a counterfactual (the surface
follows the table, not a literal); ⛔ the new artefact is **not** added
to that surface, because the ruling assigns "is a snapshot diff a
Clause-② signal" to the skills seat by name and out of this card's
scope. Both directions are now pinned, so the boundary is declared
rather than forgotten. 483 cases pass, up from 481.

## Verification

- **Gate families**: derived with `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --commands` from the merge base, 120
commands, every exit code redirected to a file and read back. **All 120
green.** Four returned exit **3** PREREQUISITE NOT MET on first pass
(`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:type-check-debt`); each names a
build, each was built and re-run green, and none is recorded as a
finding. Reconciled with `--ran`.
- **Tests**: `@objectstack/spec` local project **488 files / 14,182
tests passed**; the tooling suites that name the edited scripts, both
projects, **10 files / 220 tests passed** (`sharded-artifacts`,
`check-generated-ledger`, `dist-freshness`, `dist-freshness-adoption`,
`api-surface-dual-kind-rows.pin`, `build-schemas-check-mode`,
`def-key-collisions`, `root-index`, `export-list`,
`docs-import-surface`). `pnpm --filter @objectstack/spec typecheck`
green.
- **eslint, the union rather than a narrowing**: `eslint .
--no-inline-config --format json` at `b33898f5d` examined **6856
files**, **0 errors, 0 warnings**, exit 0. The population is eslint's
own config resolution and the count is read from its JSON output;
type-aware linting is not enabled in `eslint.config.mjs` (no
`parserOptions.project`, no typed rules), so this diff cannot move an
untouched file's verdict either way.
- **Control bytes**: `check:nul-bytes` green over 8906 files, plus a
direct scan of all 31 changed paths for the wider control-byte class —
no matches.
- `scripts/check-single-claim-paths.mjs` in the diffstat is **not
mine**: it arrived with the one-commit `origin/main` merge (`16cb493d5`)
this PR carries.

## Acceptance notes

- `.claude/skills/spec-property-retirement/SKILL.md` line 124 lists
`api-surface-signatures` as an instance of a retirement shape, and that
row goes stale with this landing. ⛔ Left untouched on purpose:
`.claude/**` is a governed surface, so editing it would make this whole
PR maintainer-landed for a one-word prose nit. Noted, not filed.
- `packages/spec/scripts/build-schemas.ts` line 830 carries the same
stale mention. Left untouched because PR objectstack-ai#18952 holds that file; noted,
not filed, with the later lander as the natural carrier.
- Three files in this diff are held by open PRs and were edited anyway
because the retirement forces it, not by choice:
`scripts/pm/check-widening-tells.mjs` (PR objectstack-ai#18948),
`scripts/pm/dispatch-gates.mjs` (PR objectstack-ai#18903) and
`.github/workflows/lint.yml` (PRs objectstack-ai#18946, objectstack-ai#18889, objectstack-ai#18414). All are
hand-written files where a text conflict is visible rather than silent,
and all three of my hunks are small and far from theirs. Whoever lands
second resolves.
- The top-20 skew above is a reading, not a finding: no gate is wrong
and nothing is unenforced. It is recorded here because the ruling's own
justification for text over hash is per-declaration readability, and at
21,000 lines a declaration that argument thins out.

## 维护者速读(草稿)

**改了什么。** `@objectstack/spec` 从今天起为它的**每一个**公开导出留一份"形状快照" —— 不是哈希,而是打包后
`.d.ts` 里那段声明原文,按入口点分成 17 个文件签入仓库,并配一道 CI
闸门:重新生成后对不上就红,失败信息里直接给出重新生成的命令。同时退休了旧的 27 条签名哈希文件。

**为什么改。** 原来的 pin 只记"某个名字还在不在",5336 行里只有 27
行能看出"形状变没变"。也就是说:把一个接口字段改名、砍掉一个联合成员、改一个函数签名 —— 这些都是会让客户升级后编译失败的破坏性改动 ——
全部一路绿灯。本次 PR 里有实测:改了 `JobRunOutcome` 的一个字段名之后,旧闸门 `check:api-surface`
退出码 **0**(看不见),新闸门退出码 **1**(点名了那个 interface)。路线是 2026-09-06 决策批次 objectstack-ai#60
里您逐字「同意」的那一条。

**风险与代价(含回滚)。** 代价是体积:12.08 MiB 文本、23.7 万行,压缩后 1.02 MiB,相当于 npm 包增长约
5.8%。更值得注意的是分布极不均匀 —— 最大的 4 个 schema 各自超过 2 万行声明文本,一旦它们变动,复核席位面对的是一份 2
万行的 diff;而裁决选"文本不选哈希"的理由恰恰是"diff 可读"。这一点我按实测如实报告,未自行改动路线。回滚成本很低:从
`files[]` 去掉一行即可停止随包发布;整道闸门回滚就是撤销本 PR,不留任何数据迁移。

**席位意见。**

**你要做的。** 只有一件事需要您判断:12 MiB / 23.7 万行这个量级,以及最大 4 个 schema 的 diff
可读性,是否仍符合当初选 A 方案时的预期。若认为需要收窄,那是裁决层面的一次增补,不是本 PR 的返工。其余部分已按裁决落地并自证。

---
_Generated by [Claude
Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…the question — a zero-hit reading names the instrument's reach and one known target outside it (objectstack-ai#18921)

Fixes objectstack-ai#18755
Clause-②: no

## What the rows add

The 平台读数纪律 band pairs every zero with a lit control on the same
instrument and reads two zeros on one instrument as a dead instrument
(SKILL.md :163–:164, core-rules :45). That catches a DEAD instrument. It
does not catch a LIVE instrument pointed at a question its domain cannot
see, and from inside the rule the two look identical. The card measured
it twice: a content `git grep` with a lit control (53 files) and an
absent control (0) read 1 for a test suite that exists — the suite's
name is a FILE NAME, which a content grep cannot see; and `git grep -l
NamedListView` (16 mentioning files, both controls fine) answered "which
file DECLARES it" with `views.ts` when the declaration is in
`objectql.ts` — a content grep cannot tell a declaration from a mention.

The deliverable's shape is the triage's (comment 5720454579, verbatim):
「承接席:交付物是**判别式**,⛔ 不是口号 ——
要能回答「我这个零,是仪器活着,还是问题问对了」。建议至少包含:**该工具在语料上的可达半径**(它**能**看见哪些形态),以及**一个必然落在半径之外的反例**(一个已知存在、但该问法必定看不见的目标)。」
The filer proposed no wording (「⛔ No rule text is proposed here; the gap
is the product」); the wording here is mine.

**SKILL.md, 平台读数纪律, directly under the 双零 rule** (bytes measured with
`len(line.encode())`, leading `- ` included):

- `- 控制通过 ≠ 问题问对:零命中须写仪器可达半径与一个必在半径外的已知目标。` — 107 B
- `- 半径按形态写(内容/文件名/声明/字段);所求在半径外 ⇒ 零非读数,换看得见它的仪器。` — 116 B

**core-rules, 平台读数纪律, directly under its 零命中/双零 row** (the twin,
compressed):

- `- 控制通过 ≠ 问题问对:零命中须写仪器可达半径与半径外已知目标,半径外的零非读数。` — 117 B

The discriminator is a test the seat performs, not a slogan: (1) write
the instrument's reach BY SHAPE — contents, file names, declarations,
API fields; (2) name one target known to exist that this asking cannot
see; (3) if the sought target is a shape outside that reach, the zero is
not a reading — switch to an instrument whose reach contains the shape.
The head clause is the sibling sentence the triage asked for beside
「不可验证 ≠ 被违反」; the `≠` spelling already lives on the file (`declared ≠
enforced`, :369).

## Reader test — both instances answered by the rows alone

A seat holding 「content grep, lit control fired, absent control 0,
target 0」 reads the rows:

1. **File existence by name.** Row 2 makes the seat write the reach by
shape: `git grep` sees 内容. The sought thing is a 文件名 — a shape on the
list and not the one written — so 所求在半径外 ⇒ the zero is not a reading,
and the row sends the seat to an instrument whose reach contains file
names (`git ls-tree` / `git ls-files`). Row 1's counterexample
obligation produces the same answer from the other side: a target known
to exist that a content grep cannot see is exactly "a file whose name no
file body mentions" — the very target being sought.
2. **Which file declares a symbol.** Reach by shape: a content grep sees
内容 (mentions). The sought thing is a 声明 — on the list, not the reach —
so the 16-file answer is not a reading of "declares", and the seat
switches to a declaration-shaped read (the `^(export
)?(const|type|interface) NAME` spelling already recorded in
`references/platform-readings.md`, 读数六坑 ③). The known-existing target
outside the reach: the one declaring file among the sixteen, which the
mention grep cannot single out.

Neither answer needs the card, the reference, or the triage comment —
the shape list on row 2 is what makes the question 「can this instrument
see a file name / a declaration?」 mechanical rather than a matter of the
seat's imagination.

## Line budget — both files at headroom 0, ratchet's own lines quoted

```
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0).
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/core-rules.md is 151 lines (ceiling 151; headroom 0).
```

SKILL.md 812 → 812 (+2 rows, −2 rows, both retirements in-band, both
paid by deleting content that survives on a neighbour — ⛔ no re-wrap):

- **Retired :175** 「main-red 的跳队例外与事故锚卡约定见
`references/landing-operations.md` B 节。」 — a pointer, not a rule. The
two 约定 it points at are unchanged in landing-operations B; the pointer
folds into the adjacent rule it serves, :174 → 「advisory 门禁红着进 main
是共享损伤,任何车道发现都立即止血并立单,见 landing-operations B。」 (118 B).
- **Retired :178** 「立卡者不查重,只附 3–5 个查重词;分诊按词查自有列表,零命中须控制词背书。」 — three
clauses, each already stated on the file: 「零命中须控制词背书」 IS :163;
「分诊按词查自有列表」 is :315 (查重缓存住席内) and :366 (跨仓查重); the filer clause is
re-packed onto the adjacent duplicate rule, :180 → 「立卡者不查重、只附 3–5
查重词;真撞上重复,先比数值与作用域再决定关哪个。」 (107 B), and also lives at os-dev.md :50 and
core-rules :80.

core-rules 151 → 151 (+1 twin row, −1 line): the two intro PROSE lines
(:2–:3, not rules) compress into one, 「每行一条规则,按 SKILL.md 章节分组;细节以
SKILL.md 与其它 references 为准,⛔ 不新增规则。」 (114 B). Dropped words: 有约束力的 (the
file is 核心条款 by title), 供人工复核 (the title says 人读摘要), 参数、事实表与操作配方 (→ 细节),
同目录. No rule row was demoted.

Why not the in-place widening of core-rules :45 the dispatch preferred:
:45 is 117/120 B (3 B of headroom); the shortest one-row form carrying
reach + counterexample + consequence measures 115 B on its own, so it
cannot share a line with :45.

Why two SKILL.md rows and not one: a 117 B single row exists (「控制通过 ≠
问题问对:零命中须写仪器可达半径与半径外已知目标,半径外之零非读数。」) but drops the shape list and the
switch-instrument remedy, and the shape list is what answers the reader
test above mechanically; both rows are paid.

## Subset relation, held by hand

core-rules ⊆ SKILL.md is enforced by no gate. The twin row compresses
the two SKILL.md rows and states nothing they do not. Every clause of
the two retired SKILL.md rows survives on the lines named above, so
core-rules :80 (「立卡者不查重、只附查重词」) keeps its SKILL.md source (:223, the
re-packed :180, :777). The four-axis block (SKILL.md :733–:754) is
untouched; `check:skill-frame-sync` and `check:skill-frame-freshness`
are green.

## In-flight overlap

Draft PR objectstack-ai#18903 (card objectstack-ai#18536) is open on both files. Its hunks: SKILL.md
:512, :522, :636–:646 and the retired :672; core-rules :112. This PR's
hunks: SKILL.md :164–:166, :174–:175, :178–:180; core-rules :2–:3,
:45–:46 — disjoint. `origin/main` was merged at 625db0e (merge commit
6450bcc) and the gates below ran on that head; the four `main` commits
that landed after it touch only `scripts/pm/check-half-states.mjs`.

## Gates — derived, run, reconciled at 6450bcc

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths; the change set from the merge
base) derived 20 commands, identical before and after the `main` merge.
Every one was run with its exit code captured before any pipe;
reconciliation: `Run reconciliation — 20 derived, 20 run, 0
NOT-MEASURED, 0 UNRUN.`

All 20 exit 0. One needed its declared prerequisite first: `pnpm
--filter @objectstack/lint run check:doc-formula-expressions` exited 3
(`PREREQUISITE NOT MET` — `@objectstack/formula` and `@objectstack/lint`
not built; nothing measured), the build ran under `os-verify-lock.sh`
(`VERDICT command-exit 0 · held the lock 147s`), and the re-run exited
0. Verdict lines:

```
✓ check-skill-id-lint: 27 file(s) clean (pattern /#[0-9]{3,}/g).
✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces … and claim no others.
✓ check-skill-frame-sync: the one declared copy of the decision frame is internally coherent (.claude/skills/pm-dispatch/SKILL.md; …)
check-nul-bytes: OK (scanned 8886 text file(s) … no raw ASCII control bytes).
```

Extra, named by the dispatch and not derived: `pnpm
check:skill-frame-freshness` — exit 0 (「the frame itself is unchanged —
that is fine」).

## skip-changeset

Nothing publishes: both paths are under `.claude/**`, a fast-lane
unpublished surface in no package's `files[]`.

## Acceptance notes

- noted, not filed: `references/platform-readings.md` :283 already
carries the declaration-shaped regex as the harder instrument for the
mention-vs-declaration case; the new row sends the seat to "an
instrument whose reach contains the shape" and the reference holds the
spelling — no change owed there. 承接者:无.
- noted, not filed: `check:skill-frame-freshness` is not in the derived
list for a diff on these two files while `check:skill-frame-sync` is;
run as an extra, green. 承接者:无.

## 维护者速读(草稿)

**改了什么**:PM 读数纪律里加了一条判别式,分两行写在「零命中须配控制词」「控制词双零 =
仪器坏」之后:控制词通过只证明工具是活的,不证明问题问对了;报一个零命中时,要写清这个工具在语料上能看见哪些形态(文件内容 / 文件名 / 声明
/ 接口字段),并举一个已知存在、这种问法却必定看不见的目标;所求的东西落在可达半径之外,这个零就不是读数,换一个看得见它的工具再报。核心条款同
PR 加一行压缩版。

**为什么改**:这一班量到两次同型事故——用文件内容 grep 去判「这个文件存不存在」(文件名不在内容里,读 1
个命中而文件明明存在),用「哪些文件提到这个符号」去答「哪个文件声明它」(声明和提及在内容 grep 里长得一样,把 `views.ts`
报成了 `objectql.ts` 的活),第二次已经写进认领评论、当成串行围栏用了。既有规则对这两次全部放行:控制词都点亮了、缺席控制也读
0,规则内部看不出问题问错。

**风险与代价(含回滚)**:两个文件都在行数棘轮上限(812 / 151),新增行靠删同文件里已在别处说过的内容付账:SKILL.md
退掉一行纯指针(内容折进它服务的止血规则)和一行三句皆有重复的汇总行(立卡者附查重词那句挪到相邻的查重规则上);核心条款把两行说明文字压成一行。没有规则被降级或删除,门禁全绿。回滚
= revert 这一个提交。

**席位意见**:(留空,由席位定稿)

**你要做的**:一个动作——审阅并批准这个 draft PR(受管面 `.claude/**`,需要维护者的批准后由席位落地)。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…n board of both repos — same finding ⇒ `duplicate_of`, never a second dispatch (objectstack-ai#18981)

Fixes objectstack-ai#18963
Clause-②: no

Governed rules layer (`.claude/skills/pm-dispatch/SKILL.md`) — draft PR,
four-piece terminal; the skills seat's contract-tier review, then the
maintainer's approval lands it. `skip-changeset`: nothing published
moves (`.claude/**` is internal agent tooling, in no package's
`files[]`).

## What changed — three rows of the 分诊 band, 812 / 812 held, no line
added, no re-wrap

The triage band's shadow rows read as a cross-repo keyword search only,
and the dedupe step that would have caught two cards on one file and one
defect — graded two days apart into two lanes by the same seat, both
dispatched, the second PR superseded — did not exist in the rules text:
a dedupe by FILE / MECHANISM against the OPEN board, own repo included,
before keywords, with the finding (not the card) as the unit. Governing
triage reading, verbatim: 「缺的不是勤勉,是一条按『文件/机制』而不是按『关键词』去重的步骤」 ·
「去重的单位是『发现』,⛔ 不是『卡』」.

| line | before (bytes) | after (bytes) |
|---|---|---|
| :366 | 「跨仓查重(shadow 检查):跟跨仓引用 + 关键词搜各姊妹仓。」 (75 B) | 「查重/shadow
检查先按文件/机制查本仓与姊妹仓 open 卡(含 `pm:dispatched`),再跟引用与关键词。」 (120 B) |
| :367 | 「shadow 命中在飞 ⇒ `Blocked-by:` 不派;open 未认领 ⇒ 先收敛成一个派发入口。」 (99 B)
| 「同文件同缺陷 = 同一发现,不分车道:证据搬到先卡,后卡关 `duplicate_of`,⛔ 不并排派发。」 (115 B) |
| :368 | 「shadow 命中已完成 ⇒ 卡可能过期。」 (47 B) | 「其余在飞 ⇒ `Blocked-by:` 不派;open
未认领 ⇒ 先并成一个派发入口;已完成 ⇒ 卡可能过期。」 (119 B) |

Band total 221 B → 354 B (+133 B) at +0 lines; every row is at or under
the 120-byte cap (120 / 115 / 119). Lines :223
(「分诊座位唯一生产:定级/路由/type/查重/shadow/`duplicate_of`…」) and every line outside
:366–:368 are byte-identical to `origin/main` — the diff is 3 insertions
/ 3 deletions on one file. The parallel-draft bands of PR objectstack-ai#18903
(:509–:670) and PR objectstack-ai#18921 (:162–:183) are untouched.

## What the new rows say (measured against the card's requirements)

- **By file / mechanism, before keywords, over the OPEN board of both
repos, `pm:dispatched` included** — :366: 「先按文件/机制查本仓与姊妹仓 open 卡(含
`pm:dispatched`),再跟引用与关键词」. The population is stated once, in the same
row (the PM's mechanism assumption 2, confirmed: 「跨仓」 became 「本仓与姊妹仓」 in
place).
- **The unit is the finding, not the card** — :367: 「同文件同缺陷 =
同一发现,不分车道」; the later card's evidence moves to the earlier card
(「证据搬到先卡」), the later card shuts as `duplicate_of` (「后卡关
`duplicate_of`」, the state the :136 row already binds to the `duplicate`
reason), and it is never dispatched beside the earlier one (「⛔ 不并排派发」).
- **Every hit that is NOT the same finding keeps the old trichotomy** —
:368: in flight ⇒ `Blocked-by:`, not dispatched; open and unclaimed ⇒
fold into ONE dispatch entry first; completed ⇒ the card may be stale.
「其余」 scopes the row to what :367 did not already settle, so a
same-finding sibling in flight lands on `duplicate_of`, not on
`Blocked-by:`.

## Retired clauses and their survivors (nothing retired without one)

| retired spelling | survivor |
|---|---|
| :366 「跨仓查重(shadow 检查)」 | :366 「查重/shadow 检查 … 本仓与姊妹仓」 — same name (the
:310 spelling 「查重/shadow 检查」), population widened to both repos |
| :366 「跟跨仓引用 + 关键词搜各姊妹仓」 | :366 「再跟引用与关键词」 — same two methods, now
second to file / mechanism; 「各姊妹仓」 is carried by 「本仓与姊妹仓」 earlier in the
row |
| :367 「shadow 命中在飞 ⇒ `Blocked-by:` 不派」 | :368 「其余在飞 ⇒ `Blocked-by:` 不派」
— same mechanism, scoped to hits that are not the same finding |
| :367 「open 未认领 ⇒ 先收敛成一个派发入口」 | :368 「open 未认领 ⇒ 先并成一个派发入口」 — 「收敛成」 →
「并成」 (3 B) is the only compression; 「先」, 「open 未认领」 and 「一个派发入口」 all
kept |
| :368 「shadow 命中已完成 ⇒ 卡可能过期」 | :368 「已完成 ⇒ 卡可能过期」 — folded into the
trichotomy row |

The 「shadow 命中」 subject of the old :367/:368 is now carried by :366
naming the check and :368's 「其余」 reading against :367; no row outside
the band was touched to make that binding.

`references/core-rules.md` (151 / 151): its only shadow mention is :56
「跨仓查重与 shadow 检查恒归中央 ⛔ 不下放」 — a rule about WHO runs the check (central
triage), not about its population or order, and it stays true under the
new rows (:55 already makes same-repo dedupe central too). No rule moved
there, so no twin is owed (the PM's mechanism assumption 3, confirmed by
`git grep -i shadow` on that file: 1 hit, :56).

## Reader test — the second grading, replayed under the new rows

At 2026-09-18T00:06Z the triage seat grades objectstack-ai#18844
(`check-single-claim-paths.mjs` 401s because node `fetch` ignores
`HTTPS_PROXY`; remedy: the proxy re-exec the sibling scripts already
carry). Platform state of objectstack-ai#18314 at that instant, from its label events:
open, `pm:queue` + `domain:spec` + `priority:p2` since
2026-09-17T10:10Z, unclaimed (`pm:dispatched` and the assignee arrived
at 05:49Z, five hours later — so at grading it was a queued sibling, not
yet an in-flight one; the dispatch text's 「`pm:dispatched` when objectstack-ai#18844
was graded」 describes the later claim-time instant, which is the seat's
own half, objectstack-ai#18964).

- :366 — search the open board of this repo by FILE:
`check-single-claim-paths.mjs` ⇒ objectstack-ai#18314's title carries the file name
verbatim; it is on the open board (`pm:queue`; had it already been
`pm:dispatched`, 「含 `pm:dispatched`」 keeps it in the population).
- :367 — same file, same defect (`fetch` vs `HTTPS_PROXY`, the re-exec
remedy), a different lane (`domain:spec` vs the skills seat's filing) ⇒
「同一发现,不分车道」 ⇒ objectstack-ai#18844's evidence (the seven-file census) moves onto
objectstack-ai#18314; objectstack-ai#18844 shuts as `duplicate_of` objectstack-ai#18314; ⛔ no second dispatch, no
PR objectstack-ai#18945.
- :368 — not reached: the hit was settled as the same finding, so
neither `Blocked-by:` nor 「先并成一个派发入口」 applies.

Outcome: one card (objectstack-ai#18314, p2, `domain:spec`), one dispatch, one PR. The
old :366 could not reach this: its population was 「各姊妹仓」 only, and its
method was keywords.

## Gates — all at `ea2521bd9` (the final commit; the tree did not move
after these runs)

Derived from the worktree's own changeset with `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (stderr: 「gate list derived from the tree of
'objectstack-ai/objectstack' at commit ea2521b」; change set: 1 path vs
merge base `26c73fb4e`): 20 commands, 20 run, every exit code captured
by redirect-then-`$?`, never through a pipe. `--ran` reconciliation: 「20
derived famil(ies) accounted for — 20 run, 0 NOT-MEASURED, 0 UNRUN」
(exit 0).

| command | exit | verdict line |
|---|---|---|
| `pnpm check:pm-skill-ratchet` | 0 | ✓
`.claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom
0)` · ✓ `references/core-rules.md is 151 lines (ceiling 151; headroom
0)` · widest table row 342 (pin 342) |
| `pnpm check:pm-skill-id-lint` | 0 | ✓ 27 file(s) clean (pattern
`/#[0-9]{3,}/g`) |
| `pnpm check:skill-frame-sync` | 0 | ✓ the one declared copy is
internally coherent; 4 axes; 74 markdown files scanned — four-axis block
:733–:754 md5 `3327d02c56f8a0eca88569dad2270f32` before and after |
| `pnpm check:pm-governed-prose` | 0 | ✓ 2 instruction surface(s) name
all 5 registered governed surfaces and claim no others |
| `pnpm check:nul-bytes` | 0 | OK (8891 text files, no raw ASCII control
bytes); control-byte grep of the edited file: 0 hits |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 | ✓ self-test 58 cases · 22 record-scoped examples across 438 files /
1378 TS blocks clean · 9 TSDoc examples clean · 14 `*When` predicates
clean — after `pnpm --filter "@objectstack/lint..." build` under
`scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, 30 s held; first
attempt without the lint build exited 3 PREREQUISITE NOT MET, which is a
refusal, not a measurement) |
| `pnpm check:agent-test-spelling` | 0 | ✓ 0 violations — 515 files |
| `pnpm check:cross-package-test-inputs` | 0 | OK: 29 package(s) read
outside themselves, all declared; 255 self-test cases |
| `pnpm check:doc-authoring` | 0 | ✓ 821 pinned sites across 231 files
hold the baseline |
| `pnpm check:driver-memory-census` | 0 | OK — every declaration
ledgered, every entry live |
| `pnpm check:pm-expected-skips` | 0 | ✓ self-test 99 cases |
| `pnpm check:pm-governed-merges` | 0 | ✓ 328 assertions |
| `pnpm check:pm-half-states` | 0 | ✓ self-test 4963 cases |
| `pnpm check:refd-timer-probe` | 0 | ✓ 11 cases, negative controls
included |
| `pnpm check:watch-hint-literal` | 0 | ✓ 71 declarations across 4
rostered names |
| `node scripts/check-closing-keyword-parity.mjs` | 0 | OK (3 parsers
agree on all 9 keywords; 5 files carrying the grammar, all registered) |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | ✓ 40
assertions, 5 mutations driven to red |
| `node scripts/check-comment-mask-corpus.mjs` | 0 | ✓ 6857 files, 0
disagree, 0 unparseable |
| `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | ✓
253 cases |
| `node scripts/pm/check-harness-current.mjs --self-test` | 0 | all 26
cases passed |

Builds were run with `OS_SKIP_DTS=1` (the formula gate imports compiled
JS, not declarations); no gate that reads `.d.ts` is in this derivation.
Repo-wide scans (`pnpm lint`) are CI's run, not owed here — this diff
touches no lintable source.

## Line budget

- `SKILL.md`: 812 before / 812 after / ceiling 812 — paid in-band by
rewriting the three rows (:366 75 → 120 B, :367 99 → 115 B, :368 47 →
119 B); the one compression is 「收敛成」 → 「并成」 in the :368 survivor.
- `references/core-rules.md`: 151 / 151 / 151 — untouched, no twin owed.

## Acceptance notes

- `origin/main` moved from `26c73fb4e` (`BASE`) to `873e0e8e2` while
this ran; `git log BASE..origin/main -- .claude/skills/pm-dispatch/` is
empty, and neither PR objectstack-ai#18903 nor PR objectstack-ai#18921 has landed, so no merge of
`origin/main` was performed — the branch is one commit on `BASE` and its
three-dot diff against `main` is exactly the table above. Declared here
rather than done: a merge commit would carry no content for this file.
- noted, not filed: the seat's own half (the claim-time stem scan in
`references/dispatch-runbook.md`) is objectstack-ai#18964 and was not touched; the
runbook carries no spelling of the shadow check to drift against these
rows (`git grep -i shadow` on `references/`: only `core-rules.md:56`).
承接者: objectstack-ai#18964.
- noted, not filed: `check-closing-keyword-parity` reports it skipped
`packages/spec/CHANGELOG.md` (6,080,453 B over its 2 MiB cutoff for
UNREGISTERED files) — the gate's declared, printed behaviour, not a
defect; 承接者:无.
- noted, not filed: `dispatch-gates --ran` accepts an exit code per
recorded line and flags a bare record as 「CLAIMED」; the record here was
re-run in that form (see the report). 承接者:无.

## 维护者速读(草稿)

**改了什么** — 分诊规则里的三行「shadow
检查」被改写为一条真正的去重步骤:定级时先按**文件/机制**、再按关键词,对**本仓与姊妹仓**全部 open
卡(含已派发的)查重;同文件同缺陷视为**同一个发现**,后卡的证据搬到先卡、后卡以 `duplicate_of` 关闭,不再并排派发。行数
812/812 不变,只在原三行内改写,没有折行凑数。

**为什么改** — 同一个文件、同一个缺陷的两张卡,被同一个分诊席在两天里定进两个车道,两边都派了 dev,第二个 PR 作废:浪费了一轮
dev。原规则只说「跨仓 + 关键词」,本仓已有的卡根本不在检查范围里。

**风险与代价(含回滚)** — 纯规则文本,不碰代码与发布包;风险是分诊多做一次按文件名的板面检索(成本很小)。回滚 = revert 这一个
commit,三行恢复原文。

**席位意见** — (留空,席位定稿成评论)

**你要做的** — 一个动作:APPROVE(受管面,需你的批准后由席位落地)。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
objectstack-ai#19117)

Fixes objectstack-ai#18263

Clause-②: no

## The defect, stated as it measures rather than as the title spells it

The card's title says "an entirely empty check-run output". Measured
later on the same card (comment `5705401851`, PR objectstack-ai#18524, run
`104955982460`), the failing `Check Changeset` answers `output.title` =
null with 0-byte `summary` and `text` and carries **`annotations_count`
= 1** — the runner's own generic exit-code annotation (`path .github`,
`level failure`, `title ''`, `message 'Process completed with exit code
1.'`). So the accurate description is **not** "an empty output"; it is
**"one generic annotation that states no cause"**.

That distinction is what makes this fix cheap. The annotation channel is
already open and already carried by this job, and a plain `run:` step
owns exactly one way onto it — the `::error::` workflow command, whose
sibling `::notice` this script has emitted for years. The gate's prose
refusals — among the best in the repo, naming the missing reading,
quoting the offending line and spelling the remedy down to "this red
clears with no push and no re-run" — reach the job log and are then
discarded at the check-run boundary. The reason is produced and thrown
away; the repair is to say it.

## What changed, entirely inside `scripts/check-changeset-no-major.mjs`

1. **Every near miss now renders its REASON.** `readClause2Line` has
always returned `spelling` / `inline-key` / `describing` beside the
offending line; this gate printed "a near miss" and dropped the reason.
It now prints the reason, the line, and the remedy that reason owes —
three different sentences, because `check-clause2-carriers.mjs` says in
its own words that "⛔ The reason changes the sentence, never the state".
2. **Every refusing lane emits exactly one `::error::` annotation**, so
the diagnosis crosses the check-run boundary onto the channel the run
already carries. Greens emit none.

The reason is **interpolated, never matched against a list held here**.
`CLAUSE2_NEAR_MISS_REMEDIES` is a lookup *from* whatever the reader
produced, and its miss path is loud: a reason this file has never been
taught still prints, still names the offending line, and still says
where the reason came from. That direction is deliberate — the reader is
a live surface (PR objectstack-ai#18903 is open on it, +225/-41), and a gate that
matched reasons against a frozen list would answer a new one with
exactly the silence this card is about.

Exactly one annotation per refusal, deliberately: a check run caps
annotations at ten per level, and this script has been past that cap
before (its stock-scoped predecessor emitted 171 `::notice` lines on PRs
that introduced none of them). And the annotation is **not** conditioned
on `GITHUB_ACTIONS` — `render` and `renderLevel` are pure by design,
which is what lets the self-test assert the MESSAGE rather than the exit
code, and an env read inside them would make the one thing this PR adds
the one thing the fixtures cannot see.

### What did NOT change

- **No `.github/workflows/**` file is touched.** The fix lives in the
`.mjs`, as the card asked.
- **Which bodies are ACCEPTED is unchanged.** `CLAUSE2_KEY_LINE` and
`readClause2Line` are not edited,
`scripts/pm/check-clause2-carriers.mjs` is not edited, and no verdict
moves. objectstack-ai#16303 remains open on the accept-set question and no arm of it
is implemented here. This PR makes the existing verdict legible and
nothing else.

## Post-condition 1 — the real predicate drives every row, with a
negative control that can fail

Every row below was produced by importing `readClause2Line` (the same
function the gate calls) and running the real `judgeLevel` /
`renderLevel`. ⛔ No hand-written matcher anywhere.

| body line | `readClause2Line` | exit | reason now in the emitted
annotation |
|---|---|---|---|
| `## Clause-②: no — …` (heading) | `near-miss` / `spelling` | 1 |
`spelling` + remedy |
| `` `Clause-②: no` · `skip-changeset` `` | `near-miss` / `describing` |
1 | `describing` + remedy |
| ``Domain: `domain:devx` · Clause-②: no`` | `near-miss` / `inline-key`
| 1 | `inline-key` + remedy |
| `Clause-②: no` (bare, own line) | `declared` / `no` | 0 | no
annotation — a green annotates nothing |
| NEGATIVE CONTROL `nothing here` | `null` | 1 | "the PR body carries no
`Clause-②:` line" — and no remedy, because there is no line to remedy |

The emitted text, for the heading shape (one line, escaped, abridged):

```
::error title=Check Changeset (level axis)%3A no readable `Clause-②%3A` declaration%2C and it is
the reading this PR needed::… %0A· declaration line: a near miss, not a declaration — reason
`spelling` — ## Clause-②: no — nothing published moves here%0A· remedy for `spelling`: the line
does not carry `Clause-②:` in the fixed spelling at the start of a line. The reader tolerates a
`- `, `* `, `> ` or `**` prefix and NOTHING else, so a markdown HEADING is a near miss and not a
declaration. Write it bare, on a line of its own.%0ADECLARE IT: …
```

## Post-condition 2 — a reason the code has no message for still prints

Driven through the exported
`nearMissReadings('a-reason-this-file-has-never-been-taught', …)`:

```
· declaration line: a near miss, not a declaration — reason
  `a-reason-this-file-has-never-been-taught` — ## Clause-②: no — the line that would otherwise be lost
· remedy for `a-reason-this-file-has-never-been-taught`: this gate carries no remedy sentence for a
  near miss of reason "a-reason-this-file-has-never-been-taught" — `readClause2Line`
  (scripts/pm/check-clause2-carriers.mjs) reports a reason this file has not been taught, and the
  reason plus the line are printed rather than swallowed. The offending line is: … Add the sentence
  for this reason to CLAUSE2_NEAR_MISS_REMEDIES in scripts/check-changeset-no-major.mjs.
```

Control: the same call with a KNOWN reason returns a different sentence,
so the miss path is not silently borrowing a known remedy — a wrong
prescription is worse than a named gap. `null` and `undefined` reasons
also return a sentence; an empty remedy would be this card's defect
moved one function along.

## Post-condition 3 — the two refusals, side by side

| refusal | where it is emitted | annotation |
|---|---|---|
| Clause-② declaration unreadable (near miss, or absent) | this script,
level axis | `::error title=Check Changeset (level axis)%3A no readable
`Clause-②%3A` declaration…` with the reason, the line and the remedy in
the message |
| declared `yes`, no moved package graded `minor`+ | this script, level
axis | `::error title=Check Changeset (level axis)%3A clause-② declares
YES while no moved package is graded `minor` or above…` |
| this PR adds no changeset | `pr-automation.yml`, `Require a changeset`
step, **unchanged by this PR** | `::error::This PR adds no changeset.
FIRST: …` |

A near miss and an absent line share a verdict (`not-measured-material`)
and used to share every byte anyone outside the run could read; they now
differ in the message, which the self-test pins. The missing-changeset
refusal is the workflow's own and already carried an `::error::`; the
self-test now **pins that it keeps one**, because without it that
refusal and this script's are once again one event from outside.

## Post-condition 4 — self-test and battery floor, before and after

| reading | before (`07c6f822e`) | after |
|---|---|---|
| `node scripts/check-changeset-no-major.mjs --self-test` | exit **0**,
299 assertions | exit **0**, 335 assertions |
| `SELF_TEST_BATTERY_FLOOR` (pinned roster size) | **18** | **19** |
| declared batteries | 18 | 19 |
| `'Missing input is a failure, never a pass (objectstack-ai#4690 / objectstack-ai#7006)'` floor |
**5** | **6** |

**Both floor moves are reported rather than absorbed, and neither is a
battery shrinking.**

- The roster grows by one because this PR declares one new battery,
`'objectstack-ai#18263: the refusal says its reason, and says it where the API can
read it'` (35 cases). The floor pin is the roster's own size, so
declaring a battery necessarily moves it; leaving it at 18 would let the
new battery be deleted later with nothing going red. The mechanism was
exercised in the process: the run before the roster entry existed failed
with *"registered 35 case(s) but is not declared in
SELF_TEST_BATTERIES"*.
- The `objectstack-ai#4690 / objectstack-ai#7006` battery goes 5 → 6 because one assertion there was
split into two. The old one was `render(unreadable).stdout.length ===
0`; what objectstack-ai#4690 forbids on stdout is a **tick**, and the `::error::`
annotation is the opposite of one, so the pin is now spelled as what it
always meant — every stdout line must start with `::error `, and there
must be exactly one of them. It is strictly stronger than the line it
replaces, not a relaxation.

## Reverse verification — three ablations, each proving the new battery
can fail

Each leg mutates the committed file, proves the mutation reached disk by
an anchor count, runs the self-test, then restores with `git checkout
HEAD -- …` and proves the restore by `git hash-object` against the HEAD
blob. A `trap … EXIT INT TERM` carries the restore on the crash path.
Predicted direction for all three: RED.

| leg | mutation | on-disk proof | self-test |
|---|---|---|---|
| A | delete the `::error::` annotation from the `not-measured-material`
lane | anchor `title: 'Check Changeset (level axis): no readable` 1 → 0
| **exit 1, 9 failures** |
| B | collapse the three near-miss remedies into one shared sentence |
anchor `Object.prototype.hasOwnProperty.call(CLAUSE2_NEAR_MISS_REMEDIES,
reason)` 1 → 0 | **exit 1, 3 failures** |
| C | make the unknown-reason path return an empty remedy | injected
marker 0 → 1, with the not-deleted control ` return (` held at 1 → 1 |
**exit 1, 2 failures** |

The failures name themselves. Leg A reds nine cases including *"a body
that ALMOST declared and a body that never tried produce different
text"*. Leg B reds *"the three near-miss reasons owe three DIFFERENT
remedies — one shared sentence would pass every assertion above while
reading no reason at all"*. Leg C reds *"a reason of `null` or
`undefined` still returns a sentence — an empty remedy is this card's
defect moved one function along"*.

**Leg C's first attempt was a proven no-op and its reading was
discarded, not retried quietly.** Its anchor was the text the mutation
inserts a copy of, so `grep -c` read 1 before and 1 after and the
harness refused to read a self-test result it could not prove had run.
It was re-run with an injected unique marker (0 → 1) and a control on
the text that must NOT vanish. The first attempt produced no reading at
all; the row above is the second.

Restores are proven, not assumed: each leg ends with `git checkout HEAD
-- …` (never a bare `git checkout --`, which would take the mutation
back out of the index) and then `git hash-object` against the HEAD blob
`af36b25de84a4e55c34ba323c83097c61a3f474c`, plus `git diff HEAD` empty
and the injected marker counted back to 0. All ran in a throwaway
detached worktree off this branch's commit, since the file under test is
the file the ablation mutates; that worktree is removed.

## Scope, changeset and labels

`skip-changeset`, measured rather than assumed. The diff is one file,
`scripts/check-changeset-no-major.mjs`. Resolving every tracked
`package.json` (83 tracked, 70 publishable — not private and carrying a
`files[]`) and asking which `files[]` entry would ship that path:
**zero**. Positive controls through the same resolver:
`packages/spec/dist/index.js` resolves to `@objectstack/spec`'s `dist`
entry and `packages/cli/dist/index.js` to `@objectstack/cli`'s, so the
resolver does find a shipped path when one exists;
`packages/spec/src/index.ts` correctly resolves to nothing. The
repo-root manifest is `private: true` with no `files`. Nothing published
moves, so this takes the label and not an empty changeset (workflow
route 2), and route 0 does not apply — this PR touches no
`.changeset/*.md` at all.

## One measured correction to the card's reproduction table

The card's table gives PR objectstack-ai#18959 as `` `Clause-②: no` `` on its own
line, backtick-wrapped, reading
`{"kind":"near-miss","reason":"describing"}`. Driven against
`origin/main` today, that exact line reads
**`{"kind":"declared","value":"no","arm":null}`** — a declaration, not a
near miss. `clause2LineDescribes`'s QUOTED-AND-CONTINUED tell fires only
when the backtick span opened at the key **continues past its closing
tick**; a span that closes with nothing after it is not describing. The
five PR bodies have all since been corrected, so the historical bytes
are no longer readable through the REST API and the exact line objectstack-ai#18959
carried could not be recovered — most likely it carried trailing content
after the closing tick, which is the `describing` shape and is covered
by the objectstack-ai#18946 row.

This narrows one row of the reproduction table; it moves nothing about
the card. Both near-miss spellings the card names are reproduced above
from the real reader, the third (`inline-key`) with them, and the remedy
is unchanged.

## Out of scope, noted here rather than filed

- `.github/workflows/pr-automation.yml` is untouched. Its `Require a
changeset` step's `::error::` is now **pinned** by this script's
self-test, which is a new coupling in the direction the card wants: it
is what keeps a missing-changeset refusal distinguishable from this
script's.
- The two other gates that run in the same job —
`check-empty-changeset.mjs` and `check-adr-0087-registration.mjs` —
refuse to the job log with no annotation of their own, exactly as this
one did. Same shape, different surface, and out of this card's file
surface.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… size predicate in check-governed-merges --test, the same reading in dispatch-gates, one rule line in SKILL.md and landing-operations (objectstack-ai#19033)

Fixes objectstack-ai#19012
Clause-②: no

## Maintainer ruling (verbatim, 2026-09-18)

> 「还有应该完善skills,修改代码量超过某个行数(比如5000)就应该人工审核。」

Read as: a pull request whose changed line count — GitHub's `additions +
deletions` on the PR, generated files INCLUDED — exceeds 5,000 lands
only by a human merge, at the same terminal as governed text (ACCEPT on
the card, `needs-user-decision` on the PR, a final 维护者速读, review
requested from `GOVERNED_APPROVERS`); no seat flips it ready or arms
auto-merge. 5,000 is the ruled default (「比如」), declared once as
`HUMAN_MERGE_LINE_THRESHOLD` in `scripts/pm/check-governed-merges.mjs`,
so it moves by one word from the maintainer and one edit. The case that
prompted it, PR objectstack-ai#18971 (+238,310 / −119, of which 237,706 lines were
regenerated artefacts), is the first PR the rule governs — an exemption
for generated files would exempt exactly it, so there is none.

## What changed

1. **`scripts/pm/check-governed-merges.mjs` — the SIZE predicate.**
`--pr N` reads `additions` / `deletions` off the same `GET
/repos/OWNER/REPO/pulls/N` that gives `changed_files` (a PR object
missing the pair is a refusal on exit 1 — never a size of zero, never a
"not governed" answer); `--branch REF` counts the same merge-base range
with `git diff --numstat --no-renames` (a binary file is 0 lines, as
GitHub counts it); `--test PATHS` takes `--additions N --deletions N` as
a pair, or prints `size: NOT MEASURED` on stdout naming the modes that
read it. Either limb exits on the GOVERNED code 3, so every caller that
already routes 3 to the human terminal routes an oversized PR there
without a new code; `--json` carries `size` and `humanMerge` (`governed`
stays the path limb). A certified generated-artifact regeneration lifts
the PATH off the register and lifts nothing from the size. The queue
guard's `testVerdict(paths)` reading is unchanged (no size handed in ⇒
the path answer as before).
2. **`scripts/pm/dispatch-gates.mjs` — the same reading at dispatch
time.** With no paths (the derived run) it prints `Changed lines — N (+a
/ -d; generated files INCLUDED) vs the human-merge threshold 5000:
under` or `⛔ OVER — this PR lands only by a HUMAN MERGE …` beside the
tier verdict (human and `--tier` modes), the count on stderr with the
rest of the provenance, and `changedLines` in `--json`. The count is
`--numstat` off the merge base against the working tree plus untracked
files counted from disk (under-derivation refused, like the path list).
An explicit path list carries no diff and prints `NOT MEASURED`, never a
silent under. The threshold is imported from the gate — one declaration,
no second copy.
3. **Rule text.** `.claude/skills/pm-dispatch/SKILL.md` gains one line
beside the four-piece-terminal trigger (line 608, 111 B): 「改动 >5000
行(含生成物)同换终局四件套,⛔ 无事实层例外;读数 = PR additions+deletions。」
`references/landing-operations.md` line 26 folds the size limb into the
pre-check row, now spelled `--pr N` (which reads paths and size in one
call), 117 B, ceiling unchanged at 69. The SKILL.md ceiling rises 812 →
813 in `scripts/pm/check-skill-line-ratchet.mjs` under the ratchet's own
maintainer exit, the ruling quoted in the entry (the 811 → 812
precedent's form).

## Readings — before / after, measured

| reading | before (`43f476688`) | after (this head) |
|---|---|---|
| `check-governed-merges.mjs --pr 18971` (live API through the proxy) |
exit 0 — `✅ NOT governed — ordinary queue landing applies` | exit 3 — `⛔
HUMAN MERGE — 238429 changed line(s) (+238310 / -119) > 5000` |
| `--pr 18994` (2 files, +15 / −1) | exit 0 | exit 0 — `size: 16 changed
line(s) (+15 / -1) ≤ 5000 — under the human-merge threshold` |
| `--pr 18921` (SKILL.md, +6 / −6) | exit 3 GOVERNED | exit 3 GOVERNED,
plus `size: 12 changed line(s) … under` |
| `check-governed-merges.mjs --self-test` | 328 assertions, 26 batteries
| 369 assertions, 27 batteries (new battery: the SIZE predicate, floor
30) |
| `dispatch-gates.mjs --tier` (no paths, this worktree) | no size line |
`Changed lines — 722 (+691 / -31; generated files INCLUDED) vs the
human-merge threshold 5000: under.` |
| `dispatch-gates.mjs --tier packages/spec/src/index.ts` | no size line
| `Changed lines — NOT MEASURED: a path list carries no diff to count …`
|
| `check:pm-dispatch-gates` (detached, `tail --pid`) | 1849 cases (the
dispatch's reading at `43f476688`) | 1862 cases pass (795.6 s, detached;
+13 cases) |
| `check:pm-skill-ratchet` | SKILL.md 812 / 812 · landing-operations.md
69 / 69 | SKILL.md 813 / 813 · landing-operations.md 69 / 69 |

Self-test pins on the threshold: exactly 5,000 changed lines is under;
5,001 is over; the +238,310 / −119 pair reads 238,429 and is over; a
certified pure regeneration over the threshold still lands by a human
merge; the verdict is byte-identical through `--branch` and through
`--test` once the same list and numbers are handed in.

## Line budget (measured)

- `SKILL.md`: 812 → 813 lines; ceiling 812 → 813 (maintainer exit). A
fold was not available: 0 of 598 adjacent bullet pairs merge under the
120-byte cap (smallest 123 B); the trigger line (607) stands at 118 B;
the rule's shortest self-contained form is 111 B; deleting a ruled
clause is refused on the state-machine precedent.
- `references/landing-operations.md`: 69 → 69 lines (line 26: 118 B →
117 B).
- `check:pm-skill-id-lint`: 27 files clean (no issue-ID citation in
either line).

## Gates (this head; exit codes captured before any pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree at `7fdd61ca0` (42
commands; change set 5 paths, 724 changed lines by its own reading),
every one run with `cmd > log 2>&1; status=$?` and reconciled with
`--ran`:

```text
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/check-harness-current.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:declared-population-live :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-expected-skips :: exit 0
pnpm check:pm-governed-prose :: exit 0
pnpm check:pm-half-states :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:pm-skill-ratchet :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
```

`dispatch-gates --ran`: **42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN**
(verdict line: `✓ dispatch-gates --ran: 42 derived famil(ies) accounted
for — 42 run, 0 NOT-MEASURED`). `check:pm-dispatch-gates` ran detached
(`nohup` + `tail --pid`, 795.6 s on this box): `✓ dispatch-gates
self-test: 1862 cases pass.` `check:pm-governed-merges`: `✓
check-governed-merges --self-test: 369 assertions`.
`check:doc-formula-expressions` exited 3 (PREREQUISITE NOT MET:
`@objectstack/formula` / `@objectstack/lint` not built) on the first
pass; both were built under `scripts/pm/os-verify-lock.sh` (VERDICT
command-exit 0, 152 s held) and the gate reran green — the exit 3 was
never a measurement.

NOT MEASURED locally, by the derivation itself (CI-only, value-bearing
argv): `scripts/check-shard-attestation.mjs --emit …`,
`scripts/check-test-completeness.mjs …`,
`scripts/pm/check-half-states.mjs --format=markdown --provenance=…`;
plus the 11 wide-population families and the 50 artifact-roster families
CI runs on every PR, outside the derived total by design. `pnpm lint`
(repo-wide eslint) is CI-owned and was not run here. No package
build/test is owed: the diff touches no `packages/**` file (no ①/② in
the local verification scope), so the only lock-wrapped run was the
formula/lint build above.

Line-budget after the final commit (`7fdd61ca0`):
`check:pm-skill-ratchet` — `.claude/skills/pm-dispatch/SKILL.md is 813
lines (ceiling 813; headroom 0)`, `references/landing-operations.md is
69 lines (ceiling 69; headroom 0)`; `check:pm-skill-id-lint` — 27
file(s) clean.

## Deviations from the dispatch brief

1. Mechanism assumption 1 said a failing size read exits PREREQUISITE
NOT MET (3). Under `--pr`, 3 already means GOVERNED — the file's own
rule is that no invocation carries both meanings — so a PR object
without the pair is a REFUSAL on the derivation code 1 (a stated
refusal, never 0, never a size of zero). The ruling's intent (never read
as "not governed") is kept.
2. "812 / 812 — fold or pay": measured, neither was available (above),
so the SKILL.md line lands under the ratchet's own maintainer exit (812
→ 813), the form the 811 → 812 entry took. The hunk sits at :608,
disjoint from PR objectstack-ai#18903's bands (:509–:525, :633–:675) and from the two
PRs that landed on SKILL.md meanwhile (merged into this branch; the line
is still there once). If the seat prefers the follow-up route, drop
commit 3's SKILL.md hunk and the ratchet entry together.
3. `--branch` derives the size itself (`--numstat` on the range it
lists) rather than taking passed-in numbers; the flags beside a deriving
mode (`--pr`, `--branch`) are refused as two readings of one number, the
way two mode flags are.
4. `dispatch-gates.mjs`'s self-test pins a NAMED census of live
population markers by file and line; the import block moved this file's
own `inherited-population` marker from :702 to :705, so that one row is
updated — the census exists to be updated exactly this way.

## Acceptance notes

- to file (class b — a declared contract the queue cannot yet hold): the
queue guard's `merge_group` leg reads the PATH register only; a seat
that skips the landing pre-check can still enqueue an oversized PR.
Dedupe words: `queue guard size threshold`, `merge_group additions
deletions`, `check-governed-queue-guard 5000`, `human merge line count`.
- to file (class b): AGENTS.md §7 lists "two classes of PR never enter
this path on green alone" (governed surface; Version Packages) — the
ruled third class is missing from the rules layer. Dedupe words:
`AGENTS.md green alone third class`, `5000 lines human merge AGENTS`.
- noted, not filed: the post-merge sweep (default mode of
`check-governed-merges.mjs`) lists governed-surface merges only; an
oversized PR that landed through the queue is not listed. 承接者: the
skills seat, together with the queue-guard follow-up above.
- noted, not filed: `check:doc-formula-expressions` exits 3
(PREREQUISITE NOT MET) on a fresh worktree until `@objectstack/formula`
and `@objectstack/lint` are built — by design of that gate; built under
the verify lock here and rerun. 承接者: none.

## 维护者速读(草稿)

**改了什么**:落地前检 `check-governed-merges.mjs` 新增「体量」判据:PR 的 additions +
deletions 超过 5000 行(含生成物)⇒ 只能人合,与受管面走同一终点;`dispatch-gates`
在派发/认领时就把同一读数印在 tier 行旁;SKILL.md 与 landing-operations.md
各落一行规则。阈值只声明一次(`HUMAN_MERGE_LINE_THRESHOLD = 5000`),改它是一个词。

**为什么改**:您 2026-09-18 的裁决。触发案例是 PR objectstack-ai#18971(+238,310 / −119,其中 237,706
行是生成物)只凭 AI 审查就经队列合入;生成物不豁免,否则恰好豁免它。

**风险与代价(含回滚)**:大 PR 的落地从「席位挂 auto-merge」变成「等您点一下」,每张超 5000 行的 PR
多一次人工动作;回滚 = revert 本 PR(纯脚本 + 两行规则文本,无发布物)。已知缺口:队列守卫的 merge_group
腿尚未读体量,眼下靠席位跑落地前检;已列为后续单。

**席位意见**:(留空)

**你要做的**:确认 5000 这个默认值(「比如」)是否就是您要的;是 ⇒ 人合本 PR;要改数字 ⇒ 说一个数即可。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land?

2 participants