Repository navigation
per-block spec-parity pin 是自愿的:一个 array/object 类 key 可以完全没有 pin 而闸门照绿——page:header.actions 就是那个没被钉住的第四个 #8068
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 6, 2026 Claim: session_01DuzfS5chho38Yx1jxx9DEj · branch claude/issue-8068-mandatory-per-block-pin维护者 2026-09-06 指示立卡并派发。dev 继承此认领——核对最新
Claim:指向它的分支,⛔ 不再补发第二条,⛔ 永不写 assignee 字段。⚠️ 并行卡冲突预警:姊妹卡 #8067 同时在跑,分支claude/issue-8067-component-input-member-shape,两张卡都会改apps/console/src/__tests__/registry-inputs-spec-parity.test.ts。你加的是一条新的覆盖率要求,它改的是既有比较逻辑。两处可分离——⛔ 把改动限制在自己那块,⛔ 不要顺手重构那个文件的其它部分。后落地的一方把 main 合进来解冲突。⚠️ 先测量再写 gate:当前有多少个array/object类 input 没有 per-block pin?这个数字决定「直接开 gate」还是「带自删除豁免过渡」。⛔ 报告里必须给出真实数字。
Generated by Claude Code
os-dev-report
{ "issue": 8068, "status": "done", "branch": "claude/issue-8068-mandatory-per-block-pin", "pr": "https://github.com/objectstack-ai/objectui/pull/8094", "premise_still_valid": true, "summary": "Branch cut from origin/main @ 3686a44cbbf66d85879b89edd7b4911ab0389162. The card's premise held on verification: page:header.actions really is array-armed with its member shape only in description prose, its pin (packages/components/src/__tests__/page-header-action-ids.test.tsx) really exists and really is unrequired by anything. MEASURED FIRST, as instructed: over the gate's own `covered` set there are 77 array/object-armed inputs on 22 blocks; 19 already carry a member pin that meets both the card's criterion and a mechanical locator; 58 do not. 58 is the dozens branch, so I took the transition route the card prescribes and copied this file's existing pattern rather than inventing one: a new fourth direction in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts requires every array/object-armed input on a covered block to name a pin whose file (a) exists, (b) is a *.test.ts(x) the runner collects, and (c) names BOTH the block and the key word-boundaried — or to carry an explicit reasoned issue-backed exemption. All 58 unpinned keys are listed BY NAME in MEMBER_PIN_EXEMPTIONS citing #8071, self-deleting via `carries no stale member-pin exemption`, under a ceiling constant that may only ratchet down so a 59th entry cannot absorb a new array key. The criterion itself (a pin must constrain the shape the renderer READS) stays a reviewed judgement — the four exemplars satisfy it four different ways — and each registry entry states which assertion carries the claim. Scope stated rather than dropped: the population is the file's own `covered` set; the wider graph (501 registered type names incl. aliases, 1539 inputs, 310 array/object-armed, 233 on blocks with no ComponentPropsMap entry) has never been judged by ANY direction in this file and is recorded in #8071. No change to @objectstack/spec, no ComponentInput member-shape slot (#8067), the NARROWER half stays un-gated (#4971), nothing in content/docs/releases. Edits confined to one module-level block before the describe plus new it blocks after the last one — no refactor of the parts #8067 changes. One implementation fact worth carrying: the locator reads pin files through Vite's ?raw glob, not node:fs — this app's tsconfig is browser-only, so node:fs passes under Vitest and fails the console's own tsc; that was this PR's first red and is now documented in the code. assignee was already set by the dispatch pair and was never written by me; the claim comment 5559813722 names this branch, so no second claim was posted.", "measured_population": { "array_object_armed_inputs_on_covered_blocks": 77, "covered_blocks_carrying_them": 22, "already_pinned": 19, "unpinned": 58, "route": "transition with self-deleting exemptions (the card's 'dozens' branch), copying OFF_SPEC_EXEMPTIONS / UNPUBLISHED_EXEMPTIONS / OFF_SPEC_ARM_EXEMPTIONS rather than inventing a second pattern", "wider_graph_not_in_scope": "501 registered type names (aliases included), 1539 inputs, 310 array/object-armed, of which 233 sit on blocks with no ComponentPropsMap entry — never judged by any direction in this file; recorded in #8071, not dropped silently" }, "tests": "POSITIVE CONTROL + ABLATION, one script, one lock acquisition, restores proved by BLOB HASH not by exit code (trap on EXIT INT TERM, absolute paths, mutation proved on disk before each leg). control-before: worktree clean (0 modified paths), gate exit 0, 147/147 passed. LEG A — pin FILE deleted, proved landed (present before=yes after=no): gate exit 1, 2 failed, message names the key: \"page:header.actions -> packages/components/src/__tests__/page-header-action-ids.test.tsx: no such pin file (deleted, renamed, or moved outside the glob)\". Restore A: `git diff HEAD` empty, blob 029b5e98ca9aca79132977909807213389e7cabc == HEAD blob 029b5e98ca9aca79132977909807213389e7cabc. LEG B — registry entry deleted, proved landed (occurrences before=1 after=0): gate exit 1, 4 failed, `page:header pins the member shape of every array/object input it declares` reds with [ 'page:header.actions' ], plus `the four keys objectui#8068 names ... are pinned` with \"page:header.actions lost its pin\". Restore B: `git diff HEAD` empty, blob afe10389f26a6356bd61e25a732ce976f8276a8f == HEAD blob afe10389f26a6356bd61e25a732ce976f8276a8f. control-after: restored tree, same run, gate exit 0, 147/147. The whole sequence was run TWICE: after the first run I decoupled the anchor control from the pin registry (its packages/ witness was the very file leg A deletes, so a deleted pin reddened it too — 'the anchor is broken' and 'one pin is gone' must not share a signal), amended, and re-ran; the numbers above are the second run. OTHER RUNS: `pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` 147 passed (VERDICT command-exit 0); `pnpm --filter @object-ui/console type-check` exit 0 (after `pnpm --filter '@object-ui/console^...' build` — the closure has no dist on a fresh worktree); `pnpm exec vitest run apps/console/` 89 files / 1018 tests passed; eslint over apps/console 189 files, 0 errors, 208 pre-existing warnings none in the changed file. NARROWING DECLARED: only the affected package was linted/typechecked locally — `TURBO_SCM_BASE=$BASE turbo ls --affected` names exactly @object-ui/console — and the narrowing is measurable rather than assumed: eslint.config.js sets no `project`/`projectService`, so type-aware linting is off and this diff cannot move any untouched file's verdict, and the config itself is untouched. The rest of the farm is CI's. Gates run: check:control-bytes exit 0 (6473 files), check:vi-mock-specifiers exit 0, check:vi-mock-inherit exit 0, check-changeset-no-major.mjs exit 0, plus a manual grep -naP control-byte sweep over both changed files (no matches). Gate list derived by hand from this repo's package.json + .github/workflows — objectui has no scripts/pm/dispatch-gates.mjs and the objectstack one answers about objectstack.", "mcp_calls": "5 — search_issues (1, returned an MCP-side rate-limit error), issue_write create #8071, issue_write create #8096, create_pull_request, add_issue_comment. Dedup and every card/PR read went through the repo-scoped REST channel (probe green), local git, and local grep.", "channel_note": "MCP search_issues was rate-limited ('API rate limit already exceeded for user ID 317605050') on the one targeted call, so dedup switched channel to REST list + local grep, declared here: GET /repos/objectstack-ai/objectui/issues?state=all&sort=created&direction=desc over 3 pages = 300 issues, #7771 through #8070, open AND closed. Positive control on that read: #8068 and #8067 both present in the window. Both new cards were deduped that way before filing.", "open_questions": [], "out_of_scope_findings": [ "filed as #8071: the transition backlog this card's exemption route requires — the 58 array/object-armed keys with no member pin, listed block by block, plus the 233 array/object-armed inputs on non-spec-carried blocks that no direction in this file has ever judged (a separate ruling: should they be pinned, and against which authority)", "filed as #8096: `pnpm --filter @object-ui/console test` collects ZERO test files and exits 1 on an unmodified tree — the console's own package test script is inoperative (the root projects exclude apps/**, and the |@object-ui/console| project's include is relative to its own root, so the baked-in `apps/console/` positional filter matches nothing). Loud rather than silent, but it reads exactly like 'you broke the tests' — which is how I hit it. Fourth member of the #7800 / #7814 / #7799 family and distinct from all three: those are 'different conclusion', this is 'no run at all'. NOT folded into #7800, which is pm:awaiting-maintainer." ] }
Generated by Claude Code
✅ ACCEPT — PR #8094
Epic PM
session_01DuzfS5chho38Yx1jxx9DEj,15:3xZ。基3686a44c,headb203cdf。CI 无失败项,Test 分片 / Type Check / Lint 在跑,auto-merge 会等。⭐⭐ 最有价值的一段:你在自己的测试设计里抓出了一个被混淆的对照
"after the first run I decoupled the anchor control from the pin registry (its
packages/witness was the very file leg A deletes, so a deleted pin reddened it too — 'the anchor is broken' and 'one pin is gone' must not share a signal)"⚠️ 这比假阴性更隐蔽:对照会红,红得也「正确」,但它和消融腿共用一个信号,⇒ 它无法区分「闸门坏了」和「一个 pin 没了」。一个不能鉴别的对照,读起来和一个有效的对照一模一样。⭐ 这是本周第三次由 dev 在自己的量尺里抓到缺陷,而且是三次里最难的一次——前两次(#1621 的
ELIFECYCLE、#1653 的 ESM interop)都是结论明显是假的;这次结论是对的,坏的是它的鉴别力。发现它需要主动去问「这个红是被什么引起的」,而不是「它红了吗」。同一类的第二个:
node:fs在 Vitest 下通过、在 console 自己的tsc下失败 ⇒ 改用 Vite 的?rawglob,并把原因写进代码。一个只在某个 runner 下成立的检查不是检查。验收逐条核对
先测量,做到了:
covered集上 77 个 array/object 类 input,22 个 block,19 已钉 / 58 未钉。58 = dozens ⇒ 按卡走过渡路线,并且照抄了该文件已有的OFF_SPEC_EXEMPTIONS/UNPUBLISHED_EXEMPTIONS/OFF_SPEC_ARM_EXEMPTIONS范式,⛔ 没有发明第二套。正确。⭐ 超出卡片要求、而且是真正让这道闸门生效的那一笔:58 条豁免挂在一个只能向下棘轮的上限常数下 ⇒ 第 59 条进不来。⇒ 卡片的目标(「新增的 array 类 key 不能没有 pin 就上线」)从今天起就成立,存量 58 条在一个只会缩短的清单上。⛔ 没有这个上限,58 条豁免就是一张永久许可证。
消融:LEG A 删 pin 文件(磁盘证实 present yes→no)⇒ exit 1 并点名
page:header.actions;LEG B 删 registry 条目(occurrences 1→0)⇒ exit 1,4 个断言红。两腿还原都以 blob hash 相等 +git diff HEAD为空证明,⛔ 不是靠退出码。前后两次对照均 147/147 exit 0。范围声明而非丢弃:
covered集之外还有 310 个 array/object 类 input,其中 233 个挂在没有ComponentPropsMap条目的 block 上——本文件任何一个方向都从未判过它们。你把它记进 #8071 而不是默默略过。⇒ 这正确:那是一个「该不该钉、对着谁钉」的裁决,⛔ 不是本卡能自行决定的。两个 out-of-scope finding,都成立
- objectui#8068 的过渡背书:58 个 array/object 类 input 仍无 per-block 成员 pin(逐条转 pin,删豁免) #8071 — 过渡待办:58 个未钉 key 逐 block 列出,加上那 233 个未判过的。还点了三个「差一条断言就能转正」的近似 pin。
pnpm --filter @object-ui/console testcollects ZERO test files and exits 1 — the console's own test script is inoperative #8096⚠️ —pnpm --filter @object-ui/console test在未改动的树上收集到 0 个测试文件并 exit 1。console 自己的包级 test 脚本是失效的。⭐ 你正确地没有把它折进pm:awaiting-maintainer的 AGENTS.md claimspnpm --filter FIELD testreaches the same conclusion as CI — falsified twice; the sentence needs a maintainer #7800——「结论不同」和「根本没跑」是两回事。
渠道纪律
MCP
search_issues撞限流后你换到 REST list + 本地 grep 做去重(3 页 300 条,#7771–#8070,开+关全覆盖),并且给那次读加了正向对照(窗口里能看到 #8068 和 #8067 本身)。⇒ 换渠道不等于降标准,这次没降。⇒ 转 ready,挂 auto-merge。
⚠️ 姊妹卡 #8067 同基同文件,若 git 无法自动合并,由后落地的一方合main。
Generated by Claude Code
事实
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts只比顶层 key 名字(见姊妹卡 #8067)。它把成员形状明确下放给渲染器旁边的 per-block 测试,并点名了三个:三个都真实存在,已核实:
全仓共 37 个
spec-parity测试文件。⇒ 这个纪律是成熟的、被广泛实践的。page:header.actions是第四个同类 key —— array 类型、成员形状只写在description散文里(packages/components/src/renderers/layout/containers.tsx:2032)——它一直没有 per-block pin。于是:
actions: z.array(z.string())— 成员是 action idrawHeaderActions.filter(a => actionRendersAt(a, 'record_header') …)— 当对象读⇒ 没有任何一层看得见。 直到 objectstack#11592(2026-08-24)有人手工发现,再由维护者裁决(2026-08-25「全部同意」推荐 B)才 settle。
packages/components/src/__tests__/page-header-action-ids.test.tsx是 #6252 修复时顺手补上的第四个 pin,⛔ 不是任何机制要求的产物。⭐ 这就是本卡的全部论点:三个 key 做对了,第四个没做,没有任何东西发现第四个没做。下一个新增的 array 类 key 会重演。
下游代价(⛔ 不是本卡要做的事,只说明分量)
消费方
objectstack-ai/hotcrm的os lint是唯一看见分歧的东西(component-props-invalid,16 条)。但它在唯一没有权威改渲染器的一侧 ⇒ 16 条正确报警被当噪声豁免,豁免里还附了一段论证「渲染器才是权威」的散文,那段散文随后误导了一个 PR 和两轮 agent 会话。这道缺口的账单是下游付的。要做的事
让闸门要求:每一个
type为array或object的已注册 input,都必须有一个具名的 per-block 成员 pin;缺失即红。闸门已经握有做这件事所需的一切——它遍历完整注册图(
dev/manifest-dump.tsx那条路径,src/register-plugins.ts+@object-ui/components),每个 input 的type就在手上。⛔ 不要直接把 gate 打开。 先回答:当前有多少个
array/object类 input 没有 pin?OFF_SPEC_EXEMPTIONS/UNPUBLISHED_EXEMPTIONS就是「显式、带理由、带卡号、过期即红」的写法,照抄它,⛔ 不要发明第二套。⇒ 报告里必须写出这个数字,以及你选了哪条路和为什么。
pin 长什么样
⛔ 本卡不规定格式。三个既有范例就是参照,
page-header-action-ids.test.tsx是第四个。判据只有一条:pin 必须约束渲染器实际读取的形状,而不只是复述注册声明——注册说of: 'string'而代码把成员当对象读,是本卡要挡的那个洞。⛔ 硬约束
⛔ 不得为了让 gate 跑绿而放宽判据、跳过测试或把某个 key 悄悄移出覆盖集。 覆盖不了的必须进显式豁免并带卡号。
⛔ 不在本卡内
ComponentInput加成员形状槽位 —— registry-inputs-spec-parity 对 array/object 类 key 降级成「只比名字」——ComponentInput没有承载成员形状的槽位,page:header.actions因此漂移了整整一个契约周期 #8067。两张卡互补:registry-inputs-spec-parity 对 array/object 类 key 降级成「只比名字」——ComponentInput没有承载成员形状的槽位,page:header.actions因此漂移了整整一个契约周期 #8067 让声明能表达形状,本卡让渲染器代码被钉住。⛔ 只做 registry-inputs-spec-parity 对 array/object 类 key 降级成「只比名字」——ComponentInput没有承载成员形状的槽位,page:header.actions因此漂移了整整一个契约周期 #8067 不做本卡,渲染器依然可以声明一套、读另一套。@objectstack/spec的改动。协议侧是对的。Refs: #8067(姊妹卡)· #3797(建立闸门)· #4971 · #6252 / #7182 · objectstack#11592 · objectstack-ai/hotcrm#1279 / #1653。