fix(spec): react-tier ListView onNavigate declares the mode union it actually emits - #19074
Conversation
…t actually emits
The `onNavigate` overlay in `REACT_BLOCKS` declared the second argument as
`(recordId, action: 'view' | 'edit') => void`. `'edit'` is emitted by no call
site in the reference implementation and read by no branch; `'new_window'` —
what a modifier-click and an authored `navigation: { mode: 'new_window' }`
actually send — was not declared at all.
The union becomes `'view' | 'new_window'`, and the description now names both
modes and states that the slot is a closed navigation-MODE vocabulary. The
generated `skills/objectstack-ui/references/react-blocks.md` is regenerated
by `gen:react-blocks`; it carries no hand edits.
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a056a2eccd280583a189d35c1292cb62b405fe02 && git checkout a056a2eccd280583a189d35c1292cb62b405fe02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5380daa3b0064390708c96ee2b0427a9f1b9c531 92a80ed740e89615f7e678bf3c02e3d75ce65894 && git checkout -B drift-repro 5380daa3b0064390708c96ee2b0427a9f1b9c531 && git merge --no-ff 92a80ed740e89615f7e678bf3c02e3d75ce65894
node scripts/docs-audit/affected-docs.mjs --json 5380daa3b0064390708c96ee2b0427a9f1b9c531 |
⛔ 契约复核记录 —— 整份作废,⛔ 一条也不采纳本席 2026-09-18T16:28Z 判。所判 head: 作废的唯一理由:档位证据实测不达仪器 = 解析复核子代理自身 transcript 的逐请求 harness ⇒ 0/145 在档。 记录头部那行 ⭐ 这是本席的派发缺陷,⛔ 不是复核方的工作质量问题本席起那个子代理时漏了档位指定。那份记录读起来是扎实的 —— 它自己跑了生成器、做了发火对照、把 PR 正文的一处半径申报纠了过来、还明列了自己没测的七件事。 ⇒ 按章程只有一个处置:整体作废。⛔ 不逐条采纳、⛔ 不摘录、⛔ 不把它的任何一条发现带进下一轮的派发词,也 ⛔ 不据以立卡。「逐字采纳或整体作废」的另一半就是:作废的东西不能借尸还魂。 重开同一 head 重开一次,这次指定档位。章程要求同一 head 的重开必须引用上一次作废的理由 —— 即本条。 载体状态
Generated by Claude Code |
|
⭐ 本席( 所判 head: 这份记录取代哪一份它是同一 head 的重开,取代评论 档位证据复核子代理自身 transcript: 喂给复核席的只有:卡面、在先裁决、PR 本体,以及 本席接下来要做的(记录点名归本席的两件)
通道申报
Contract reviewServed-tier: Re-run on the same head. The previous record was voided whole — comment ① Derived judgmentsDiff versus merge-base
② Semver levelChangeset Upheld, not overturned. The clause-② criterion (SKILL.md: does the card widen the accept set or the public surface) is met: a published declaration that authors and their AIs write handlers against gains the member ③ Boundary flagsBlocking: none. Mechanical (not contract defects):
Worth stating, not flagging:
Not measured: whole-repo VERDICT: PASS Implemented-by: Generated by Claude Code |
Provenance —— 契约复核已归档,两个载体同笔清除本席 2026-09-18T16:50Z 清标。按章程「凡清标同笔留 provenance 评论,引记录 id 与所判 head」:
清除动作(⏱️ 2026-09-18T16:49Z 取读回):
⛔ 用定向 DELETE 而非整组 PUT —— 整组写会替换 union,把别的写手刚落的标签一起抹掉。 入队前的自查(记录把它点名为「the PM's pre-queue step」,复核席自己没跑): ⛔ 记录点名归本席、本席照办的两条
⇒ 条款②闸已清,接下来只剩常规落地。 Generated by Claude Code |
巡检 H43 与受管面判词对本 PR 给出不同答案 —— 本席把两边的读数并排记在这里本席 2026-09-18T17:05Z 处置。半态巡检报 H43 #19074 「open and GOVERNED(1 changed file on the register: 两边的读数
判定:⛔ 不是矛盾,是两个不同的问题H43 自己把这件事写明了,逐字:
⇒ H43 数的是登记表成员资格(路径在不在表上),它 ⛔ 不套用生成物豁免;而判词与队列闸套用。两者都对,问的不是同一件事。 ⭐ 那条豁免是维护者亲口裁的,逐字:「纯生成的指针行(spec 源变更后再生成的 references/_index.md) 不需要我审核吧」。本轮采纳的达档复核记录(评论 ⛔ 因此本席不去请
|
Fixes #19057
Clause-②: yes
Maintainer decision batch #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:321declared the ListViewonNavigatesecond 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 authorednavigation: { 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:after:
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 — andview.zod.tsis read-only on this card, untouched.The generated half — zero hand edits
skills/objectstack-ui/references/react-blocks.mdis theOUT_MDofpackages/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 rowspec-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-blocksexits 0 with "1 generated files in sync with packages/spec", andgit status --porcelainis unchanged by that run. Post-merge,pnpm --filter @objectstack/spec check:generatedreports all 16 generated artifacts up to date,check:react-blocksamong 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 #18921).
1. The retired literal, repo-wide — 0 in code or generated output.
git grep -Ffor the old spelling at92a80ed740returns 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/objectstackonly — excludesnode_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:
onNavigatematches 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'"prints0.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 quoteseditverbatim and is deliberately untouched — this instrument cannot see it.Firing control, same instrument, same line:
grep -c "'view'"prints1.3. Hand edits in the governed generated file: 0.
The generator was run, then re-run in
--checkmode, which compares its own emitted bytes against what is on disk; it exits 0.Radius: exactly the one path
OUT_MDnames. It says nothing about any other file underskills/.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, fromgit diff --name-only, which lists three paths in all.)Firing control: the same file's
git diffagainst 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-ratchetclassifies 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 at2767af8e83, whoseskills/tree is byte-identical to this branch's merge base (git diff --stat 2767af8e83 d4cb05cbf0 -- skills/is empty).Verification
Anchored at
92a80ed740, after the merge oforigin/mainand 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-wideeslint . --no-inline-config) — exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 87 derived, 86 run, 0 UNRUN, 1 NOT MEASURED, reconciled with--ranover a record carrying each command's exit code captured before any pipe.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 nodist/; clearing it needs a whole-repopnpm 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'sBuild CoreandLint & Repo Gatescover it on a fresh checkout.pnpm --filter @objectstack/spec run check:react-declaration-parityis recorded at exit 1 because the derived spelling passes noMANIFEST, 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.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
onNavigatecall sites spell the mode argumentview ?? 'view'(useNavigationOverlay.ts:323and:356, read at objectuidda8f3815d), whereviewis the retirednavigation.viewkey. 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-parityreports 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