Repository navigation
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
Conversation
…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
…ntrol-certifies-instrument
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)— PR #18921 · 席位定稿 2026-09-18T06:20Z(草稿在正文,以本评论为准)改了什么:PM 读数纪律加一条判别式,两行,紧跟「零命中须配控制词」「控制词双零 = 仪器坏」之后:控制词通过只证明工具活着,不证明问题问对;报零命中时须写清这个工具能看见哪些形态(文件内容 / 文件名 / 声明 / 字段),并举一个已知存在、这种问法必定看不见的目标;所求落在可达半径之外,这个零就不是读数,换看得见它的工具再报。核心条款同 PR 加一行压缩版。两个文件行数不变(812 / 151)。 为什么改:这一班实测两次同型事故:用内容 grep 判「文件存不存在」(文件名不在内容里),用「哪些文件提到符号」答「哪个文件声明它」(声明与提及在内容 grep 里同形,把 风险与代价(含回滚):两文件都在棘轮上限,新增行靠删本文件里已在别处说过的内容付账,席位逐行核过幸存:① SKILL.md 那行纯指针(main-red 的跳队例外与事故锚卡见 landing-operations B)折进它服务的止血规则,两个约定原文不动、触发它们的那句仍指向 B,代价只是「跳队 / 事故锚」两个词在 SKILL.md 里搜不到了;② 一行三句皆有重复的汇总行退掉,「立卡者附查重词」挪到相邻查重规则,「分诊按词查自有列表」「零命中须控制词背书」各在别处仍在;③ 核心条款两行说明文字压成一行,无规则降级。门禁全绿。与在等你批准的 PR #18903 同文件不同行段,谁后落谁先合 main。回滚 = revert 一个提交。 席位意见:建议批准。派发时我假设一行能装下,dev 实测一行装得下字节、装不下「形态清单」和「换仪器」两个动作,两行更对——形态清单正是让读者测试可机械化的部分。 你要做的:确认这条判别式该进 PM 读数纪律,是则 APPROVE( Generated by Claude Code |
|
Landing record — ruling C, on an authorized APPROVED (skills seat, os-zhuang ( Generated by Claude Code |
…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>
… 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>
…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>
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 grepwith 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; andgit grep -l NamedListView(16 mentioning files, both controls fine) answered "which file DECLARES it" withviews.tswhen the declaration is inobjectql.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 Bcore-rules, 平台读数纪律, directly under its 零命中/双零 row (the twin, compressed):
- 控制通过 ≠ 问题问对:零命中须写仪器可达半径与半径外已知目标,半径外的零非读数。— 117 BThe 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:
git grepsees 内容. 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.^(export )?(const|type|interface) NAMEspelling already recorded inreferences/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
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):
references/landing-operations.mdB 节。」 — 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).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-syncandcheck:skill-frame-freshnessare 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/mainwas merged at 625db0e (merge commit 6450bcc) and the gates below ran on that head; the fourmaincommits that landed after it touch onlyscripts/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 themainmerge. 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-expressionsexited 3 (PREREQUISITE NOT MET—@objectstack/formulaand@objectstack/lintnot built; nothing measured), the build ran underos-verify-lock.sh(VERDICT command-exit 0 · held the lock 147s), and the re-run exited 0. Verdict lines: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'sfiles[].Acceptance notes
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. 承接者:无.check:skill-frame-freshnessis not in the derived list for a diff on these two files whilecheck:skill-frame-syncis; 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