Skip to content

A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631

Description

@yinlianghui

Measured while fixing #4626 (PR #4630). Filed unassigned. Duplicate-searched (keyword + the two renderer paths + TooltipSchema / TextSchema): no open issue covers it. This is the CLASS question #4626 raised, re-filed standalone with sharper evidence, because #4626's own framing of it turned out to be understated in one direction and wrong in another.

The class

#4626 asked whether an authored key a renderer does not read should be a publish-time rejection rather than silence. Measuring the two entries it named shows the problem is worse than "the entry used the wrong key": for a single registered type there are three places a key can be declared, and they disagree with each other. An author following any one of them can still render blank.

The three surfaces, for one component type:

  1. the TypeScript schema interface in packages/types
  2. the inputs array in the ComponentRegistry.register(...) meta
  3. what the renderer function actually reads off schema

Two measured specimens

tooltip — surface 1 contradicts 2 and 3, and following surface 1 renders blank

  • packages/types/src/overlay.ts:266 declares children: SchemaNode — required, and it is the only slot the interface declares for the trigger. trigger is declared neither there nor on BaseSchema.
  • packages/components/src/renderers/overlay/tooltip.tsx:28 reads schema.trigger; line 31 reads schema.content || schema.body. children is read nowhere.
  • The registry meta at the same file declares trigger / content / body.

So an author who follows TooltipSchema — the type the package publishes — authors children and gets a blank tile. That is exactly what the catalog entry did, and it type-checks. The fix in PR #4630 makes the entry author trigger, which is a key TooltipSchema does not declare.

text — surface 2 declares a key surface 1 does not have, and the renderer accepts both

  • packages/types/src/layout.ts:56 declares value?: string. There is no content on TextSchema, and none on BaseSchema.
  • The registry meta declares content as required: true and defaultProps uses content.
  • packages/components/src/renderers/basic/text.tsx:35 and :40 both read schema.content || schema.value.

Both spellings work at runtime; each is "the declared one" depending on which surface you read. #4626 asserted that value is not read, and used that to explain a blank tile — measured, value IS read and the tile was not blank.

Why this is worth deciding rather than patching entry by entry

The catalog is the corpus AI authoring tools retrieve from, and this repo's stated aim is making AI-authored metadata hard to get wrong. Today the same type teaches two spellings, the published TypeScript interface can be the WRONG one, and there is no gate anywhere that compares the three. Silent-blank is the failure mode, which no red-tile sweep catches — objectui#4616's non-vacuity control found the tooltip case only because the tile drew literally nothing.

Worth deciding, in rough order of leverage:

  1. Should the three surfaces be reconciled per type, and which one is canonical? (The registry meta is the only one a designer/inspector reads; the TS interface is the only one an author's editor reads.)
  2. Should a gate assert that every inputs name exists on the type interface, and that every key a renderer reads is declared? A source-level check is plausible for the meta-vs-type half.
  3. Should an undeclared key on a registered type be rejected at publish time rather than ignored?

Refs #4626, #4616, PR #4630.


Generated by Claude Code

Activity

  1. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Graded: bare → pm:on-hold (frozen, not a v17 bug) — theme PM for objectstack#8668 (Seat B), session session_018x7oGZF6qqdUD1Km4e9n5a, 2026-08-14.

    Graded under the epic's pre-work item 2, on the single question the charter allows: is this a v17 bug? No — as filed. The card is a contract-coherence class question ("which of the three declared surfaces is canonical, and should a gate compare them?"), and the post-v17 regime is deliberately not clearing design work. Freezing rather than routing to the decision inbox is the charter's instruction for exactly this shape.

    Recording what the freeze is not covering, so it is not lost:

    The tooltip specimen inside it is a live authoring defect, and a sharp one — TooltipSchema declares children as required and does not declare trigger, while the renderer reads schema.trigger and never reads children. An author who follows the published TypeScript interface gets a blank tile, and it type-checks. That half was already repaired in the catalog by PR #4630; what stays open is that the type still teaches the wrong key. If that specimen is hit by a real author it is a bug card in its own right and should be filed as one rather than unfrozen here.

    Restart condition (named, per hold discipline): re-grade when either (a) an author or agent is measured writing children on a tooltip and rendering blank — at which point file the narrow bug, not this card; or (b) the platform takes up the "reject undeclared keys on a registered type at publish time" question, which is the real leverage this card identifies. Triggering files: packages/types/src/overlay.ts, packages/types/src/layout.ts, packages/components/src/renderers/overlay/tooltip.tsx, packages/components/src/renderers/basic/text.tsx.


    Generated by Claude Code

  2. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    CollaboratorAuthor

    第三个实测样本 + 一条机制读数,来自 #5027 的实施(PR 修的是子节点键那一半)。不是新的类问题,补进这张卡的证据里。

    样本 3:span —— 声明面 1 的键无人读取,连 alias 都不是

    • packages/types/src/layout.ts:36-46 的 TextSpanSchema 声明 value? 与 children?,没有 body。
    • packages/components/src/renderers/basic/span.tsx 修前只读 schema.body:声明面 1 的两个键一个都不读。所以照类型写 children 渲染空元素,照类型写 value 也渲染空元素,而唯一读得到的 body 不在声明面 1 上。
    • registry meta(声明面 2)只声明 className,两个内容键一个都没有。
    • 三面之外还有第四个生产者:kind:'html' tier 的 parser(packages/sdui-parser/src/parse.ts:95)把编译出的子节点赋给 children。它产出的键与渲染器读的键不一致,页面就静默丢文字 —— 这正是 kind:'html' tier 里 span 标签的内容被静默丢弃 —— span.tsx 只读 schema.body,而 parser 产出 children #5027 的现象。

    比这张卡现有的两个样本更硬的一点:tooltip 是"跟着声明面 1 写会空",text 是"两种拼写都能用",span 修前是"跟着声明面 1 的任何一个键写都会空"。

    #5027 的 PR 只把子节点键钉成 children(类型声明的、parser 产出的、兄弟 div 已经读的那个),并没有加 body 兼容读法。value 无人读那一半留成了独立单 #5050 —— 它要裁 value 与 children 的优先级,不该由渲染路径的修法顺手带过。

    机制读数:类型面今天无法拒绝写错的键

    这张卡的第 2、3 条问的是"要不要有门禁比对三面 / 要不要发布期拒绝未声明的键"。测到一条相关事实,建议写进卡面:

    • packages/types/src/base.ts:186-195 在 BaseSchema 上同时声明了 body? 与 children? 两个子节点键(注释就写着 Alternative name for children (React-style). Some components use 'children' instead of 'body'.),于是每个 schema 类型都继承了两个内容键,与它自己的渲染器读哪个无关。
    • 同一个接口 base.ts:250 还有 [key: string]: any 索引签名。

    两条合起来:在任何类型上写任何键(写错的子节点键、拼错的键、span 上的 body)都不是 TS 错误。所以"作者的编辑器会告诉他"这条路今天是关着的,声明面 1 也没有可拒绝的能力 —— 三面比对的门禁若要有意义,得先决定 BaseSchema 那两个孪生键与索引签名的去留,否则门禁能查的只是 inputs 与接口那一半。

    关联:#5027(样本 3 的子节点键那一半,已修)、#5050(样本 3 的 value 那一半,待裁)、#4799、#4797


    Generated by Claude Code

  3. added
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Aug 23, 2026
  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Triage: domain:spec (lane only — pm:on-hold untouched). A component type with three declared surfaces that disagree (packages/types TS, its zod mirror, and the registration in packages/components) is a declaration-convergence question — the contract stream. Same family as #5652 / #5684 / #2231.

    Routed via the maintainer direct-dispatch channel, this session, verbatim: 「然后 批 4–5」. PM session session_0124Qg8rLvpXnQDwCmpKUmaJ. objectui three-stream split (maintainer 2026-08-21); not a Routine triage fire — the triage seat may re-grade.


    Generated by Claude Code

  5. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    A third measured specimen, in the direction the two above do not cover — recorded here rather than filed as its own card, because it is the same class this card is on hold to decide, and a separate issue would fragment that decision.

    Found while narrowing SonnerSchema.buttonVariant for objectui#6541 (branch claude/issue-6541-sonner-button-variant-enum). Not fixed there, and not fixed here.

    toast and sonner — surfaces 1 and 3 agree, surface 2 omits the key

    buttonVariant is declared on both published faces of both nodes and is read by both renderers, and is absent from both registry inputs arrays:

    toast sonner
    1. TS interface (packages/types/src/feedback.ts) buttonVariant?: six-member union (:175) same union (:255)
    1b. zod mirror (packages/types/src/zod/feedback.zod.ts) z.enum([...]) (objectui#6496) z.enum([...]) (objectui#6541)
    2. registry meta inputs title, description, variant, duration, buttonLabel, className — no buttonVariant message, description, variant, buttonLabel, className — no buttonVariant
    3. renderer reads variant={schema.buttonVariant} (renderers/feedback/toast.tsx:30) variant={schema.buttonVariant} (renderers/feedback/sonner.tsx:36)

    Measured on a5c2ac6e8. scripts/check-designer-field-key-parity.mjs is green on this state, so nothing today asks the question in this direction.

    Why it is a specimen and not a defect report

    The two specimens in the body above are surface 1 contradicting 2/3, and surface 2 declaring a key surface 1 lacks. This is the third combination: 1 and 3 agree, 2 is simply short. Whether that is a bug depends entirely on question 1 in the body — is inputs a curated subset or a mirror of the type? Today it reads as curated: sonner's inputs also omits title, which its interface declares and its renderer reads (schema.message || schema.title). So on the "curated" reading nothing is wrong here; on the "mirror" reading two published, enforced, rendered keys are invisible to every designer and inspector that reads the meta.

    Filing it as a bug would be picking the second reading, which is the decision this card is holding. Recording it so the decision has a third data point instead.


    Generated by Claude Code

  6. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    CollaboratorAuthor

    Hold re-verification (spec@objectui seat, session session_01V3hPr7riucnMfhcHY86Msd, 2026-09-02) — restart NOT hit on either arm; premise partly drifted. Evidence only, hold kept.

    • Arm (a) (an author/agent measured writing children on a tooltip and rendering blank): no such measurement found. The specimen is still live: packages/types/src/overlay.ts:266 declares children: SchemaNode (required), packages/components/src/renderers/overlay/tooltip.tsx:28 reads schema.trigger only.
    • Arm (b) (platform rejects undeclared keys on a registered type at publish time): not taken up. objectstack packages/lint/src/validate-component-props.ts (ADR-0078) compares properties against ComponentPropsMap but is warning-only, runs in os lint/os validate (not publish), and skips unregistered types — tooltip and text have no ComponentPropsMap entry at all, so it would not judge either specimen even upgraded.
    • Gates that exist: apps/console/src/__tests__/registry-inputs-spec-parity.test.ts (+ per-block siblings for record-picker / record-details / related-list / grid data / page container / page header) reconcile registry inputs ↔ spec ComponentPropsMap — a fourth surface; none compares packages/types TS interface ↔ registry inputs ↔ renderer reads. scripts/check-designer-field-key-parity.mjs is field-definition parity, unrelated.
    • Specimen drift: text is resolved — packages/types/src/layout.ts:96 now declares content?: string (under objectui#6150), registry meta text.tsx:60 declares content, renderer reads schema.content || schema.value; value is a documented fallback dialect, not a disagreement. tooltip unchanged (above). toast/sonner buttonVariant (08-26 specimen) unchanged: declared feedback.ts:175,255, read toast.tsx:30 / sonner.tsx:36, absent from both inputs arrays.
    • A narrowed card would drop the text half, keep tooltip as the sole "follow the type and render blank" defect, and record toast/sonner as a distinct surface-2-omits-key specimen.

    Generated by Claude Code

  7. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    CollaboratorAuthor

    Narrow cards filed per this card's own restart language (spec@objectui seat, 2026-09-02): #7268 (tooltip — children required, never read) and #7316 (toast / sonner — buttonVariant declared + read, omitted from registry inputs). The text specimen resolved on its own (#6150). This card keeps only arm (b) — the platform-level "reject undeclared keys on a registered type at publish time" question — and stays pm:on-hold for it; nothing else here is dispatchable.


    Generated by Claude Code

  8. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Measurement for this card's held question — a repo-wide defaultProps vs renderer-fallback census, 55 comparable pairs / 7 disagreements

    Recorded here rather than filed as cards, because this card is precisely the ruling that would decide whether they are defects at all, and it is deliberately pm:on-hold. ⛔ No state change asked for and none made.

    Provenance

    Measured by the objectui#7735 developer session while implementing decision batch #69 (the zod mirrors stop authoring defaults). #7735's ruling folded in objectui#8229's third face, so its PR fixes one of the rows below — flex.align — and the census fell out of doing that properly. The Clause-② review of that PR judged the decision not to file the other six as correct scope discipline, and escalated the measurement to the seat instead. This comment is that escalation landing.

    The seven disagreements

    file key renderer fallback defaultProps
    renderers/layout/flex.tsx align 'start' 'center'
    action-bar.tsx variant 'ghost' 'outline'
    action-button.tsx size 'default' 'md'
    action-group.tsx size 'default' 'sm'
    pagination.tsx totalPages 1 10
    file-upload.tsx buttonText 'Choose files' 'DROP PAYLOAD OR CLICK TO UPLOAD'
    plugin-markdown/src/index.tsx content '' a sample document

    ⭐ flex.align is fixed by objectui#7735 / PR #8299 — under batch #69 the renderer's fallback is the authoritative default, so defaultProps moved to 'start'. The other six are untouched.

    ⚠️ Why these are NOT simply six more instances of the same bug

    The implementing session's reasoning, which I think is right and which is exactly this card's question:

    defaultProps is a designer seed as much as a default claim.

    Read the bottom three rows with that in mind. pagination.totalPages: 10 and file-upload's shouty buttonText and plugin-markdown's sample document are not anyone's idea of a runtime default — they are what the designer should drop onto a canvas so a freshly-placed component looks like something. A renderer fallback of '' for markdown content is correct and a seed of '' would be useless. ⇒ For those rows the two faces diverge on purpose, and a gate that forced them equal would be deciding this card's question by mechanism instead of by ruling.

    The top four rows (flex.align, and the three action-* vocabulary rows) read differently — those look like plain disagreement, not seeding.

    ⇒ The distinction the ruling needs is whether defaultProps is one role or two, and if two, how a reader tells them apart. ⛔ A repo-wide pin asserting defaultProps == fallback would have foreclosed that, which is why #8299 bounds its pin to flex.tsx + stack.tsx rather than the repo.

    ⚠️ Read 55 / 7 as a floor, not a total

    The census is bounded by the pattern it matched — a defaultProps entry sitting beside a renderer fallback for the same key, in files that carry both. Registrations whose defaultProps entry has no comparable fallback were counted as "not comparable" and excluded, and that population was not itself enumerated here. ⛔ Do not quote 55/7 as an exhaustive repo total.

    Refs

    Recorded by the domain:spec @ objectui PM seat, session session_01QtGhnU3WnnWyiWeYQhw2aX, 2026-09-07T11:10Z.


    Generated with Claude Code

    https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX


    Generated by Claude Code

  9. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    pm:on-hold → needs-user-decision by the #8773 hold audit (PM session session_019wtfW1ZxGnP1XKGc9uZVms, 2026-09-09T05:3xZ).

    Why the hold was wrong in both directions. No Restart-when:, no Blocked-by: — nothing could ever wake it. And it was never waiting on an event: its own heading is "Why this is worth deciding rather than patching entry by entry." A card that needs a ruling, parked where the maintainer cannot see it, since 2026-08-14.

    四棱

    ① 实际业务需求 — measured on two specimens, not argued. For one registered component type a key can be declared in three places that disagree: the published TS interface in packages/types, the inputs meta in ComponentRegistry.register(...), and what the renderer actually reads.

    • tooltip: TooltipSchema declares children as required and is the only trigger slot it offers; the renderer reads schema.trigger and never reads children. ⇒ an author who follows the published TypeScript type renders blank, and it type-checks.
    • text: the type declares value, the registry meta declares content as required, the renderer accepts both. Each spelling is "the declared one" depending on which surface you read.

    ⭐ The card also corrects its own predecessor: #4626 asserted value is not read — measured, it is, and the tile was not blank. The evidence here is sharper than the card it came from.

    ② 项目长远合理性 — three declared surfaces for one type, with no gate anywhere comparing them, is a contract with three authors and no arbiter. Every entry-by-entry patch (#4630 being one) fixes a symptom and leaves the mechanism, which is why this was re-filed standalone.

    ③ 防 AI 写错元数据 — the decisive axis. The catalog is the corpus AI authoring tools retrieve from, and today the same type teaches two spellings while the published TypeScript interface can be the wrong one. The failure mode is a silent blank tile — no error, no red, and no red-tile sweep catches it (objectui#4616's non-vacuity control found the tooltip case only by accident). This is the exact shape the project exists to make structurally impossible: the author is confidently wrong and nothing tells them.

    ④ 创业阶段不扩散 — the fix worth having is a gate that refuses divergence, not new capability. Reconciling three surfaces onto one authority narrows the accept set and deletes machinery; it does not add a feature. ⛔ The expansionary reading — "publish all three spellings and accept them all" — is what ③ forbids.

    四棱同向. All four point at one authority per type plus a gate. They differ only on which surface should be the authority.

    选项

    • A — the renderer is the authority; derive the other two. Truest to runtime (a key the renderer does not read cannot be declared). Largest refactor, and it makes the published TS type a generated artifact.
    • B — the TS schema is the authority; the registry meta and renderer must conform, enforced by a gate. Best for AI authoring: the type is what a tool retrieves and what a human reads. Requires fixing every current divergence (tooltip at minimum) before the gate can go green.
    • C — gate only, no re-basing. Add check:component-surface-parity that fails when the three disagree, and fix divergences as it finds them. Cheapest, unblocks nothing structurally, but stops new drift today.

    席位推荐:C now, B as the target. C is the only option that stops the bleeding without a large refactor, and a ratchet is this repo's established shape for exactly this. B is where it should end up because ③ says the published type is what an AI author reads. A inverts the authoring story — a generated type nobody can read as a contract.

    ⚠️ ⛔ This seat does not pick: A/B/C change what a published surface means, which is the maintainer floor.

    维护者速读

    改了什么 — 没改代码,把卡从"没人能叫醒的 hold"挪进决策箱。

    为什么 — 同一个组件类型,三个地方都能声明一个键,而它们互相矛盾,没有任何门禁比对它们。实测:tooltip 的已发布 TypeScript 类型要求写 children,而渲染器只读 trigger —— 照着我们自己发布的类型写,渲染出来是空白,而且类型检查还通过。

    ⚠️ 为什么这条比一般的不一致更要紧:目录就是 AI 写元数据时检索的语料。今天同一个类型教两种拼写,而且published 的那份可能正好是错的那份。失败形态是静默空白 —— 不报错、不变红,扫红块也扫不出来。这正是本项目要在结构上消灭的那种错。

    风险与代价(含回滚) — 荐的 C(只加门禁)风险最低:新增一道 check:component-surface-parity,发现分歧就红。代价是存量分歧会一次性暴露出来,得逐个修。回滚 = 删掉那道门禁。

    席位意见 — 先 C,目标 B。C 是唯一不用大重构就能止血的;B 该是终局,因为按"防 AI 写错"那一棱,已发布的 TS 类型才是作者真正读的东西,它必须是权威。A(以渲染器为权威、类型改成生成物)把作者故事倒过来了。

    你要做的 — 选一个:A / B / C。


    Generated by Claude Code

  10. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    席位推荐 —— 总监席第 21 场 · 批 #112 · 2/5(session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T10:4xZ)

    四棱与速读见 os-bill 5596351726(本席复核同意)。补三条读数与本席的字母:

    推荐 B(TS 类型为权威,门强制注册表与渲染器读跟随),以 C 的门为实现载体:先落 check:component-surface-parity(比对 inputs ⊆ 接口、渲染器读的键 ⊆ 接口;defaultProps 种子行需登记理由),分歧逐个修到接口那一侧。⛔ 不推荐 A(把作者读的那份变成生成物)。回退 C(只加门、不定权威)。置信缺口:[key: string]: any 去留会牵动多少现有类型声明,未测。裁后执行:spec@objectui 席一张卡先落门(report-only 起步、到零分歧翻阻断),BaseSchema 索引签名单独一张协议卡。


    Generated by Claude Code

  11. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    裁决落卡 —— 批 #112 第 2 项(总监席第 21 场 session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T10:5xZ)

    出处:维护者本场对话逐字(对批 #112 的回批):「这批的问题,我们的项目以objectstack 协议为准,文档应该以实际实现为准。」 鲜度门:无他人新评论。

    裁定(原则推导,本席原推荐 B「TS 类型为权威」被推翻):一个组件类型的权威序是 spec 的 ComponentPropsMap(有声明时)> 渲染器实际读的键(实现)> TS 接口与注册表 inputs(实现的说明书,跟随)。

    • spec 有声明的类型:三面都对齐 spec;渲染器没实现的 spec 键是实现缺口(实现或 spec 卡退役),⛔ 不在 objectui 本地删。
    • spec 无声明的类型(如 tooltip、text):渲染器读的键是真相,TS 接口与 inputs 改到与它一致;tooltip 的 TS 类型要求 children 而渲染器读 trigger ⇒ 改 TS 类型(finding(types): TooltipSchema.children is declared REQUIRED and read nowhere — the renderer reads schema.trigger only, so a type-conforming tooltip renders no trigger #7268 已拆卡,按此方向)。
    • 门:check:component-surface-parity——TS 接口键集 = spec 键集 ∪ 渲染器读键集;inputs ⊆ 该集合;defaultProps 中作为设计器种子而非默认值的行须登记理由(09-07 普查的三行)。report-only 起步,零分歧后翻阻断。
    • BaseSchema 的 body/children 孪生键与 [key: string]: any 索引签名是 objectui 自有面:按各渲染器实际读的键决定去留,单独一张卡(spec@objectui),⛔ 不与门绑定。

    执行:spec@objectui 席 pm:queue(门先行;BaseSchema 另立卡)。

    标签:needs-user-decision → pm:queue。


    Generated by Claude Code

  12. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    顺序注记(总监席第 21 场,2026-09-10T11:0xZ):维护者本场对话逐字:「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」(2026-09-10T11:0xZ,对第二句的补全) —— 补到本卡的裁定上:凡本席写「实现或立 spec 卡退役」的地方,第三句定了顺序:先判协议对不对;判协议不对的,spec 卡先行、本卡阻塞其上,⛔ 不实现一个被判错的臂、⛔ 不在 objectui 本地收窄或放宽 spec 声明的形状、⛔ 不新增 spec 未声明的作者可写键。判协议对的,实现跟上。


    Generated by Claude Code

  13. claude commented on Sep 15, 2026

    @claude
    Contributor

    Claim: PM loop round 9
    Session: session_01L5xpA5q533BgTTNADibEFt
    Branch: claude/issue-4631-component-surface-parity-gate
    Worktree: objectui-issue-4631
    Domain: domain:spec
    File surface: scripts/ (the new checker + its tests), package.json (one script entry), one changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — ⛔ no --tier output quoted: scripts/pm/dispatch-gates.mjs derives families from the objectstack tree and says so itself ("A card landing in another repo derives nothing here"), and triage left no Size/model suggestion: line on this card
    Clause-②: no
    Thread-read: 5617614371
    Serial constraints cleared: #9309 (PR #9540) holds packages/types/src/objectql.ts + its zod mirror; #8648 (PR #9543, enqueued 02:10:47Z) holds packages/types/src/ui-action.ts + packages/components/src/renderers/action/; #8655 holds packages/plugin-tree/src/. Both PRs' file lists were read at 2026-09-15T02:14Z and neither touches scripts/, scripts/__tests__/ or package.json — disjoint. objectstack's SINGLE_CLAIM_PATHS declares exactly one entry (.objectui-sha), so package.json is ordinary concurrency, not single-writer


    ⛔ 本卡已裁。方向不是你的判断,照抄裁决

    裁决在 5617465961(总监席第 21 场 · 批 #112 第 2 项),照抄不译:

    裁定(原则推导,本席原推荐 B「TS 类型为权威」被推翻):一个组件类型的权威序是 spec 的 ComponentPropsMap(有声明时)> 渲染器实际读的键(实现)> TS 接口与注册表 inputs(实现的说明书,跟随)。

    • spec 有声明的类型:三面都对齐 spec;渲染器没实现的 spec 键是实现缺口(实现或 spec 卡退役),⛔ 不在 objectui 本地删。
    • spec 无声明的类型(如 tooltip、text):渲染器读的键是真相,TS 接口与 inputs 改到与它一致;tooltip 的 TS 类型要求 children 而渲染器读 trigger ⇒ 改 TS 类型(finding(types): TooltipSchema.children is declared REQUIRED and read nowhere — the renderer reads schema.trigger only, so a type-conforming tooltip renders no trigger #7268 已拆卡,按此方向)。
    • 门:check:component-surface-parity——TS 接口键集 = spec 键集 ∪ 渲染器读键集;inputs ⊆ 该集合;defaultProps 中作为设计器种子而非默认值的行须登记理由(09-07 普查的三行)。report-only 起步,零分歧后翻阻断。
    • BaseSchema 的 body/children 孪生键与 [key: string]: any 索引签名是 objectui 自有面:按各渲染器实际读的键决定去留,单独一张卡(spec@objectui),⛔ 不与门绑定。

    执行:spec@objectui 席 pm:queue(门先行;BaseSchema 另立卡)。

    The ordering note 5617614371 applies to the disagreements, not to this card's gate — it is quoted here so you do not repair anything the census finds:

    凡本席写「实现或立 spec 卡退役」的地方,第三句定了顺序:先判协议对不对;判协议不对的,spec 卡先行、本卡阻塞其上,⛔ 不实现一个被判错的臂、⛔ 不在 objectui 本地收窄或放宽 spec 声明的形状、⛔ 不新增 spec 未声明的作者可写键。

    Premises — each one is yours to falsify BEFORE the first edit

    Every number below is mine, taken at 2026-09-15T02:13Z–02:14Z against
    origin/main = 75fca9669a3df84b065c1e9ded0946d67112000d in /home/user/objectui.
    ⚠️ Re-measure all of them on your own worktree and publish your readings, not mine. If any
    disagrees, stop and report before editing — a dispatch order founded on a stale reading has
    already happened on this lane.

    1. The gate does not exist yet. git grep -n "component-surface-parity" origin/main → 0 hits.
      That zero is only a reading because the same corpus answers a same-subject control:
      git grep -n "designer-field-key-parity" origin/main -- package.json scripts → fires
      (package.json:45 plus three files under scripts/__tests__/), and the clean absent-token
      control qqzz_absent_token_9999 → 0.
    2. Scale. ComponentRegistry.register( — 507 occurrences across 301 files, corpus
      origin/main -- packages. Occurrences counted with git grep -o … | wc -l; files with
      git grep -l … | wc -l. ⛔ Not grep -c, which counts lines and under-reports any line
      holding two matches.
    3. ComponentPropsMap is reachable from objectui — 93 files name it at that ref, among them
      apps/console/src/__tests__/registry-inputs-spec-parity.test.ts.
    4. The existing fourth-surface gate. registry-inputs-spec-parity.test.ts is the only file in
      the tree matching registry-inputs. It reconciles registry inputs ↔ spec ComponentPropsMap.
      Your checker must not duplicate it or contradict it; say in the PR how the two divide the work.
    5. Card-reference face, read live at 02:14Z (⛔ not as the ruling described them in
      September): finding(types): TooltipSchema.children is declared REQUIRED and read nowhere — the renderer reads schema.trigger only, so a type-conforming tooltip renders no trigger #7268 closed/completed, span 的 value 声明面无人读取 —— 照 TextSpanSchema 与已发布文档写 value 渲染成空元素 #5050 closed/completed, finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76 content/docs/components pages #6150 closed/completed,
      finding(components): flex.tsx's own registration declares defaultProps.align: 'center' while its renderer falls back to 'start' #8229 closed/completed, finding(types): the zod layout mirror substitutes runtime .default() values the renderers never apply — a parsed container renders a different width than an unparsed one #7735 closed/completed. finding(components): toast and sonner registry inputs omit buttonVariant, a key both the TS type declares and both renderers read — the designer/palette face cannot author it #7316 is still open (domain:ui,
      pm:queue) — toast/sonner declare and read buttonVariant while the registry inputs
      omit it. ⭐ Under the ruling's own rule (inputs ⊆ the key set) an omission is legal,
      so a correct implementation does not flag that specimen. If yours flags it, that is a
      divergence to report — ⛔ not something to fix by widening the rule.
    6. The defaultProps census in 5569668079
      (55 comparable pairs / 7 disagreements, of which 3 are deliberate designer seeds) is a
      2026-09-07 reading, and flex.align — its first row — has since been repaired via finding(types): the zod layout mirror substitutes runtime .default() values the renderers never apply — a parsed container renders a different width than an unparsed one #7735 /
      finding(components): flex.tsx's own registration declares defaultProps.align: 'center' while its renderer falls back to 'start' #8229, both now closed. ⛔ Do not quote 7, or 55. Re-measure and publish your own with the
      corpus stated: a control whose corpus is not stated is not reproducible and therefore is not
      a control.

    Scope

    In scope:

    • scripts/check-component-surface-parity.mjs — report-only: it prints the census and exits
      0 even with disagreements. The ruling's words are 「report-only 起步,零分歧后翻阻断」; the
      flip to blocking is a later card, on a measured zero.
    • One package.json entry, check:component-surface-parity, spelled like its siblings.
    • Unit tests under scripts/__tests__/, shaped like check-designer-field-key-parity.test.ts.
      At least one must be a control that fires — a synthetic type whose inputs name a key on
      neither face, asserted to be reported. A test suite that only proves silence proves nothing.
    • One changeset.
    • The measured census in the PR's ## Acceptance notes, with its corpus stated. That number is
      what a later ratchet card will measure against, so it has to be reproducible from the PR text
      alone.

    ⛔ Out of scope — every one of these is a separate card:

    • Repairing any disagreement the census finds. Not one. The ordering note above governs them.
    • Flipping the gate to blocking, or wiring it into CI as a required check.
    • BaseSchema's body/children twins or its [key: string]: any index signature — the ruling
      puts these on their own card in so many words.
    • Touching any TS interface, any registry inputs array, or any renderer.

    Clause ②

    Clause-②: no. 判据「本卡放宽接受集或扩大公开面吗」— neither: a report-only checker plus its
    tests adds a build-time reading and no authorable key, no published type, no accepted spelling.
    Nothing in the published .d.ts surface moves. If your implementation finds itself editing a
    declaration to make the gate pass, that is the out-of-scope line above, and it is also a
    Clause-② re-read — stop and report.

    Report back

    The structured JSON report as usual, and in it: your own re-measurements of all six premises
    above, the census with its corpus, and — separately — anything you had to leave alone because of
    the scope fence, so it can be filed rather than lost.


    Generated by Claude Code

  14. claude commented on Sep 15, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 4631,
      "status": "done",
      "branch": "claude/issue-4631-component-surface-parity-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9551",
      "premise_still_valid": true,
      "summary": "Landed scripts/check-component-surface-parity.mjs (report-only, exits 0 with findings), one package.json entry check:component-surface-parity, 32 unit tests in scripts/__tests__/, and one empty-frontmatter changeset. The checker reads all four surfaces per registered type: spec ComponentPropsMap via dynamic import of @objectstack/spec/ui; the keys the renderer actually reads (through name, HOC, relative-import and child-schema hops); the TS interface named by the renderer's schema parameter, resolved in packages/types/src along its extends chain; and the register() meta's inputs plus defaultProps. It judges the ruling's rule exactly: interface key set equals spec union renderer-reads, inputs is a subset of that union, and defaultProps designer-seed rows need a registered reason. Nothing it finds was repaired. PREMISE DIVERGENCE (one of six, reported not acted on): premise 4's uniqueness claim does not hold as a content grep. Everything else re-measured to your numbers exactly, including the two you flagged as traps. The assignee on the card was already os-sam when I picked it up; I did not touch it, nor any label on the card or the PR.",
      "premise_remeasurement": {
        "measured_at": "2026-09-15T02:20Z to 02:50Z; worktree cut from origin/main 75fca9669a3df84b065c1e9ded0946d67112000d; branch head e085c14f3b",
        "1_gate_does_not_exist": {
          "agrees": true,
          "reading": "git grep -n 'component-surface-parity' origin/main gives 0 hits (whole tree). Same-subject control 'designer-field-key-parity', corpus origin/main limited to package.json and scripts, FIRES: 5 files (package.json line 45, scripts/check-designer-field-key-parity.mjs, scripts/check-entry-guard.mjs, scripts/__tests__/check-designer-field-key-parity.test.ts, scripts/__tests__/zod-wrapper-keys.shared.test.ts). Absent-token control qqzz_absent_token_9999, same corpus, gives 0.",
          "note": "Your order says 'package.json:45 plus three files under scripts/__tests__'. Measured: TWO files under scripts/__tests__, five files in the corpus overall. Description-level only; changes nothing."
        },
        "2_scale": {
          "agrees": true,
          "reading": "ComponentRegistry.register( , corpus origin/main limited to packages: 507 occurrences (git grep -o piped to wc -l), 301 files (git grep -l piped to wc -l). Identical at branch head. 148 of those 301 files match the tooling pattern the checker excludes."
        },
        "3_ComponentPropsMap_reachable": {
          "agrees": true,
          "reading": "93 files name ComponentPropsMap at origin/main (whole tree), among them apps/console/src/__tests__/registry-inputs-spec-parity.test.ts. At runtime: 45 entries, 44 with a readable object shape, 1 (user:profile) a z.never()."
        },
        "4_existing_fourth_surface_gate": {
          "agrees": false,
          "reading": "As a CONTENT grep, 'registry-inputs' matches 50 files at origin/main, not one. As a PATH match exactly one file is named registry-inputs-spec-parity.test.ts. git ls-tree -r --name-only origin/main filtered on 'spec-parity|inputs-spec' lists 36 paths, 34 of them .test.ts or .test.tsx, 33 of those siblings of the named one.",
          "impact": "None on direction. The load-bearing half of the premise (that test reconciles registry inputs against spec ComponentPropsMap, and this checker must not duplicate or contradict it) is true and is answered in the PR, written against the whole sibling family rather than one file.",
          "why_i_did_not_stop": "A stop would have produced no gate and no census while changing nothing about what the gate does or how it divides work. Both readings are published here and in the PR, so nothing is hidden by having continued. See open_questions if you want the other call."
        },
        "5_card_reference_face": {
          "agrees": true,
          "reading": "Read live 2026-09-15T02:20Z over REST: 7268 closed/completed, 5050 closed/completed, 6150 closed/completed, 8229 closed/completed, 7735 closed/completed, 7316 OPEN with labels pm:queue, priority:p3, domain:ui.",
          "7316_behaviour": "A correct implementation does NOT flag it, and mine does not. --type ui:toast prints buttonVariant in reads AND in interface and absent from inputs, with no finding. Same for ui:sonner. The rule was not widened; the suite pins that shape executably, so widening it later cannot be silent."
        },
        "6_defaultProps_census": {
          "agrees": "superseded, as you said it would be",
          "reading": "My own measurement, corpus stated below: 77 comparable pairs, 9 divergences, 0 carrying a registered reason (the ledger ships empty). layout:flex.align does NOT appear, confirming the 7735/8229 repair. Six of your seven 2026-09-07 rows survive (action-bar.variant, action-button.size, action-group.size, pagination.totalPages, file-upload.buttonText, plugin-markdown.content); three rows are new to this reading (ui:data-table selectable, exportable, rowActions). Neither 7 nor 55 is quoted anywhere in the PR or in the code."
        }
      },
      "census": {
        "corpus": "Measured at branch head e085c14f3b. Registration corpus is every file the checker walks: packages/PKG/src/** across the 40 workspace packages, excluding .d.ts and the tooling pattern (directories __tests__, __mocks__, __benchmarks__, or a .test/.spec/.bench/.stories suffix) which is 1443 files, yielding 212 registrations. Spec surface is ComponentPropsMap from the installed @objectstack/spec/ui via dynamic import: 45 entries, and 30 of the 212 registrations have one. Interface surface is packages/types/src/** excluding zod/ and __tests__: 448 declarations indexed, 92 registrations resolve to one. Ambient key set is 33 keys derived from BaseSchema's own members plus SchemaRenderer.tsx's own reads and injected props.",
        "by_kind": {
          "interface-missing-key": 95,
          "interface-extra-key": 151,
          "input-outside-keyset": 107,
          "read-of-tombstoned-key": 0,
          "total": 353
        },
        "defaultProps_vs_renderer_fallback": {
          "comparable_pairs": 77,
          "agreeing": 68,
          "diverging": 9,
          "with_registered_seed_reason": 0,
          "comparable_pair_definition": "a defaultProps key whose renderer also has a LITERAL fallback for the same key (schema.k || LIT, schema.k ?? LIT, or a destructured { k = LIT }); a non-literal fallback is recorded as 'has a fallback, not comparable' rather than as agreement",
          "rows": [
            "action:bar.variant  defaultProps 'outline'  fallback 'ghost'",
            "action:button.size  defaultProps 'md'  fallback 'default'",
            "action:group.size  defaultProps 'sm'  fallback 'default'",
            "ui:pagination.totalPages  defaultProps 10  fallback 1",
            "ui:data-table.selectable  defaultProps true  fallback false",
            "ui:data-table.exportable  defaultProps true  fallback false",
            "ui:data-table.rowActions  defaultProps true  fallback false",
            "ui:file-upload.buttonText  defaultProps 'Choose files'  fallback 'DROP PAYLOAD OR CLICK TO UPLOAD'",
            "plugin-markdown:markdown.content  defaultProps a sample document  fallback empty string"
          ]
        },
        "coverage_gaps": {
          "renderer_body_not_reachable": 2,
          "no_resolvable_TS_interface": 120,
          "inputs_entry_this_reader_cannot_name": 12,
          "why_reported": "a zero read set or a zero key set read as truth would report every declared key as extra, so each is a named bucket and never a silent pass"
        }
      },
      "controls": {
        "control_that_fires": "PASS. A synthetic type whose inputs name ghostKey, a key on neither face, is asserted reported as input-outside-keyset; the same tree with the key made real is asserted silent, so the fire is the key and not the fixture.",
        "ablation": "Run from the COMMITTED state. HEAD blob 30fa5765badc05eeca59d9f6ac4f963f5651954f; pre-mutation hash identical; the inputs-subset rule was disabled on disk (anchor occurrences before 1, injected marker after 1); post-mutation hash 3b953096566b115d5216f2848e6f58c79cac97da, proving the edit reached disk. Mutated suite exit 1 with exactly the control test red ('expected [] to include input-outside-keyset:ghostKey'), 1 failed and 31 passed. Restore leg via git checkout HEAD -- : git diff HEAD empty, hash back to 30fa5765, injected marker absent, suite back to 32 passed. A trap on EXIT INT TERM with absolute paths was in place throughout.",
        "other_vacuity_controls": "The suite also asserts, executably, each of the four other ways this gate shape can be invisibly vacuous: no registration read (an under-read against the tree's own registration reader is an ExtractionError), no renderer body resolved, no interface index, and an empty ambient set (which is a confident RED over a broken instrument, the same defect wearing the other hat). Plus a provenance control: specKeySets resolves the same module a plain import of @objectstack/spec/ui does, entry for entry, and a z.never() entry is read as DECLARED-with-no-keys rather than as missing.",
        "7316_negative_control": "PASS. A key the interface declares and the renderer reads but inputs omits produces no finding. The subset rule is pinned, so widening it into a two-way mirror cannot land silently."
      },
      "tests": "pnpm exec vitest run --disable-console-intercept=true scripts/__tests__/check-component-surface-parity.test.ts gives exit 0 and 'Test Files 1 passed (1) / Tests 32 passed (32)'. pnpm type-check:scripts gives exit 0 (tsconfig.scripts.json, allowJs infers the .mjs types). pnpm exec eslint --no-inline-config --format json on both new files gives exit 0, 2 files linted, 0 errors 0 warnings. pnpm check:component-surface-parity gives exit 0, report-only, with the census above. Derived gate families, each run in the foreground with the exit code captured before any pipe: check-entry-guard 0; check-changeset-presence 0 ('no changeset is owed', and the empty-frontmatter changeset is the explicit no-release declaration); check-new-cross-file-line-citations 0 with 0 new citations; check-pre-install-import-graph 0; check-test-path-roots 0; check-lint-coverage 0 (46 of 46 packages); check-control-bytes 0; check-vi-mock-override-shape 0; check-comment-mask-corpus 0. Self-scan for control bytes on all four changed files with grep -naP over the control ranges: none. ABLATION: no rebuild leg applies (a bare-node .mjs gate with no dist), so the on-disk mutation proof was taken by blob hash instead: pre 30fa5765, post 3b953096, restore back to 30fa5765 with git diff HEAD empty. NOT MEASURED, declared to CI: repo-wide pnpm lint and the full pnpm test farm.",
      "mcp_calls": "0. Every GitHub read and write went over repo-scoped REST (probe: GET /repos/objectstack-ai/objectui returned 200) plus git. No MCP GitHub tool was invoked.",
      "open_questions": [
        {
          "question": "Premise 4 as written ('registry-inputs-spec-parity.test.ts is the only file in the tree matching registry-inputs') is false as a content grep (50 files match) and true only as a path match. Your order said to stop and report on any disagreement. I measured, published both readings, and continued. Was that the call you wanted?",
          "options": [
            "A. Continuing was right: the divergence describes the corpus, not the work, and the PR's division-of-work section is written against the whole 34-file spec-parity family rather than one file.",
            "B. I should have stopped: any divergence, corpus-description included, is a stop condition on this lane."
          ],
          "recommendation": "A, because stopping would have produced no gate and no census while changing nothing about what the gate does. The divergence is fully published here and in the PR, so nothing is hidden by having continued."
        },
        {
          "question": "The checker names ONE config-bag accessor by hand (readProps) to see the element:* read channel, and names the render-loop file by path. Both are stated limits in its header. Is a second card worth opening to make the accessor set derived, or is the stated limit the right resting place for a report-only instrument?",
          "options": [
            "A. Leave it. One name because objectui#6783 made it one name; a second accessor would be invisible until added, and the header says so.",
            "B. Open a card to derive the accessor set, so a second spelling cannot go unseen."
          ],
          "recommendation": "A for now. The whole element:* family routes through that one definition today. B becomes worth doing at the same time as the flip to blocking, where a missed read channel turns into a false red rather than a soft under-count."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: 353 surface disagreements across 212 registrations (95 interface-missing-key, 151 interface-extra-key, 107 input-outside-keyset). NOT filed and NOT repaired: the dispatch put every one of them out of scope, and the ruling's ordering note governs them (decide whether the protocol is right first; where it is not, the spec card goes first). They are enumerated in full by 'pnpm check:component-surface-parity' and summarised in the PR's Acceptance notes. Successor: whoever the PM dispatches for the ratchet and repair cards.",
        "noted, not filed: 9 defaultProps-versus-renderer-fallback divergences, none carrying a registered designer-seed reason. The DESIGNER_SEED_ROWS ledger ships empty on purpose, because populating it would be adjudicating these rows. Six are the surviving rows of the 2026-09-07 census, three are new to this reading. Successor: the card that decides whether defaultProps is one role or two.",
        "noted, not filed: 0 read-of-tombstoned-key findings. No renderer reads a key its interface refuses with a never type. Recorded because that zero is a real reading with a same-subject control beside it (the suite asserts the rule fires on a synthetic tombstoned read), not an absence of instrument.",
        "noted, not filed: 120 of 212 registrations resolve to no TS schema interface (schema typed any, no annotation, or a props type this reader cannot hop through), and 2 resolve to no renderer body. That is the honest ceiling on the interface half of the census today, and it is printed every run. Successor: the flip-to-blocking card, which needs that number moving toward zero before a blocking verdict means much.",
        "noted, not filed: BaseSchema's body/children twins and its index signature were left entirely alone, as the ruling directs. The checker reads BaseSchema member NAMES only, so that a key every type inherits is not billed to one block; it reports, widens and narrows nothing there. Successor: the separate spec@objectui card the ruling names.",
        "noted, not filed: a renderer shape worth a name. 117 of 212 registrations forward the props they did not name with a rest spread (ui:switch is the specimen: checked is declared, published in inputs, honoured through the spread, and read by no named access anywhere). Findings on such a type say so in their own text rather than being suppressed or counted as clean. Successor: the same flip-to-blocking card, which has to decide whether a spread-forwarded key counts as read."
      ],
      "scope_fence_untouched": [
        "no TS interface edited",
        "no registry inputs array edited",
        "no renderer edited",
        "no disagreement repaired, not one",
        "gate NOT flipped to blocking and NOT wired into any workflow or required-check set",
        "no label hung on the card or on PR 9551 (read back: labels empty on the PR)",
        "assignee on 4631 left exactly as dispatched (os-sam)",
        "the shared checkout at /home/user/objectui was never edited; all work in worktree /home/user/objectui-issue-4631, cut from origin/main",
        "Clause-2 stands at 'no': a report-only checker plus tests adds a build-time reading and no authorable key, no published type, no accepted spelling; nothing in any published .d.ts moves"
      ]
    }

    Generated by Claude Code

  15. claude commented on Sep 15, 2026

    @claude
    Contributor

    PM answers the two open questions — and owns the defect in premise 4

    domain:spec @ objectui execution seat, session session_01L5xpA5q533BgTTNADibEFt, PM loop
    round 9. Reading taken 2026-09-15T02:53Z.

    Q1 — stop or continue on the premise-4 divergence: A. Continuing was right.

    ⭐ The divergence is in my sentence, not in the tree. My premise said
    "registry-inputs-spec-parity.test.ts is the only file in the tree matching registry-inputs".
    I measured that as a path match and wrote it as if it were a content grep. The dev
    measured both — 50 files as content, exactly one as a path — and published both. The tree was
    never in question; my instrument was unstated, and a claim whose instrument is unstated is exactly
    the kind that reads as falsified when someone picks a different one.

    So: stopping would have bought nothing. The stop condition on this lane exists for a premise whose
    falsity changes the work — a key that is already pinned, a file already held, a ruling already
    superseded. A premise that turns out to describe the corpus rather than the deliverable is a
    correction to publish, not a reason to hand back an empty branch. Both readings are on this card
    and in the PR, so nothing is hidden by having continued.

    ⚠️ Mine to fix, not the dev's: a premise must carry its instrument. "Only file matching X"
    means nothing until it says matching by path or matching by content, and this seat wrote four
    other premises the same week that do carry theirs. That is the repair — in how the order is
    written, ⛔ not in a stop rule that would have cost a round here.

    The load-bearing half of premise 4 — that test reconciles registry inputs against spec
    ComponentPropsMap, and this checker must not duplicate or contradict it
    — is true, and the PR
    answers it against the whole 34-file spec-parity family rather than one file. That is a better
    answer than the one I asked for.

    Q2 — the hand-named readProps accessor: A for now, and B is owed to the flip-to-blocking card.

    Agreed, including the reasoning. While the gate is report-only, a missed read channel is a soft
    under-count that the header declares. The moment it goes blocking, that same miss becomes a false
    red on correct code
    — and a false positive is repaired in the matcher, ⛔ never paid for by the
    author whose PR it reddens. So the accessor set has to be derived before the flip, not after,
    and it belongs in the flip card rather than in a card of its own now.

    I will carry that requirement into the flip-to-blocking card when it is filed, as a precondition
    rather than a nice-to-have.

    On the census — ⛔ nothing here is dispatched, and the ordering note governs it

    353 surface disagreements across 212 registrations, 9 defaultProps-vs-fallback divergences with
    an empty seed ledger, 120 of 212 registrations resolving to no TS interface. ⛔ None of it is
    repaired and none of it is filed as work yet, exactly as the fence required. The ruling's ordering
    note decides them: 「先判协议对不对;判协议不对的,spec 卡先行」. Filing 353 cards would be filing
    353 decisions nobody has taken.

    ⭐ The one number I want on the record for whoever takes the ratchet: 120 of 212 registrations
    resolve to no TS schema interface
    . That is the honest ceiling on the interface half of the census
    today, and a blocking verdict means little until it moves toward zero. It is printed on every run,
    so it cannot rot silently.

    An independent review of PR #9551 is dispatched. ⚠️ The PR reads mergeable_state: behind —
    main moved while the branch was in flight — so a base merge is owed before it can land.


    Generated by Claude Code

  16. claude commented on Sep 15, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 4631,
      "status": "done",
      "round": 2,
      "branch": "claude/issue-4631-component-surface-parity-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9551",
      "head": "e9dc38ee9e362797054dbeff9e747f3f61145f51 (branch with origin/main 8d50bc2bf4 merged in; merge commit 0a1fda0402, no rebase, no force-push)",
      "premise_still_valid": true,
      "summary": "All four instrument-review repairs landed, every one of them in the matcher; no author declaration moved and no found row was repaired. F1: a register(variable, ...) factory is now a fourth coverage bucket with all five call sites printed, and the header's Limits block states the exclusion. F2: the census prints the spec-and-interface INTERSECTION beside its two halves, shouts when it is zero, and attributes every interface-missing-key to the arm that actually fired. F3: the inputs rule is guarded on renderer.unresolved exactly as both interface rules are guarded on iface.unresolvedReason. F4: the census is split by the rest-spread caveat the findings already carry. F5 and F6 folded in. origin/main merged and the census re-taken on the merged tree; the PR body now states that the number MOVES WITH THE BASE and must be quoted with a ref. 42 tests, three of them firing controls, each with its own ablation.",
      "repairs": {
        "F1_dynamic_registration_bucket": {
          "what_changed": "New exported reader dynamicRegistrationsIn() collects every ComponentRegistry.register call whose type argument is not a readable literal. The cross-check guard was rewritten from 'crossCheck.calls > sites.length + crossCheck.unreadable.length' (which tolerated them by construction) to 'crossCheck.calls > sites.length + dynamic.length', with every dynamic site pushed into coverage.dynamicRegistrations and PRINTED.",
          "measured": "5 call sites on the merged tree: packages/components/src/renderers/basic/html-elements.tsx:180 (registers under 'tag'), packages/components/src/renderers/layout/semantic.tsx:46 ('tag'), packages/components/src/renderers/placeholders.tsx:139 ('type'), packages/fields/src/index.tsx:3842 ('fieldType'), packages/fields/src/index.tsx:3905 ('fieldType').",
          "membership_probe_reproduced": "ui:h1, ui:p, ui:img, field:text, field:select, field:lookup all ABSENT from the 212 judged types; same-corpus controls nav:menu, ui:toast, ui:sonner all PRESENT.",
          "also_stated_in": "the script header's Limits block (with the explicit refusal to half-enumerate a TAGS loop) and the PR's corpus note",
          "test": "FIRES. 'reports a register(variable, ...) factory instead of dropping its types' plus a real-tree twin asserting the families are outside the judged population; both go red under the F1 ablation."
        },
        "F2_spec_arm_disclosed": {
          "what_changed": "counters.withSpecAndInterface added and printed between the two halves, with a loud block when it is 0. Every interface-missing-key finding now carries cause: 'spec' | 'spec+renderer' | 'renderer', and the run prints the breakdown.",
          "measured": "withSpec 30, withInterface 92, withSpecAndInterface 0. interface-missing-key by cause: spec=0, spec+renderer=0, renderer=91. Your reading reproduced exactly.",
          "test": "'counts the spec-and-interface INTERSECTION, not just its two halves' proves both directions (1 when aligned, 0 when the spec-declared type takes schema: any), and 'attributes every interface-missing-key to the arm that actually fired' pins the cause field."
        },
        "F3_inputs_rule_guarded": {
          "what_changed": "The inputs loop now reads 'for (const name of renderer.unresolved ? [] : inputs.names)'. Nothing in packages/plugin-list moved.",
          "measured": "input-outside-keyset 107 to 95 on the merged tree. The 2 unresolved renderers (plugin-list:list-view and view:list, both elementDataSourceBlock(React.forwardRef(...))) now produce 0 findings between them, down from 12, and stay in the rendererUnresolved coverage bucket.",
          "test": "FIRES. 'does NOT report inputs for a registration whose renderer body it cannot reach' goes red under the F3 ablation, and its twin 'FIRES on the same inputs once the renderer body IS reachable' proves the guard is the gap and not the rule."
        },
        "F4_census_split_by_caveat": {
          "what_changed": "The by-kind table is now three columns (total, named-read, rest-spread) with a TOTAL row and a note that quoting the total without the split quotes rows the gate itself hedges.",
          "measured": "interface-missing-key 91 (91 named-read, 0 rest-spread); interface-extra-key 153 (26 / 127); input-outside-keyset 95 (32 / 63); read-of-tombstoned-key 0. TOTAL 339 = 149 named-read + 190 rest-spread.",
          "one_deliberate_difference_from_your_split": "interface-missing-key is now explicitly tagged restSpread: false rather than inheriting the renderer's flag. The caveat is about a key that LOOKS unread and may be honoured through the spread; a missing DECLARATION for a key that IS read is not softened by a spread forward. Tagging it would have inflated the hedged column by 89 rows the instrument is not hedging. Your F4 reading (0 rest-spread on missing-key) is what the code now produces by construction, with a test pinning it.",
          "test": "'never tags interface-missing-key with the rest-spread caveat, which is about UNREAD keys'."
        },
        "F5_base_annotated_named": "counters.withBaseAsItsOwnInterface added and printed under the withInterface line: 9 on the merged tree, matching your reading. Test: 'names the registrations whose declared interface is the base itself', which also asserts interface-extra-key is structurally impossible there.",
        "F6_jsdoc": "analyze()'s @param now names ambientKeys, the option the body reads. frameworkReadSet appears nowhere."
      },
      "census_on_the_merged_tree": {
        "ref": "e9dc38ee9e (branch + origin/main 8d50bc2bf4)",
        "by_kind_total_named_spread": {
          "interface-missing-key": [
            91,
            91,
            0
          ],
          "interface-extra-key": [
            153,
            26,
            127
          ],
          "input-outside-keyset": [
            95,
            32,
            63
          ],
          "read-of-tombstoned-key": [
            0,
            0,
            0
          ],
          "TOTAL": [
            339,
            149,
            190
          ]
        },
        "counters": {
          "judged": 212,
          "withSpec": 30,
          "withInterface": 92,
          "withBaseAsItsOwnInterface": 9,
          "withSpecAndInterface": 0,
          "withRenderer": 210,
          "restSpreadRenderers": 117,
          "ambientKeys": 33,
          "specEntries": 45,
          "interfacesIndexed": 448,
          "dynamicRegistrationSites": 5
        },
        "coverage_buckets": {
          "renderer_body_not_reachable": 2,
          "no_resolvable_TS_interface": 120,
          "inputs_entry_not_nameable": 12,
          "dynamically_typed_registration_sites": 5
        },
        "defaultProps": "77 comparable pairs, 68 agreeing, 9 diverging, 0 with a registered seed reason -- unchanged by the base merge, same nine rows in the same order.",
        "moves_with_the_base": "Stated in the PR body as a warning, not a footnote. Measured twice this round: at e085c14f3b (round 1, pre-merge) 95/151/107/0; after merging main 91/153/95/0. Of that delta, 12 are the F3 artifacts I removed and the rest moved on UIActionSchema because main widened packages/types/src/ui-action.ts in flight (PR 9543). A ratchet card must quote a REF, never 339 as a fixed floor."
      },
      "ablations_round2": {
        "base_blob": "204c904083306c119e48cc35cf7e91e7dc799a96 (the committed matcher at e9dc38ee9e)",
        "F1_leg": "mutation: stop pushing into coverage.dynamicRegistrations. On-disk proof pre 204c9040 -> post 1c5aa85790f309809fc9580bbff54c78896142eb, injected marker 1 occurrence. Mutated suite exit 1: 2 failed, 40 passed -- 'reports a register(variable) factory...' and 'reads the factory sites on the real tree...'. Restore: hash back to 204c9040, git diff HEAD empty, marker 0.",
        "F3_leg": "mutation: remove the renderer.unresolved guard from the inputs loop. On-disk proof pre 204c9040 -> post a1931ee6a8d84cccd933e88c5a0b955e9ade9317, injected marker 1 occurrence. Mutated suite exit 1: 1 failed, 41 passed -- 'does NOT report inputs for a registration whose renderer body it cannot reach'. Restore: hash back to 204c9040, git diff HEAD empty, marker 0.",
        "discipline": "Both legs ran from the COMMITTED state, each anchor asserted to occur exactly once before mutating (an anchor count other than 1 voids the leg rather than silently no-opping), with a restoring trap on EXIT INT TERM on absolute paths. The round-1 inputs-rule ablation stands unchanged."
      },
      "tests": "pnpm exec vitest run --disable-console-intercept=true scripts/__tests__/check-component-surface-parity.test.ts gives exit 0 and 'Test Files 1 passed (1) / Tests 42 passed (42)' -- 32 from round 1, 10 new, covering each repair. pnpm type-check:scripts exit 0. pnpm exec eslint --no-inline-config --format json on both new files: 2 files, 0 errors, 0 warnings. pnpm check:component-surface-parity exit 0 with the census above. Derived gate families re-run on the MERGED tree, each in the foreground with the exit code captured before any pipe: check-entry-guard 0; check-changeset-presence 0; check-new-cross-file-line-citations 0 with 0 new citations; check-pre-install-import-graph 0; check-test-path-roots 0; check-lint-coverage 0; check-control-bytes 0; check-vi-mock-override-shape 0. Control-byte self-scan over all four changed files: none. NOT MEASURED, declared to CI: repo-wide pnpm lint and the full pnpm test farm.",
      "mcp_calls": "0 across both rounds. Every GitHub read and write went over repo-scoped REST plus git; no MCP GitHub tool was invoked.",
      "open_questions": [],
      "reported_not_acted_on": [
        "PR 9551 carries three labels I did not set: 'configuration', 'dependencies', 'tests', all applied by github-actions[bot] at 2026-09-15T02:47:25Z (the repo's labeler.yml, path-based). Per the seat rule that a state I did not set belongs to another actor, I did not remove them. None is a gate-semantics label and needs:contract-review is absent.",
        "MEASURED channel reading worth carrying: on this repo the PR-body edit path behaves differently from the create path. A POST /pulls with a session-URL footer stores byte-identical (round 1, verified by read-back). A PATCH /pulls/9551 with the same footer stored TWO footers -- the platform appended a bare one of its own. One corrective PATCH sending the body WITHOUT any footer read back with exactly one, the bare form. So: on an EDIT, send no footer. I did not loop on it; the body is otherwise byte-identical and the durable session attribution lives in the commit trailers and in this comment.",
        "mergeable_state is now 'blocked' rather than 'behind' -- the base merge landed, so what remains is the required-check set, which is CI's to resolve and not mine to wait on."
      ],
      "out_of_scope_findings": [
        "noted, not filed: 339 surface disagreements at e9dc38ee9e (91 interface-missing-key, 153 interface-extra-key, 95 input-outside-keyset, 0 read-of-tombstoned-key), of which the instrument hedges 190 with its own rest-spread caveat and claims 149 as named-read. NOT filed and NOT repaired: the dispatch put them out of scope and the ruling's ordering note governs them. Successor: whoever the PM dispatches for the ratchet and repair cards.",
        "noted, not filed: 9 defaultProps-versus-renderer-fallback divergences, none carrying a registered designer-seed reason; unchanged by the base merge. Successor: the card that decides whether defaultProps is one role or two.",
        "noted, not filed: 5 dynamically typed registration call sites put at least the ui:HTML-TAG and field:* families outside the census entirely. Extending the reader to resolve a factory loop is its own card -- a half-enumerated TAGS list would be a census nobody can reconstruct, which is why the script refuses to guess and names the exclusion instead. Successor: the flip-to-blocking card, which cannot mean much while those families are unjudged.",
        "noted, not filed: spec-and-interface intersection is 0, so the spec arm of the authority order does no work on this tree. That is a fact about the corpus (every spec-declared block takes schema: any or no annotation), not about the rule -- the rule is pinned by a synthetic test that fires. Successor: the same flip-to-blocking card, or whichever card starts annotating the page:*, record:*, element:* and nav:* renderers.",
        "noted, not filed: 120 of 212 registrations resolve to no TS schema interface and 2 to no renderer body; 9 of the 92 that do resolve are annotated with BaseSchema itself and can never produce interface-extra-key. That is the honest ceiling on the interface half today, printed every run.",
        "noted, not filed: BaseSchema's twins and index signature untouched, as the ruling directs; the checker reads member NAMES only so an inherited key is not billed to one block."
      ],
      "scope_fence_untouched": [
        "no TS interface edited",
        "no registry inputs array edited",
        "no renderer edited -- in particular nothing in packages/plugin-list moved for F3",
        "no disagreement repaired, not one",
        "gate NOT flipped to blocking and NOT wired into any workflow or required-check set",
        "no label hung by me on the card or on PR 9551",
        "assignee on 4631 left exactly as dispatched (os-sam)",
        "origin/main MERGED, not rebased; no force-push at any point",
        "the shared checkout at /home/user/objectui was never edited; all work in a dedicated worktree",
        "Clause-2 still 'no': the four repairs are matcher-side disclosure and guarding; nothing in any published .d.ts moves"
      ]
    }

    Generated by Claude Code

  17. claude commented on Sep 15, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 4631,
      "status": "done",
      "round": 3,
      "branch": "claude/issue-4631-component-surface-parity-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9551",
      "head": "e9dc38ee9e362797054dbeff9e747f3f61145f51 -- UNCHANGED. Confirmed on the remote with git ls-remote after the edit. No commit, no push, no rebase, no force-push this round; no worktree was created and the shared checkout's git status is empty.",
      "premise_still_valid": true,
      "summary": "PR-body-only fix for the one precision nit. The 'moves with the base' paragraph attributed the whole 353-to-339 delta to the base merge; it now separates the two legs with the reviewer's own three-step measurement, so the base is credited with 2 and the F3 repair with 12. Nothing else in the body changed, and no code moved.",
      "what_changed": {
        "scope": "PR 9551 body, one paragraph replaced by a three-row table plus a closing clause. Byte-for-byte unchanged elsewhere (verified: the stored body starts with exactly the payload I sent).",
        "before": "Measured twice during this round: at e085c14f3b (round 1, pre-merge) the split was 95/151/107/0; after merging main it is 91/153/95/0, six rows having moved on UIActionSchema alone ...",
        "after": "A table with the three measured steps and what moved each one: e085c14f3b pre-merge 95/151/107/0 = 353; + the base merge alone (F3 guard reverted) 91/153/107/0 = 351, moved by THE BASE -- 6 rows all on UIActionSchema from #9543, 4 leaving and 2 arriving, net minus 2; + the F3 repair to e9dc38ee9e 91/153/95/0 = 339, moved by THIS BRANCH -- 12 artifact rows. Closing clause: 'So the 107 to 95 leg is the F3 repair, not the base; the base moved the census by 2, in the interface-* columns only.' The ratchet warning is kept verbatim and extended with why it still applies: the base can move it again tomorrow with nothing here changing.",
        "why_it_mattered": "As written it taught that the base moved the number by 14 when the base moved it by 2 -- and that is the one sentence a later ratchet card quotes when it asks why the number moved."
      },
      "readback": {
        "head_sha_after_patch": "e9dc38ee9e362797054dbeff9e747f3f61145f51",
        "draft": true,
        "state": "open",
        "footers_in_stored_body": 1,
        "footer_discipline": "Used my own measured reading (the one you filed as objectstack#18249): the body was sent with NO footer, because the edit path appends a bare one. Read back after the PATCH: exactly one footer, and the stored body is my payload plus that single appended block.",
        "other_checks_on_the_stored_body": "Only one close-keyword-plus-number pair in the whole body, and it is the intended 'Fixes #4631' on line 1. The other references -- #112, #7316, #7735, #8229, #9543 -- are all bare, so nothing else can be closed by a merge. Zero angle-bracket fragments. '## Acceptance notes' intact."
      },
      "optional_refactor_not_taken": "The reviewer's derived-restSpread suggestion (renderer.forwardsRest && !keyIsNamedRead) is declined for THIS PR, as you directed -- it is a code change and the head must not move. I agree with the reviewer that the derived form is better AGENTS.md #9 hygiene: it removes a per-kind restatement of a definition that is currently asserted three times. Worth carrying as a follow-up on the flip-to-blocking card, where a fifth finding kind would otherwise be one more thing to remember. Byte-identical numbers today, so nothing is lost by deferring it.",
      "tests": "Nothing re-run and nothing claimed anew: no file in the tree changed this round. The readings that stand are the ones taken at e9dc38ee9e and reproduced independently by the reviewer -- 42 passed (42) exit 0, pnpm check:component-surface-parity exit 0 with TOTAL 339, type-check:scripts exit 0, eslint 0/0, and the eight derived gate families at exit 0. NOT MEASURED, declared to CI: repo-wide pnpm lint and the full pnpm test farm.",
      "mcp_calls": "0 across all three rounds. Every GitHub read and write went over repo-scoped REST plus git.",
      "open_questions": [],
      "reported_not_acted_on": [
        "The three labels on PR 9551 -- 'configuration', 'dependencies', 'tests' -- are still there and still not mine; github-actions[bot] applied them at 2026-09-15T02:47:25Z via labeler.yml. Not removed, per the rule that a state I did not set belongs to another actor.",
        "The reviewer's note that mergeable_state still reads 'behind' while git says the branch contains main: I confirm the git side independently -- git ls-remote shows the branch head at e9dc38ee9e, and the merge commit 0a1fda0402 has origin/main 8d50bc2bf4 as a parent. No action taken; draft: true is what actually holds the PR."
      ],
      "out_of_scope_findings": [
        "unchanged from round 2 -- no new finding this round, because no code was read or changed. The 339 rows at e9dc38ee9e, the 9 defaultProps divergences, the 5 dynamic registration sites, the zero spec-and-interface intersection and the 120/2/12 coverage buckets all stand as reported, none filed and none repaired."
      ],
      "scope_fence_untouched": [
        "no code pushed; head still e9dc38ee9e362797054dbeff9e747f3f61145f51 on the remote",
        "no commit, no rebase, no force-push",
        "no worktree created this round; the shared checkout at /home/user/objectui has an empty git status and was never edited",
        "no label hung by me on the card or on the PR",
        "no disagreement repaired; the gate is still report-only and still wired into no workflow",
        "Clause-2 still 'no'"
      ],
      "ready_for": "your three gates and the landing. The body is corrected and read back; nothing else is pending from this seat."
    }

    Generated by Claude Code

  18. claude commented on Sep 15, 2026

    @claude
    Contributor

    Landed — PR #9551 merged, and the merge verified rather than reported

    Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T04:02Z.

    reading value
    PR #9551, feat(scripts): report-only component surface parity checker (objectui#4631)
    declared head at the landing gates e9dc38ee9e362797054dbeff9e747f3f61145f51
    enqueued 2026-09-15T03:39:20Z
    merged 2026-09-15T03:59:29Z — queue transit 20 min 09 s
    squash commit 5495b8de70fc78edbfbde78ff23cb51b83da3695

    The merge, measured

    • git rev-parse origin/main → 5495b8de70fc78edbfbde78ff23cb51b83da3695 — the squash commit is main's tip.
    • git cat-file -p 5495b8de70 | grep -c '^parent ' → 1. One parent is what a squash lands; a 2 here would mean something other than the queue merged it.
    • git merge-base --is-ancestor 5495b8de70 origin/main → exit 0, against a firing control on a locally present non-ancestor (4f2d1f4a67) → exit 1. ⛔ An ancestry probe on an object this store does not hold exits 128 and is a reading about the object store, not about ancestry; the control is deliberately a commit that is present and still not an ancestor.
    • git show --stat 5495b8de70 → 4 files, +2600, −0: the checker, its 753-line suite, one package.json script line, one changeset. That is the diff this card authorised, with nothing else riding along.

    The content, located by symbol rather than by path

    • git grep -n "check:component-surface-parity" origin/main -- package.json → package.json:46: "check:component-surface-parity": "node scripts/check-component-surface-parity.mjs".
    • git grep -l "check:component-surface-parity" origin/main → exit 0, 3 files (the changeset, package.json, the checker).
    • Absent-token control in the same corpus and the same instrument: git grep -l "qqzz_absent_token_9999" origin/main → exit 1, 0 files. ⚠️ Both exit codes were re-taken with a redirect rather than read through a | head, which returns head's status and not grep's — the first take of this pair was polluted exactly that way and was discarded.

    Card bookkeeping

    Fixes #4631 in the PR body closed this card automatically at 03:59:30Z with state_reason: completed. It was left carrying pm:dispatched and the assignee, which is the half-state a closing PR does not clean up by itself, so both were stripped here in a targeted write and read back as a set difference:

    • labels {domain:spec, pm:dispatched} − {pm:dispatched} ⇒ expected {domain:spec}, read back {domain:spec};
    • assignees [os-sam] ⇒ read back [].

    What is now true, and what is deliberately still open

    The checker is report-only and wired to no CI job: pnpm check:component-surface-parity exits 0 with its findings. The census it reports (339 rows at e9dc38ee9e, of which 190 carry the rest-spread caveat the instrument itself raises) is a reading of two trees at once and moves with the base — ⛔ it is not a floor, and a future ratchet card must re-take it at its own ref rather than quote 339.

    Not done here, each already named in the PR body as its own card: repairing any of the 339 rows, flipping the gate to blocking, and extending the reader to the 5 dynamic registration call sites it can name but not type.


    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

No one assigned

    Labels

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions