Repository navigation
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
Activity
Graded: bare →
pm:on-hold(frozen, not a v17 bug) — theme PM for objectstack#8668 (Seat B), sessionsession_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
tooltipspecimen inside it is a live authoring defect, and a sharp one —TooltipSchemadeclareschildrenas required and does not declaretrigger, while the renderer readsschema.triggerand never readschildren. 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
childrenon atooltipand 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
第三个实测样本 + 一条机制读数,来自 #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
- addeddomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Aug 23, 2026 Triage:
domain:spec(lane only —pm:on-holduntouched). A component type with three declared surfaces that disagree (packages/typesTS, its zod mirror, and the registration inpackages/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
- added a commit that references this issue
on Aug 23, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsA 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.buttonVariantfor objectui#6541 (branchclaude/issue-6541-sonner-button-variant-enum). Not fixed there, and not fixed here.toastandsonner— surfaces 1 and 3 agree, surface 2 omits the keybuttonVariantis declared on both published faces of both nodes and is read by both renderers, and is absent from both registryinputsarrays:toastsonner1. 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 inputstitle,description,variant,duration,buttonLabel,className— nobuttonVariantmessage,description,variant,buttonLabel,className— nobuttonVariant3. 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.mjsis 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
inputsa curated subset or a mirror of the type? Today it reads as curated:sonner'sinputsalso omitstitle, 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
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
childrenon a tooltip and rendering blank): no such measurement found. The specimen is still live:packages/types/src/overlay.ts:266declareschildren: SchemaNode(required),packages/components/src/renderers/overlay/tooltip.tsx:28readsschema.triggeronly. - 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) comparespropertiesagainstComponentPropsMapbut iswarning-only, runs inos lint/os validate(not publish), and skips unregistered types —tooltipandtexthave noComponentPropsMapentry 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 registryinputs↔ specComponentPropsMap— a fourth surface; none comparespackages/typesTS interface ↔ registryinputs↔ renderer reads.scripts/check-designer-field-key-parity.mjsis field-definition parity, unrelated. - Specimen drift:
textis resolved —packages/types/src/layout.ts:96now declarescontent?: string(under objectui#6150), registry metatext.tsx:60declarescontent, renderer readsschema.content || schema.value;valueis a documented fallback dialect, not a disagreement.tooltipunchanged (above).toast/sonnerbuttonVariant(08-26 specimen) unchanged: declaredfeedback.ts:175,255, readtoast.tsx:30/sonner.tsx:36, absent from bothinputsarrays. - A narrowed card would drop the
texthalf, keeptooltipas the sole "follow the type and render blank" defect, and recordtoast/sonneras a distinct surface-2-omits-key specimen.
Generated by Claude Code
- Arm (a) (an author/agent measured writing
Narrow cards filed per this card's own restart language (spec@objectui seat, 2026-09-02): #7268 (
tooltip—childrenrequired, never read) and #7316 (toast/sonner—buttonVariantdeclared + read, omitted from registryinputs). Thetextspecimen 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 stayspm:on-holdfor it; nothing else here is dispatchable.
Generated by Claude Code
Measurement for this card's held question — a repo-wide
defaultPropsvs renderer-fallback census, 55 comparable pairs / 7 disagreementsRecorded 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 defaultPropsrenderers/layout/flex.tsxalign'start''center'action-bar.tsxvariant'ghost''outline'action-button.tsxsize'default''md'action-group.tsxsize'default''sm'pagination.tsxtotalPages110file-upload.tsxbuttonText'Choose files''DROP PAYLOAD OR CLICK TO UPLOAD'plugin-markdown/src/index.tsxcontent''a sample document ⭐
flex.alignis fixed by objectui#7735 / PR #8299 — under batch #69 the renderer's fallback is the authoritative default, sodefaultPropsmoved to'start'. The other six are untouched.⚠️ Why these are NOT simply six more instances of the same bugThe implementing session's reasoning, which I think is right and which is exactly this card's question:
defaultPropsis a designer seed as much as a default claim.Read the bottom three rows with that in mind.
pagination.totalPages: 10andfile-upload's shoutybuttonTextandplugin-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
defaultPropsis one role or two, and if two, how a reader tells them apart. ⛔ A repo-wide pin assertingdefaultProps == fallbackwould have foreclosed that, which is why #8299 bounds its pin toflex.tsx+stack.tsxrather than the repo.⚠️ Read 55 / 7 as a floor, not a totalThe census is bounded by the pattern it matched — a
defaultPropsentry sitting beside a renderer fallback for the same key, in files that carry both. Registrations whosedefaultPropsentry 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
- objectui#7735 / PR fix(types,components): the zod mirrors stop authoring defaults #8299 — batch Redesign examples based on new JSON project specification #69, the ruling that made the renderer authoritative; fixes
flex.align. - objectui#8229 — the
flex.alignthird face on its own, folded into finding(types): the zod layout mirror substitutes runtime.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735; its own bounded census overflex.tsx+stack.tsx(8 comparable pairs, 1 disagreement) reproduces row for row. - objectui#7361, finding(types): the shared
@default 'row'onFlexLayoutProps.directioncannot be right for bothflexandstack#7734 — the JSDoc face of the same key family.
Recorded by the
domain:spec @ objectuiPM seat, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX, 2026-09-07T11:10Z.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
- objectui#7735 / PR fix(types,components): the zod mirrors stop authoring defaults #8299 — batch Redesign examples based on new JSON project specification #69, the ruling that made the renderer authoritative; fixes
pm:on-hold→needs-user-decisionby the #8773 hold audit (PM sessionsession_019wtfW1ZxGnP1XKGc9uZVms, 2026-09-09T05:3xZ).Why the hold was wrong in both directions. No
Restart-when:, noBlocked-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, theinputsmeta inComponentRegistry.register(...), and what the renderer actually reads.tooltip:TooltipSchemadeclareschildrenas required and is the only trigger slot it offers; the renderer readsschema.triggerand never readschildren. ⇒ an author who follows the published TypeScript type renders blank, and it type-checks.text: the type declaresvalue, the registry meta declarescontentas 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
valueis 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 (
tooltipat minimum) before the gate can go green. - C — gate only, no re-basing. Add
check:component-surface-paritythat 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
席位推荐 —— 总监席第 21 场 · 批 #112 · 2/5(
session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T10:4xZ)四棱与速读见 os-bill 5596351726(本席复核同意)。补三条读数与本席的字母:
- 存量样本已收窄:
text自行解决(finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76content/docs/componentspages #6150);tooltip拆成 finding(types):TooltipSchema.childrenis declared REQUIRED and read nowhere — the renderer readsschema.triggeronly, so a type-conforming tooltip renders no trigger #7268、toast/sonner拆成 finding(components):toastandsonnerregistryinputsomitbuttonVariant, a key both the TS type declares and both renderers read — the designer/palette face cannot author it #7316(5505237475);本卡只剩类问题:三面(TS 类型 / 注册表inputs/ 渲染器读)谁是权威、要不要门。 - 09-07 的
defaultProps普查(5569668079)给出第四面:defaultProps既是默认值又是设计器种子,7 处分歧里 3 处是刻意的种子。⇒ 任何门都要把「种子」从「默认」里区分出来,否则门会把正确状态判成错。 BaseSchema同时声明body与children且带[key: string]: any索引签名(5320138110)⇒ 今天 TS 面无法拒绝任何键,B 的「类型为权威」要先决定这两个孪生键与索引签名的去留,否则门只能查inputs与接口那一半。
推荐 B(TS 类型为权威,门强制注册表与渲染器读跟随),以 C 的门为实现载体:先落
check:component-surface-parity(比对inputs⊆ 接口、渲染器读的键 ⊆ 接口;defaultProps种子行需登记理由),分歧逐个修到接口那一侧。⛔ 不推荐 A(把作者读的那份变成生成物)。回退 C(只加门、不定权威)。置信缺口:[key: string]: any去留会牵动多少现有类型声明,未测。裁后执行:spec@objectui 席一张卡先落门(report-only 起步、到零分歧翻阻断),BaseSchema索引签名单独一张协议卡。
Generated by Claude Code
- 存量样本已收窄:
裁决落卡 —— 批 #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.childrenis declared REQUIRED and read nowhere — the renderer readsschema.triggeronly, 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
顺序注记(总监席第 21 场,2026-09-10T11:0xZ):维护者本场对话逐字:「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」(2026-09-10T11:0xZ,对第二句的补全) —— 补到本卡的裁定上:凡本席写「实现或立 spec 卡退役」的地方,第三句定了顺序:先判协议对不对;判协议不对的,spec 卡先行、本卡阻塞其上,⛔ 不实现一个被判错的臂、⛔ 不在 objectui 本地收窄或放宽 spec 声明的形状、⛔ 不新增 spec 未声明的作者可写键。判协议对的,实现跟上。
Generated by Claude Code
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--tieroutput quoted:scripts/pm/dispatch-gates.mjsderives families from the objectstack tree and says so itself ("A card landing in another repo derives nothing here"), and triage left noSize/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.childrenis declared REQUIRED and read nowhere — the renderer readsschema.triggeronly, 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
5617614371applies 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:14Zagainst
origin/main=75fca9669a3df84b065c1e9ded0946d67112000din/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.- 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:45plus three files underscripts/__tests__/), and the clean absent-token
controlqqzz_absent_token_9999→ 0. - Scale.
ComponentRegistry.register(— 507 occurrences across 301 files, corpus
origin/main -- packages. Occurrences counted withgit grep -o … | wc -l; files with
git grep -l … | wc -l. ⛔ Notgrep -c, which counts lines and under-reports any line
holding two matches. ComponentPropsMapis reachable from objectui — 93 files name it at that ref, among them
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts.- The existing fourth-surface gate.
registry-inputs-spec-parity.test.tsis the only file in
the tree matchingregistry-inputs. It reconciles registryinputs↔ specComponentPropsMap.
Your checker must not duplicate it or contradict it; say in the PR how the two divide the work. - Card-reference face, read live at
02:14Z(⛔ not as the ruling described them in
September): finding(types):TooltipSchema.childrenis declared REQUIRED and read nowhere — the renderer readsschema.triggeronly, so a type-conforming tooltip renders no trigger #7268closed/completed,span的value声明面无人读取 —— 照TextSpanSchema与已发布文档写value渲染成空元素 #5050closed/completed, finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76content/docs/componentspages #6150closed/completed,
finding(components):flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229closed/completed, finding(types): the zod layout mirror substitutes runtime.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735closed/completed. finding(components):toastandsonnerregistryinputsomitbuttonVariant, 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/sonnerdeclare and readbuttonVariantwhile the registryinputs
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. - The
defaultPropscensus in5569668079
(55 comparable pairs / 7 disagreements, of which 3 are deliberate designer seeds) is a
2026-09-07 reading, andflex.align— its first row — has since been repaired via finding(types): the zod layout mirror substitutes runtime.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735 /
finding(components):flex.tsx's own registration declaresdefaultProps.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
0even with disagreements. The ruling's words are 「report-only 起步,零分歧后翻阻断」; the
flip to blocking is a later card, on a measured zero.- One
package.jsonentry,check:component-surface-parity, spelled like its siblings. - Unit tests under
scripts/__tests__/, shaped likecheck-designer-field-key-parity.test.ts.
At least one must be a control that fires — a synthetic type whoseinputsname 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'sbody/childrentwins or its[key: string]: anyindex signature — the ruling
puts these on their own card in so many words.- Touching any TS interface, any registry
inputsarray, 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.tssurface 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
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
PM answers the two open questions — and owns the defect in premise 4
domain:spec@ objectui execution seat, sessionsession_01L5xpA5q533BgTTNADibEFt, PM loop
round 9. Reading taken2026-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.tsis the only file in the tree matchingregistry-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
inputsagainst 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
readPropsaccessor: 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 readsmergeable_state: behind—
mainmoved while the branch was in flight — so a base merge is owed before it can land.
Generated by Claude Code
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
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
Landed — PR #9551 merged, and the merge verified rather than reported
Seat:
domain:spec@ objectui,os-sam, sessionsession_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 e9dc38ee9e362797054dbeff9e747f3f61145f51enqueued 2026-09-15T03:39:20Z merged 2026-09-15T03:59:29Z — queue transit 20 min 09 s squash commit 5495b8de70fc78edbfbde78ff23cb51b83da3695The 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, onepackage.jsonscript 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 returnshead's status and not grep's — the first take of this pair was polluted exactly that way and was discarded.
Card bookkeeping
Fixes #4631in the PR body closed this card automatically at 03:59:30Z withstate_reason: completed. It was left carryingpm:dispatchedand 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-parityexits 0 with its findings. The census it reports (339 rows ate9dc38ee9e, 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
- added a commit that references this issue
on Sep 17, 2026
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:
packages/typesinputsarray in theComponentRegistry.register(...)metaschemaTwo measured specimens
tooltip— surface 1 contradicts 2 and 3, and following surface 1 renders blankpackages/types/src/overlay.ts:266declareschildren: SchemaNode— required, and it is the only slot the interface declares for the trigger.triggeris declared neither there nor onBaseSchema.packages/components/src/renderers/overlay/tooltip.tsx:28readsschema.trigger; line 31 readsschema.content || schema.body.childrenis read nowhere.trigger/content/body.So an author who follows
TooltipSchema— the type the package publishes — authorschildrenand gets a blank tile. That is exactly what the catalog entry did, and it type-checks. The fix in PR #4630 makes the entry authortrigger, which is a keyTooltipSchemadoes not declare.text— surface 2 declares a key surface 1 does not have, and the renderer accepts bothpackages/types/src/layout.ts:56declaresvalue?: string. There is nocontentonTextSchema, and none onBaseSchema.contentasrequired: trueanddefaultPropsusescontent.packages/components/src/renderers/basic/text.tsx:35and:40both readschema.content || schema.value.Both spellings work at runtime; each is "the declared one" depending on which surface you read. #4626 asserted that
valueis not read, and used that to explain a blank tile — measured,valueIS 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:
inputsname 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.Refs #4626, #4616, PR #4630.
Generated by Claude Code