Skip to content

skills(pm-dispatch): a passing control certifies the instrument, not the question — a zero-hit reading names the instrument's reach and one known target outside it - #18921

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-18755-control-certifies-instrument
Sep 18, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-18755-control-certifies-instrument

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #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 #18903 (card #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

…the question — a zero-hit reading names the instrument's reach and one known target outside it

The reading discipline caught a dead instrument (control words read zero) but
not a live instrument pointed at a question its domain cannot see: a content
grep with a lit control and a 0 absent control still cannot see a FILE NAME,
and cannot tell a DECLARATION from a MENTION. Two rows in the 平台读数纪律 band
of SKILL.md state the discriminator (reach written by shape, one known target
outside the reach, an out-of-reach zero is not a reading), paid in-band by
folding the landing-operations pointer into the rule it serves and by deleting
the filer/dedupe summary row whose three clauses already live on neighbours;
one twin row in core-rules, paid by compressing the two intro prose lines.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
@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 the documentation Improvements or additions to documentation label Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6450bccb6b8013d0b7d72305d3f8f79af4d8e388

① Derived judgments

  • Surface: .claude/skills/pm-dispatch/SKILL.md (+4 / −4, 812 / 812) and references/core-rules.md (+2 / −2, 151 / 151); two commits (the change, then one origin/main merge at 625db0e85). No published package moves; no contract's accept/reject set moves; the public surface is unchanged. check-governed-merges.mjs --test on the two: GOVERNED, .claude/** ×2 — rules layer.
  • The discriminator (the card's triage row 「控制证明的是仪器,不是问题」) lands as two rows directly under the 双零 rule — SKILL.md :165 「控制通过 ≠ 问题问对:零命中须写仪器可达半径与一个必在半径外的已知目标。」 (107 B) and :166 「半径按形态写(内容/文件名/声明/字段);所求在半径外 ⇒ 零非读数,换看得见它的仪器。」 (116 B) — and as one twin row in core-rules :45 「控制通过 ≠ 问题问对:零命中须写仪器可达半径与半径外已知目标,半径外的零非读数。」 (117 B) under its 零命中/双零 row. Placement correct (the band the card names, adjacent to the rule it extends); the card's two measured incidents (a content grep asked for a FILE NAME; a mention grep asked for a DECLARATION) are both caught by :166's shape list, which is what makes the reader test mechanical. The dispatch's one-row hypothesis holds on bytes (a 117-B single row exists) and fails on the deliverable — the single row drops the shape list and the switch-instrument remedy; two rows, both paid, is the right call.
  • Payment — each retirement read for survivors on the head blob, not by the dev's word: (a) the :175 pointer 「main-red 的跳队例外与事故锚卡约定见 references/landing-operations.md B 节。」 folds into the rule it serves, now :176 「advisory 门禁红着进 main 是共享损伤,任何车道发现都立即止血并立单,见 landing-operations B。」 (118 B): the two conventions it named (the main-red queue-jump exception, the incident anchor card) are untouched in landing-operations B and the sentence that triggers them still points there, in the same short unbackticked form :648 / :655 / :665 / :666 already use; the cost is that 跳队 / 事故锚 no longer appear in SKILL.md (a grep for them reads zero there) — a discoverability cost, no rule lost. (b) the :178 row 「立卡者不查重,只附 3–5 个查重词;分诊按词查自有列表,零命中须控制词背书。」: the filer clause is re-packed onto :180 「立卡者不查重、只附 3–5 查重词;真撞上重复,先比数值与作用域再决定关哪个。」 (107 B); 分诊按词查自有列表 survives at :223 (分诊座位唯一生产 … 查重), :315 (查重缓存住席内 — the list itself) and :366 (跨仓查重); 零命中须控制词背书 IS :163 — nothing left without a home. (c) core-rules :2–:3, two lines of intro prose, compressed to one (114 B): no rule row touched; the dropped words (有约束力的 / 供人工复核 / 参数、事实表与操作配方) are the title's or carried by 细节. core-rules ⊆ SKILL.md holds: the twin row compresses :165–:166 and adds nothing.
  • Serial: the SKILL.md hunks sit at :162–:183, PR skills(pm-dispatch): key the clause-② contract review by lane — spec and skills owe it on every round, other lanes owe none #18903's at :509–:670; core-rules :1–:48 against skills(pm-dispatch): key the clause-② contract review by lane — spec and skills owe it on every round, other lanes owe none #18903's :109 — disjoint, as the dispatch required; whichever lands second merges origin/main before its enqueue.
  • Machine side: none. Seat spot-check on a scratch worktree at this head: check:pm-skill-ratchet exit 0 (812 / 151, both at ceiling), check:pm-skill-id-lint exit 0, check:pm-governed-prose exit 0 (2 surfaces name all 5 registered), check:skill-frame-sync exit 0; lines over 120 bytes 23 / 0, unchanged from origin/main.

② Semver level

  • skip-changeset is correct: .claude/** ships in no package's files[]. Clause-②: no on the claim (5725683048) and in the body; PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 18921 at 2026-09-18T06:16Z: exit 0, one live claim, pr-body.clause2-line DECLARED no, no widening tell.

③ Boundary flags

  • Dev flags: no open questions; two noted, not filed items (platform-readings :283 already holds the declaration-shaped regex; check:skill-frame-freshness not derived for this diff, run as an extra, green) — agreed, nothing owed. Deviations accepted: two rows instead of one (above); core-rules :45 not widened in place (117 / 120 B, no room) — the twin is a new row paid by the intro compression, one line outside the band, on no in-flight hunk; the two neighbour folds (:174 → :176, :180); one origin/main merge before the gate run (the four later main commits touch check-half-states.mjs only — the queue rebuild validates it); commit trailers in the repo's model-free form. Gates: 20 derived / 20 run / 0 NOT-MEASURED / 0 UNRUN at this head (check:doc-formula-expressions exit 3 first as PREREQUISITE NOT MET, then 0 after the build under the verify lock); 0 MCP calls; 4 REST writes.
  • CI on this head at 2026-09-18T06:16Z: 13 success · 12 skipped · 4 in_progress (three Type Check lanes, Lint & Repo Gates) — nothing red; the landing act reads it again.

Implemented-by: claude/issue-18755-control-certifies-instrument
Reviewed-by: session_01BTeBejoPUvRHN8WdAJC6oF

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

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

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

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

风险与代价(含回滚):两文件都在棘轮上限,新增行靠删本文件里已在别处说过的内容付账,席位逐行核过幸存:① SKILL.md 那行纯指针(main-red 的跳队例外与事故锚卡见 landing-operations B)折进它服务的止血规则,两个约定原文不动、触发它们的那句仍指向 B,代价只是「跳队 / 事故锚」两个词在 SKILL.md 里搜不到了;② 一行三句皆有重复的汇总行退掉,「立卡者附查重词」挪到相邻查重规则,「分诊按词查自有列表」「零命中须控制词背书」各在别处仍在;③ 核心条款两行说明文字压成一行,无规则降级。门禁全绿。与在等你批准的 PR #18903 同文件不同行段,谁后落谁先合 main。回滚 = revert 一个提交。

席位意见:建议批准。派发时我假设一行能装下,dev 实测一行装得下字节、装不下「形态清单」和「换仪器」两个动作,两行更对——形态清单正是让读者测试可机械化的部分。

你要做的:确认这条判别式该进 PM 读数纪律,是则 APPROVE(GOVERNED_APPROVERS 账户);之后由本席按裁决 C 落地。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 18, 2026 11:23
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Landing record — ruling C, on an authorized APPROVED (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T11:25Z

os-zhuang (GOVERNED_APPROVERS) approved this PR (review 5247217859 at 2026-09-18T11:23Z), marked it ready at 2026-09-18T11:23Z and added it to the merge queue at 2026-09-18T11:23Z — the two CCR calls this seat's channel could not make, made by the approver's own hand. The seat's pre-landing reads on head 6450bccb6b: the review of record 5726029614 names this head; PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 18921 exit 0 (card #18755); checks at 2026-09-18T11:25Z: 19 success · 16 skipped, none red. Parallel draft beside PR #18903 on SKILL.md and core-rules with disjoint hunks — PR #18903 (still draft, unreviewed) merges `origin/main` before it lands; the SKILL.md 平台读数纪律 band this PR rewrites lifts the serial on #18958 once the merge lands. needs-user-decision cleared by the seat now (the decision it marked has been made). The merge is the queue's; the card closes on it, and the seat clears its pm:dispatched / assignee residue after the merge lands.


Generated by Claude Code

Merged via the queue into main with commit 37ca54a Sep 18, 2026
39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-18755-control-certifies-instrument branch September 18, 2026 11:46
This was referenced Sep 18, 2026
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
… make a transport failure speak (objectstack-ai#18998)

Fixes objectstack-ai#18947

Clause-②: no

`scripts/check-closing-target-claim.mjs` read GitHub through node's
global `fetch`
with no proxy route. In an agent container api.github.com is reachable
only through
the session proxy — which is also where the credential is injected — and
`fetch`
does not read the proxy variables, so every request arrived
uncredentialled and
GitHub answered 401. This gate folded that 401 into a per-target
`UNDETERMINED` at
**exit 0**: honest in prose, green in the one thing a derived-gate sweep
records.

Two commits, in the order the triage asks for: the route, then the
negative control.

## Before / after / control — this container, transcripts

Exit codes captured with redirect-then-`$?`; no token is printed by any
leg.

⚠️ One substitution, declared: where a probe's `PR_BODY` bound
**objectstack-ai#18844**, the closing
keyword is written `CLOSING` below. The real command used the ordinary
spelling; it is
masked here so that quoting a probe does not make this PR body bind a
second card. The
keyword next to objectstack-ai#18947 is left literal — that is this PR's own target,
already bound on
line 1.

**BEFORE** (`d8b12fca9`, the seat's own probe re-run at this base):

```text
$ GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=18921 \
    PR_HEAD_REF=claude/issue-18844-probe PR_BODY='CLOSING objectstack-ai#18844' \
    node scripts/check-closing-target-claim.mjs
::warning::UNDETERMINED - objectstack-ai#18844 was not judged: its comment thread could not be read. ...
✓ check:closing-target-claim: PR objectstack-ai#18921 closes no card this run could hold against a claim; ...
  1 closing target(s) could not be judged - see the UNDETERMINED warning(s) above.
exit=0
```

**AFTER**, same invocation — the re-exec line, then a real per-target
verdict:

```text
ℹ️  re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:37101) and node's fetch does not read it.
✓ check:closing-target-claim: PR objectstack-ai#18921 closes no card this run could hold against a claim; ...
  objectstack-ai#18844 was not judged: the card is already closed, so the merge closes nothing.
exit=0
```

⭐ The reading, reported and not steered: objectstack-ai#18844 resolves to
`TARGET_CLOSED`, not to a
finding. The classifying read was really spent (it is the second read,
taken only on
the path about to go red), and it answered `state != open`. The
declined-number policy
this file already documents is what declines it — the merge closes
nothing, so there is
no second implementation to prevent.

**AFTER, live acceptance pair** against the real board, which is the
stronger evidence
that the read reaches the thread at all — this card, this branch, and
this card with a
branch nobody claimed:

```text
$ ... PR_HEAD_REF=claude/issue-18947-closing-target-claim-proxy-route PR_BODY='Closes objectstack-ai#18947'
✓ check:closing-target-claim: PR objectstack-ai#18921 closes objectstack-ai#18947, and each carries a `Claim:` whose
  `Branch:` line names `claude/issue-18947-closing-target-claim-proxy-route`.
exit=0

$ ... PR_HEAD_REF=claude/issue-18947-someone-else PR_BODY='Closes objectstack-ai#18947'
::error::PR objectstack-ai#18921 closes objectstack-ai#18947 with no `Claim:` on the card naming `claude/issue-18947-someone-else`
  - `Closes objectstack-ai#18947` - the governing `Claim:` on objectstack-ai#18947 names
    `claude/issue-18947-closing-target-claim-proxy-route`, not `claude/issue-18947-someone-else`.
exit=1
```

**CONTROL A — the negative control the triage requires**: same command,
`HTTPS_PROXY` /
`https_proxy` unset, so the request really fails at the transport:

```text
❌ check:closing-target-claim: PREREQUISITE NOT MET - the board was not read.
   GitHub API 401 for /repos/objectstack-ai/issues/18844/comments?per_page=100&page=1 - ⛔ not a verdict.
   ⛔ NOTHING was judged. ...
exit=3
```

⛔ No longer exit 0. 「修好之后,一次真实的传输失败必须能让门禁出声」 — it speaks.

**CONTROL B — the network-error leg, live**:
`HTTPS_PROXY=http://127.0.0.1:1` (a dead
port). This one also proves the hand-off is invisible in the exit codes:
the parent
re-execs, the child refuses, and 3 comes back verbatim.

```text
ℹ️  re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:1) ...
❌ check:closing-target-claim: PREREQUISITE NOT MET - the board was not read.
   GitHub API unreachable for /repos/.../issues/18844/comments?...: ECONNREFUSED - ⛔ not a verdict.
exit=3
```

## The boundary between transport and truncation, and the line that
draws it

The card's own docblock said a transport failure is 「declared
UNDETERMINED rather than
an unhandled rejection」. That sentence is what this card turns around,
for the
whole-board case only. Where the catch sat: three sites inside `collect`
— `apiOrNull`
(the queue leg's PR read and the classifying issue read) and the `try`
around the
comment-thread walk. All three swallowed everything.

The line that draws the new boundary is a single predicate, read at all
three sites:

```js
export function isBoardNotRead(error) {
  if (!(error instanceof GitHubApiError)) return false;
  if (error.networkError) return true;
  return error.status !== 404;
}
```

- **Typed, never a message match.** `githubApi` now throws
`GitHubApiError` carrying the
HTTP status, or `networkError: true` when `fetch` itself rejected. A
plain `Error` from
anywhere else — a fake transport in the self-test, a bug — is **not** a
transport
failure and keeps its old per-target reading, so no untyped throw can
promote itself to
a whole-board refusal. Every pre-existing self-test case that threw `new
Error('boom')`
  is green unchanged.
- **404 stays per-target.** It is the one status carrying information
about the resource
the path *names*: that number does not resolve for this token. So does a
thread walked
past `MAX_COMMENT_PAGES`. Both remain `exit 0` +
`::warning::UNDETERMINED` — 「never
  clean, never an accusation」 — and both are pinned.
- **Everything else is about the run.** 401/403 (credential or
permission), 429 (rate
limit), 5xx (GitHub), or no connection at all. Those propagate **out
of** `collect`, so
nothing at all is judged, and the dispatch turns them into
`boardNotReadRefusal(error)`
  — pure, `{ exit, lines }`, driven offline by the self-test.
- **429 is a declared superset** of the set named on the card
(401/403/5xx/network). It
is the same class — the board was not read — and routing it to
UNDETERMINED would be
exactly today's silence under a different number. Called out here rather
than folded in
silently. The card's claim comment also named 404 in a parenthetical;
the ruling section
names 401/403/5xx/network *and* explicitly keeps 「a target whose number
cannot be
read」 as UNDETERMINED, so 404 is per-target. That tension is resolved in
the ruling's
  direction, and stated rather than discovered.
- **⛔ No `::error::` annotation on the refusal path**, matching NOT
MEASURED: the exit
code is what makes the job red, and an annotation would name an author
who caused none
  of it.
- **Only a transport failure is refused.** The dispatch re-checks
`isBoardNotRead` and
re-throws anything else, so a genuine bug in this gate stays an
unhandled rejection
where it is visible as one, instead of being filed under "GitHub was
unreachable".
- **Residual, stated in the header rather than discovered later**: a
token that can see
nothing in this repository gets 404 on every path, so every target reads
UNDETERMINED
at exit 0. That is the same shape as a board of numbers that do not
resolve and no
status distinguishes them. What this gate *can* tell apart is what it
does tell apart.

## The route

The shape `main` carries today, not PR objectstack-ai#18980's unlanded `guard`
parameter: the shared
`proxyRearmPlan` is imported, `proxyPlanEnv()` folds this file's own
guard name
(`OS_CLOSING_TARGET_CLAIM_PROXY_REARMED`) onto the shared name, and
`rearmThroughProxy()`
is called **once**, only by a wired run about to read the API — never on
`--self-test`,
never on a NOT WIRED run. A sibling instrument's guard must **not**
suppress this one's,
which is pinned. On an Actions runner there is no proxy in the
environment, the plan
never re-arms, and nothing about this gate changes there.

## Exit register — before / after

| code | before | after |
|---|---|---|
| 0 | judged, clean | unchanged |
| 1 | judged, finding | unchanged |
| 2 | NOT MEASURED — no usable context; "a usage, wiring **or
transport** failure" | NOT MEASURED — no usable context; "a usage or
wiring failure" |
| 3 | — | **PREREQUISITE NOT MET** — a request failed at the transport,
the board was not read, nothing judged |

3 is `EXIT_PREREQUISITE_NOT_MET`, imported from the half-state module
exactly as
`check-issue-citations.mjs` spells it, ⛔ not re-declared here.

**The CI consumer needs no change — measured, not assumed.**
`.github/workflows/closing-target-claim-guard.yml` invokes the script
with a bare
`run: node scripts/check-closing-target-claim.mjs`: no
`continue-on-error`, no `|| true`,
no `if:` on the step. A non-zero exit fails the step and reds the job,
so exit 3 is a red
job with no workflow edit. That exit is also unreachable on a runner for
a routing
reason: there is no proxy there, and the token is the job's own.

## Self-test

| | before | after |
|---|---|---|
| cases | 80 | 105 |
| batteries | 10 | 12 |
| `SELF_TEST_BATTERY_FLOOR` | 10 | 12 |

The floor rose; no existing battery's pin moved. Two batteries added —
the routing
decision (9 cases, offline, pinned exactly as the landed mapper pins it:
set ⇒ route,
absent ⇒ none, flag present ⇒ none, own guard set ⇒ none, the patrol's
guard set ⇒ still
routes, plus the two structural pins that the plan is imported and the
hand-off is called
once *before* the first network read) and the transport boundary (16
cases: each refused
status, the 404 and untyped-throw exclusions, the fleet phrase, the
quoted error, `⛔ not
a verdict`, exit 3 being none of 0/1/2, no `::error::`, no `✓`, the
route named when a
proxy is configured, and both UNDETERMINED legs still at exit 0).

## Reader test

> A derived-gate run in a container reads a real per-target verdict from
this gate; with
> no proxy it reads PREREQUISITE NOT MET and exit 3, never a ✓ with a
warning.

Held, by CONTROL A above and by the live acceptance pair.

## Changeset

`skip-changeset`, measured rather than assumed. Every published package
in this repo
declares `files[]`, and every entry resolves inside that package's own
directory
(`dist`, `README.md`, `CHANGELOG.md`; `@objectstack/spec` adds generated
dirs and
`src/**/*.zod.ts`). `scripts/check-closing-target-claim.mjs` is at the
repo root, under
no `package.json` at all (there is none anywhere under `scripts/`), so
it is inside no
publishable package directory and no `files[]` entry can name it. The
root manifest is
`private: true`. Positive control: a file that *is* published —
`packages/spec/src/data/object.zod.ts` — sits inside a published package
directory and is
matched by that package's `src/**/*.zod.ts` entry. Nothing published
moves.

## Gates

Derived from this worktree with
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` — 29
commands, the derivation's own first line naming this repo and commit.
Every one was
run, exit code captured before any pipe, and the record reconciled back
with `--ran` in
the `COMMAND :: exit CODE` form the reconciler asks for:

```text
✓ dispatch-gates --ran: 29 derived famil(ies) accounted for — 29 run, 0 NOT-MEASURED
  (a DERIVED zero — all 29 recorded an exit code and none of them is 3).
```

**All 29 exit 0**, including this gate's own `pnpm
check:closing-target-claim` (105 cases)
and `pnpm check:pm-dispatch-gates` (1849 cases, 756.6s on this box, run
detached behind an
exit-capturing wrapper and waited on with `tail --pid`). `origin/main`
moved during the
run (`f347c793e`); the derivation was re-run after a fetch and the
29-command set is
byte-identical.

**Repo-wide lint, not a narrowing.** `eslint . --no-inline-config` was
run in full at
`c1e174eba`, after the final commit: **6858 files linted** (eslint's own
count from
`--format json`), **0 files with problems**, exit 0. So no narrowing
needs declaring here.

## Local verification scope

- (1) No package is touched, so the dependency closure is empty and
there is no build leg.
- (2) The changed file is a repo-root CI instrument in no package; its
own test surface is
  its `--self-test`, run above.
- (5) The diff edits a gate script, so that script's own suite is owed
on top of the
derived families — here the two are the same thing: `pnpm
check:closing-target-claim`
*is* `node scripts/check-closing-target-claim.mjs --self-test`, and it
is in the derived
  list.
- `pnpm lint` is a repo-scan CI owns; it fitted in the foreground here
and was run whole
  (above), so there is nothing narrowed to declare.

## Files changed

- `scripts/check-closing-target-claim.mjs` (only)

---
_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
…t actually emits (objectstack-ai#19074)

Fixes objectstack-ai#19057

Clause-②: yes

Maintainer decision batch objectstack-ai#166 item 1 — 「同意」 to option **C** on
objectui#9547: the false sentence is in the protocol, so the protocol is
corrected first and the sibling's four declaration faces align after.

`packages/spec/src/ui/react-blocks.ts:321` declared the ListView
`onNavigate` second argument as `'view' | 'edit'`. `'edit'` is emitted
by no call site in the reference implementation and read by no branch;
`'new_window'` — what a Cmd/Ctrl- or middle-click, and an authored
`navigation: { mode: 'new_window' }`, actually send — was not declared
at all. An author reading this contract wrote a handler with one dead
arm and one missing arm.

## The change — one source line

`packages/spec/src/ui/react-blocks.ts:321`, before:

```ts
{ name: 'onNavigate', type: "(recordId, action: 'view' | 'edit') => void", kind: 'callback', description: 'Called for page-level navigation.' },
```

after:

```ts
{ name: 'onNavigate', type: "(recordId, action: 'view' | 'new_window') => void", kind: 'callback', description: "Called for page-level navigation. The second argument is a navigation-MODE token from a CLOSED vocabulary — 'view' opens the record page, 'new_window' opens it in a new browser tab; the reference implementation emits no other value and reads no other branch." },
```

The description now names both modes and states that the slot is a
closed vocabulary. The closedness itself is not new: the protocol's own
retirement note for `view.list.navigation.view`
(`packages/spec/src/ui/view.zod.ts:1818`, removed in 17.5.0) already
records that anything outside the mode vocabulary "matched no branch".
What moves here is the MEMBERSHIP of that vocabulary, not its closedness
— and `view.zod.ts` is read-only on this card, untouched.

## The generated half — zero hand edits

`skills/objectstack-ui/references/react-blocks.md` is the `OUT_MD` of
`packages/spec/scripts/build-react-blocks-contract.ts`, and it is a
PD-14 governed path whose queue-guard generated-artifact exception
(`scripts/pm/check-governed-merges.mjs`, register row
`spec-react-blocks`) is recomputed byte-exactly at merge-group time. It
was produced here by running the generator and nothing else.

`skills/objectstack-ui/references/react-blocks.md:67`, before:

| `onNavigate` | `(recordId, action: 'view' \| 'edit') => void` |
callback | | Called for page-level navigation. |

after:

| `onNavigate` | `(recordId, action: 'view' \| 'new_window') => void` |
callback | | Called for page-level navigation. The second argument is a
navigation-MODE token from a CLOSED vocabulary — 'view' opens the record
page, 'new_window' opens it in a new browser tab; the reference
implementation emits no other value and reads no other branch. |

Idempotence, run after the generator: `pnpm --filter @objectstack/spec
check:react-blocks` exits 0 with "1 generated files in sync with
packages/spec", and `git status --porcelain` is unchanged by that run.
Post-merge, `pnpm --filter @objectstack/spec check:generated` reports
all 16 generated artifacts up to date, `check:react-blocks` among them,
and leaves a clean tree.

## Readings

Every zero below carries its instrument's reachable radius and one known
target necessarily outside it (charter rule, PR objectstack-ai#18921).

**1. The retired literal, repo-wide — 0 in code or generated output.**
`git grep -F` for the old spelling at `92a80ed740` returns exactly one
line, and it is this PR's own changeset prose quoting the removed
declaration; zero in source or in any generated artifact.
Radius: the CONTENTS OF TRACKED FILES of `objectstack-ai/objectstack`
only — excludes `node_modules`, excludes untracked files, excludes the
sibling repository.
Known target outside that radius: objectui's four declaration faces
(`ObjectGridComponentProps.onNavigate`, `ObjectGridSchema.onNavigate`,
`ListViewRuntimeProps.onNavigate`,
`UseNavigationOverlayOptions.onNavigate`) — another repository,
structurally unreadable by this instrument; they move under
objectui#9547 with its bump to `@objectstack/spec` >= 17.5.0.
Firing control, same instrument, same tree: `onNavigate` matches 18
lines across 12 files.

**2. DARK — `'edit'` is gone from the declaration line: 0.**
`sed -n '321p' packages/spec/src/ui/react-blocks.ts | grep -c "'edit'"`
prints `0`.
Radius: one line of one file, nothing else.
Known target outside that radius:
`packages/spec/src/ui/view.zod.ts:1818`, whose retirement note still
quotes `edit` verbatim and is deliberately untouched — this instrument
cannot see it.
Firing control, same instrument, same line: `grep -c "'view'"` prints
`1`.

**3. Hand edits in the governed generated file: 0.**
The generator was run, then re-run in `--check` mode, which compares its
own emitted bytes against what is on disk; it exits 0.
Radius: exactly the one path `OUT_MD` names. It says nothing about any
other file under `skills/`.
Known target outside that radius: `skills/objectstack-ui/SKILL.md` —
this generator does not emit it and this comparison never reads it. (It
is unmodified by this PR; that is a separate statement, from `git diff
--name-only`, which lists three paths in all.)
Firing control: the same file's `git diff` against the branch point is
non-empty on exactly one line, so the byte comparison is demonstrably
capable of seeing a difference.

**4. Published-skill-surface budget.** The changed file is
GENERATOR-OWNED, so it is measured and not ratcheted —
`check-skills-token-ratchet` classifies it that way by name. Whole-file:
115 lines before, 115 lines after (+0); 12504 bytes before, 12737 after
(+233); 3126 tokens before, 3185 after (+59). Whole bundle: 140038
tokens before, 140097 after (+59), of which the authored/ratcheted half
is unchanged at 129392 against a ceiling of 145656. Both ratchets exit
0. The "before" figures are taken at `2767af8e83`, whose `skills/` tree
is byte-identical to this branch's merge base (`git diff --stat
2767af8 d4cb05c -- skills/` is empty).

## Verification

Anchored at `92a80ed740`, after the merge of `origin/main` and after the
final commit.

- `pnpm --filter '@objectstack/spec...' build` — exit 0.
- `pnpm --filter @objectstack/spec check:generated` — exit 0; all 16
generated artifacts up to date.
- `pnpm --filter @objectstack/spec typecheck` — exit 0.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` — exit 0; **491 test files, 14299 tests passed**.
- `pnpm lint` (repo-wide `eslint . --no-inline-config`) — exit 0.
- Gate families derived from the real diff with `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: **87 derived, 86 run, 0 UNRUN, 1 NOT
MEASURED**, reconciled with `--ran` over a record carrying each
command's exit code captured before any pipe.
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` — exit 3, PREREQUISITE
NOT MET. It reads the emitted CJS bytes of every published package and
66 packages in this worktree have no `dist/`; clearing it needs a
whole-repo `pnpm build`, which on this shared container means holding
the single heavy-verify lock for far longer than this diff can justify.
Declared, not silently dropped. CI's `Build Core` and `Lint & Repo
Gates` cover it on a fresh checkout.
- `pnpm --filter @objectstack/spec run check:react-declaration-parity`
is recorded at exit 1 because the derived spelling passes no `MANIFEST`,
and that gate exits 1 rather than skipping when it has none. Run as CI
runs it — `MANIFEST="$PWD/sdui.manifest.json" pnpm --filter
@objectstack/spec check:react-declaration-parity --baseline
react-declaration-parity.baseline.json --strict` — it exits **0**: no
new declaration divergence versus the accepted baseline.
- Two further families exited 3 on first contact
(`check:doc-formula-expressions`, `check:lean-entry-closure`); their
prerequisites were built and both then exited 0. Those are the exit
codes recorded.

## Acceptance notes

Noted, not filed: in the reference implementation two of the four
`onNavigate` call sites spell the mode argument `view ?? 'view'`
(`useNavigationOverlay.ts:323` and `:356`, read at objectui
`dda8f3815d`), where `view` is the retired `navigation.view` key. On the
currently pinned sibling an authored view name can therefore still flow
into this closed slot — which is exactly the defect ADR-0049 retired in
17.5.0 and which the card routes to objectui#9547 and its pin bump. It
is already filed there; nothing new is owed. Successor: objectui#9547.

Noted, not filed: `check:react-declaration-parity` reports 111 spec-only
declaration divergences and one block missing from the registry, all
held flat by its accepted baseline. Pre-existing, unrelated to this
line, and the baseline ratchet is green. Successor: none — this is the
gate's standing state, not a finding of this round.

## 维护者速读(草稿)

**改了什么** — 协议里 ListView 的 `onNavigate` 回调,第二个参数的取值声明从 `'view' | 'edit'`
改成 `'view' | 'new_window'`,并在说明里点名这两个模式、写明这个槽位是封闭词表。改动是一行源码,外加由它自动再生的一份
AI 参考页。没有类型签名移动,没有运行时行为变化。

**为什么改** — 这句声明两个方向都是假的:`edit` 从来没有任何调用点发出过,也没有任何分支读它;而用户按住 Cmd/Ctrl
点一行、或配置 `mode: 'new_window'` 时真正发出的
`new_window`,声明里根本没有。照这份契约写代码的人(以及照它写代码的
AI)会写出一条永远不执行的分支,同时漏掉真正会发生的那条——表现为"新窗口打开记录"这个动作静默地什么都不做。

**风险与代价(含回滚)** —
风险很低:改的是一份"说明书",不是执行代码,本仓库没有任何程序读这两个字面量(全仓只有这一处)。代价是消费方需要把 `edit` 那条分支换成
`new_window`,changeset 里写了一行对照表。回滚就是 revert 这个 PR
再跑一次生成器,没有数据迁移、没有存量元数据受影响。需要留意的一点:`skills/` 是受管面,本 PR 里那份 `.md`
完全由生成器产出、零手改,走的是队列闸对已登记生成物的豁免路径;闸在 merge-group 时会逐字节重算,任何一处手写内容都会让豁免失效。

**席位意见** — (待席位填写)

**你要做的** — 确认"把 `edit` 从词表里去掉、把 `new_window` 加进来"这个方向是你要的(这正是
objectui#9547 上你已经同意的 C 案)。其余无需你操作:入队与落地归 PM,本 PR 停在 draft。

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

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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants