Skip to content

per-block spec-parity pin 是自愿的:一个 array/object 类 key 可以完全没有 pin 而闸门照绿——page:header.actions 就是那个没被钉住的第四个 #8068

Description

@os-steve

事实

apps/console/src/__tests__/registry-inputs-spec-parity.test.ts 只比顶层 key 名字(见姊妹卡 #8067)。它把成员形状明确下放给渲染器旁边的 per-block 测试,并点名了三个:

… which is why record:details.sections, record:highlights.fields and record:related_list.add publish their members in prose and are pinned by per-block tests next to their renderers.

三个都真实存在,已核实:

packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts
packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts
packages/plugin-detail/src/__tests__/recordRelatedListInputs.spec-parity.test.ts

全仓共 37 个 spec-parity 测试文件。⇒ 这个纪律是成熟的、被广泛实践的。

⚠️ 但它是自愿的,没有任何东西要求它

page:header.actions 是第四个同类 key —— array 类型、成员形状只写在 description 散文里(packages/components/src/renderers/layout/containers.tsx:2032)——它一直没有 per-block pin。

于是:

spec actions: z.array(z.string()) — 成员是 action id
渲染器实际读 rawHeaderActions.filter(a => actionRendersAt(a, 'record_header') …) — 当对象读
parity 闸门 key 名字对得上 ⇒ 绿
per-block pin 不存在

⇒ 没有任何一层看得见。 直到 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

⛔ 不要直接把 gate 打开。 先回答:当前有多少个 array/object 类 input 没有 pin?

  • 若是个位数 → 直接补齐并让 gate 立即生效。
  • 若是几十个 → 需要一个带自删除豁免的过渡,而本仓已有现成范式:该闸门自己的 OFF_SPEC_EXEMPTIONS / UNPUBLISHED_EXEMPTIONS 就是「显式、带理由、带卡号、过期即红」的写法,照抄它,⛔ 不要发明第二套。

⇒ 报告里必须写出这个数字,以及你选了哪条路和为什么。

pin 长什么样

⛔ 本卡不规定格式。三个既有范例就是参照,page-header-action-ids.test.tsx 是第四个。判据只有一条:pin 必须约束渲染器实际读取的形状,而不只是复述注册声明——注册说 of: 'string' 而代码把成员当对象读,是本卡要挡的那个洞。

⛔ 硬约束

⚠️ 消融是验收的主体:删掉(或改坏)某个 array 类 key 的 pin,新 gate 必须红并点名那个 key。同一轮里配上正向对照——未改动的树上 gate 通过。⛔ 一个不会红的 gate 和一个不存在的 gate 无法区分,本仓今晚已经两次栽在假阴性上。

⛔ 不得为了让 gate 跑绿而放宽判据、跳过测试或把某个 key 悄悄移出覆盖集。 覆盖不了的必须进显式豁免并带卡号。

⛔ 不在本卡内

Refs: #8067(姊妹卡)· #3797(建立闸门)· #4971 · #6252 / #7182 · objectstack#11592 · objectstack-ai/hotcrm#1279 / #1653。

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 6, 2026
  2. self-assigned this
    on Sep 6, 2026
  3. os-steve commented on Sep 6, 2026

    @os-steve
    CollaboratorAuthor

    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

  4. os-steve commented on Sep 6, 2026

    @os-steve
    CollaboratorAuthor

    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

  5. os-steve commented on Sep 6, 2026

    @os-steve
    CollaboratorAuthor

    ✅ ACCEPT — PR #8094

    Epic PM session_01DuzfS5chho38Yx1jxx9DEj,15:3xZ。基 3686a44c,head b203cdf。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 的 ?raw glob,并把原因写进代码。一个只在某个 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,都成立

    渠道纪律

    MCP search_issues 撞限流后你换到 REST list + 本地 grep 做去重(3 页 300 条,#7771–#8070,开+关全覆盖),并且给那次读加了正向对照(窗口里能看到 #8068 和 #8067 本身)。⇒ 换渠道不等于降标准,这次没降。

    ⇒ 转 ready,挂 auto-merge。⚠️ 姊妹卡 #8067 同基同文件,若 git 无法自动合并,由后落地的一方合 main。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions