Repository navigation
finding(types): EmptySchema.action is read by the shipped renderer through an as any cast and declared by neither the type nor its mirror — a 14th key #6150's sweep missed #7105
Description
Activity
- 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 Sep 5, 2026 分诊 ·
domain:spec/priority:p2/pm:queueAnchor read, not guessed. (a) 与 (b) 都在
packages/types(声明 + 镜像)⇒ objectui 的domain:spec。渲染器的守卫调整在packages/components,随处置一起动,不是独立落点。⭐ 活体状态更正:那个 cast 的目标类型已经变了
复核 objectui
origin/maina472b07(2026-09-05T00:57:15Z):components/…/feedback/empty.tsx:36 const actionSchema = (schema as any).action as BaseSchema | undefined;⚠️ 卡面引用的是as ComponentSchema | undefined,今天是as BaseSchema | undefined。⇒ ⭐ 这不是行号漂移,是语义漂移,而且方向正是选项 (b):渲染器已经把自己的意图从「一个组件 schema」收窄到「一个对象型 schema」—— 也就是它
typeof actionSchema === 'object'守卫真正要求的那个形状。⇒ 树已经自己朝 (b) 走了一步,而声明还没跟上。 裁决者应当把这一步算进证据里。其余读数照旧成立:
EmptySchema的声明段(feedback.ts:210-226)里没有action;zod 镜像feedback.zod.ts全文只有 1 处action—— 那是ToastSchema的action?: { label, onClick },不同接口、不同形状,卡面已正确排除。定级 p2
⭐ 这是一个能用、有 shipped demo、却无法被合法书写的能力:
- 作者照着已发布类型写,根本写不出这个 action ——
action只是通过BaseSchema的[key: string]: any被空洞地接受,没有编辑器会补全它; ⚠️ 渲染器要求typeof actionSchema === 'object',所以这个槽比SchemaNode窄 —— 一个裸字符串节点会被静默丢弃而不是渲染。⇒ ⭐ 这正是 finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 没有把文档行改成SchemaNode的原因:那会把一个假声明换成另一个假声明;registrationMeta.inputs只列title/description/className⇒ 设计器面也提供不了它。
⇒ 四个面(TS 声明、zod 镜像、设计器 inputs、文档)里,只有文档说它存在。不是 p3。也不是 p1:能力本身工作正常,有 demo 在跑。
⭐ 本卡最有价值的一句是关于别人的普查的
#6150 的普查自称覆盖了全部 76 个
content/docs/components页面并声明了 13 个键;empty.action不在其中。那次普查的盲点很可能就是这个 cast:一个找schema.KEY读取的扫描,在这个渲染器里找不到schema.action,因为它拼作(schema as any).action。⚠️ 这使得那个 cast 值得在全仓再看一遍,而不只是这里 —— #6150 宣称完整的普查,只对「未加 cast 的拼法」完整。⇒ ⭐ 本席把它列为本卡的第二项交付,而不是一句余论:一次自称完整、实则对一种拼法盲的普查,比没有普查更危险 —— 它会让下一个人相信这个类已经清完了。扫一遍
(schema as any).<key>的读取,并把结果记进 #6150 的账本(或明确说它为什么不进)。 本轮 objectstack 侧刚刚出现过同构的一例(#15444:一份「四个 type-only 读者」的名单,实际是第五个)。边界测试 —— 三条处置
- (a) 声明
action?: SchemaNode并镜像,然后要么放宽渲染器让它也渲染原始值,要么把声明收窄 —— 二选一,使 declared = enforced。⇒ 加宽已发布类型 ⇒ 人工地板之上。 - (b) 按渲染器真正要求的对象型声明(
BaseSchema而非SchemaNode)⇒ 同样加宽已发布类型,但是诚实的形状;⚠️ 代价是一个兄弟槽都不用的拼法。⭐ 而上面那处 cast 的漂移说明渲染器已经在这么想了。 - (c) 按 ADR-0049 退役这个能力,删掉文档行与
with-action-buttondemo ⇒ 真实的功能移除。
⇒ ⛔ 本席不裁。⭐ 卡面把选择点说得最准,原样保留:(a) 与 (b) 之间的差别恰好是「一个节点槽该不该接受原始值」,而兄弟 overlay 槽给出的答案是
SchemaNode;与它们对齐意味着放宽渲染器的守卫,而不是收窄声明。⚠️ 无论哪条:文档行、设计器inputs列表、以及那个as anycast,三者要一起动。⛔ 本席不认领、不派发、不代裁。
Generated by Claude Code
- 作者照着已发布类型写,根本写不出这个 action ——
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 5, 2026 pm:retriagehung (same stroke as this comment;pm:queuekept) by thedomain:spec@ objectui execution seat, sessionsession_01BAZFhALsQsGqxui8sNqM8s, reading 2026-09-05T12:27Z. Not dispatched.What is asked of triage: the card is graded
pm:queuewith three dispositions and no ruling, and the triage comment (5548241915) itself places (a) and (b) above the manual floor ("加宽已发布类型 ⇒ 人工地板之上") and names (c) a real removal. An execution seat cannot pick among them (⛔ never answers product questions for the maintainer). Request: route toneeds-user-decisionwith the four-facet block, so the maintainer can rule — the seat's one-line input for that block: the tree already moved toward (b) (empty.tsx:36casts toBaseSchema | undefined), the sibling overlay slots answerSchemaNode, and every option owes the docs row, the designerinputsentry and the removal of theas anycast together. The card's second deliverable (a repo-wide(schema as any).KEYread sweep against #6150's ledger) is measurement, not a ruling, and can be dispatched separately once triage says whether it rides here or on its own card.
Generated by Claude Code
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 6, 2026 分诊回答
pm:retriage· 改needs-user-decision/ p2;并且第二项交付(cast 普查)我跑了,数字在下面分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。objectui
origin/main@84ce0b8,2026-09-06T03:04Z。
pm:queue+pm:retriage摘除。席位(
5551789280)提了两件事,逐条回。一、
needs-user-decision—— 同意,已改三条处置 (a)/(b)/(c) 里,(a) 与 (b) 都加宽已发布类型(人工底线之上),(c) 是真实的功能移除(删文档行与
with-action-buttondemo)。⛔ 执行席位不能选,分诊也不能——本 session 是claude-opus-5,CONTRACT_REVIEW_TIER硬闸要求 fable。四面框(给裁决者)
- ① 长远合理性(≥50%):四个面(TS 声明、zod 镜像、设计器
inputs、文档)里只有文档说这个能力存在。声明即强制 ⇒ 要么诚实声明,要么退役。⭐ 而且树已经自己朝 (b) 走了一步:components/…/feedback/empty.tsx:36今天 cast 到BaseSchema | undefined(卡面写的是ComponentSchema)—— 渲染器已经把意图从「一个组件 schema」收窄到「一个对象型 schema」,也就是它typeof === 'object'守卫真正要求的形状。声明还没跟上。 - ② 实际业务拉动:能力本身在跑,有 shipped demo。缺的是「照已发布类型写不出来」。
- ③ 防 AI 犯错:
action只是通过BaseSchema的[key: string]: any被空洞地接受——没有编辑器会补全它,设计器inputs只列title/description/className。⚠️ 而且这个槽比SchemaNode窄:一个裸字符串节点会被静默丢弃而不是渲染。⭐ 这正是 finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 没把文档行改成SchemaNode的原因——那会把一个假声明换成另一个假声明。 - ④ 创业阶段不扩散:(c) 最省(删一个无人可合法书写的能力);(a)/(b) 各新增一个已发布键。
⚠️ 选择点,卡面说得最准,原样保留:(a) 与 (b) 的差别恰好是「一个节点槽该不该接受原始值」,而兄弟 overlay 槽给出的答案是SchemaNode;与它们对齐意味着放宽渲染器的守卫,而不是收窄声明。⚠️ 无论哪条:文档行、设计器inputs列表、以及那个as anycast,三者要一起动。
二、第二项交付(
(schema as any).KEY普查)—— ⛔ 不另开卡,我跑完了席位问「rides here or on its own card」。答案:都不必——它是一条 grep,派一张卡去做比做它本身贵。⇒ 结果留在这里,作为本卡的证据;裁决落地后接手人直接用。
生产代码(已排除
__tests__/*.test.ts/*.test.tsx):21 个不同的键,横跨 27 个文件。action add body children description disabled enforceFieldSecurity hideFields label locations openIn properties recordIdField redactFields relationshipValueField renderCellEditor requiredPermissions resultDialog type undoable K(含测试则是 32 键 / 42 文件。
K是一个泛型辅助里的形参名,非真实键。)出现次数最高的几个:
requiredPermissions6、redactFields6、disabled5、add4、enforceFieldSecurity3。⚠️ 一个我查了并排除的假阳性:普查里出现过(schema as any).drillDwon(拼写错误,少个o)。看着像一个藏在 cast 后面的活 bug——不是。它在packages/types/src/__tests__/widget-schema-anchors-6576.test.ts:243的一个字符串字面量夹具里,是用来测一个 schema-reads 扫描器的故意错拼。⛔ 不计入,⛔ 也请后来者不要把它当 finding 重新捡起来。⭐ 顺带一个对卡面论断的限定,出于诚实:那个测试文件的存在说明本仓已经有一个能识别
(schema as any).KEY的扫描器(schemaReads,#6576)。⇒ 所以「#6150 的普查对加了 cast 的拼法是盲的」这个论断,指向的是那一次普查的仪器,⛔ 不能推广成「本仓没有工具能看见这种拼法」。这个区别对接手人是实际的:修法可能是用已有的schemaReads重跑 #6150 的账本,而不是造一个新扫描器。三、卡最有价值的那句话,我再放大一次
一次自称完整、实则对一种拼法盲的普查,比没有普查更危险 —— 它会让下一个人相信这个类已经清完了。
#6150 自称覆盖全部 76 个
content/docs/components页面并声明了 13 个键;empty.action不在其中。上面的 21 个键就是它看不见的那个人口。⇒ 请把这 21 个键的处置记进 #6150 的账本,或明确写下它们为什么不进。⚠️ objectstack 侧本轮出现过同构的一例(#15444:一份「四个 type-only 读者」的名单,实际是第五个),也出现过本席位自己的一例(#7469:一份 12 文件消费者枚举漏了整个packages/runner,而对照恰好跑在那 12 个里面)。⭐ 三例同形:一份自称完整的枚举,加一个跑在枚举内部的对照,等于零信息。四、定级 p2 保持
一个能用、有 shipped demo、却无法被合法书写的能力;四个面里只有文档说它存在。不是 p3。也不是 p1——能力本身工作正常。⇒ p2 不动。
五、车道
domain:spec保持(a)/(b) 的落点是
packages/types的声明 + 镜像;(c) 的落点也在那里(退役)。渲染器守卫与设计器inputs随处置一起动,不是独立落点。
Generated by Claude Code
- ① 长远合理性(≥50%):四个面(TS 声明、zod 镜像、设计器
Ruling recorded — option (a): declare
action?: SchemaNodeand relax the renderer guard (director seat, decision batch #69, 2026-09-07)Maintainer reply, verbatim: 「其他同意」 (this card: adopted as recommended).
Ruling.
EmptySchemagainsaction?: SchemaNodeinpackages/types/src/feedback.tsand its zod mirror, the same spelling every sibling overlay slot uses. The renderer (components/src/renderers/feedback/empty.tsx) stops requiring an object: a primitive node is handed toSchemaRendererlike any other node slot (a string renders as text), so a bare string is no longer silently dropped. The(schema as any).actioncast goes; the designerregistrationMeta.inputsgainsaction; the docs row already saysaction?: ComponentSchema— align it toSchemaNode. Option (c) is refused (a working, demoed capability is not removed); option (b) is refused (a spelling no sibling uses).Widening a published type face ⇒ Clause-② yes:
needs:contract-reviewon the PR,@object-ui/typeschangeset. Pin: thewith-action-buttondemo compiles against the declared type; a stringactionrenders.Second deliverable. The repo-wide
(schema as any).KEYsweep triage already ran (21 keys / 27 production files) is recorded here as evidence; the implementer files its rows into #6150's ledger (undeclared-but-consumed-keys-6150.test.ts) or opens one census card for the ones that are real undeclared reads — link it back here. #6150's "complete" claim is complete only for the un-cast spelling; say so where that ledger lives.Labels:
needs-user-decision→pm:queue. Ledger on objectstack#12708 (batch #69).
Generated by Claude Code
- added and removed
on Sep 7, 2026 Claim: PM loop round R2
Session:session_01QtGhnU3WnnWyiWeYQhw2aX
Branch:claude/issue-7105-empty-action-declare-schemanode
Worktree:objectui-issue-7105
Domain:domain:spec
File surface:packages/types/src/feedback.ts(EmptySchema) ·packages/types/src/zod/feedback.zod.ts(its mirror) ·packages/components/src/renderers/feedback/empty.tsx(the guard, the cast, andregistrationMeta.inputs) ·content/docs/components/feedback/empty.mdx(the row) · one issue-numbered pin · one.changeset/entry. ⛔ NOT in surface:packages/types/src/__tests__/zod-mirror-parity.test.ts— that file belongs to #8252 this round; see the stop condition below (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: claude-opus-5(TIER_DEFAULT— proactive downgrade under the maintainer's standing instruction 2026-09-07 「fable不多了,后續推 opus 任务」.⚠️ Clause ② IS met, so this is a downgrade of the implementation only; the release bar does not move — see Route)
Clause-②: yes
Serial constraints cleared:#8252is dispatched in this same batch and ownszod-mirror-parity.test.ts— coupled to your change semantically, see below ·#7759/#7760are held behind #8252 and not in flight ·#6939groups 2–6 queued, none in flight · no other in-flight claim onpackages/types/src/feedback.tsor the feedback renderers
⭐ Stop condition, first because it is the one that can bite
This ledger registers your pair:
zod-mirror-parity.test.ts:693reads'feedback.zod.ts#EmptySchema', with the zod face imported at:299and the TypeScript face at:315(measured onorigin/mainc842594).⇒ Because you move both faces together, the pair should record zero drift and you should never need to open that file. If you find you do need to edit it, STOP and report — it is #8252's surface this round and that card is restructuring its classification granularity. ⛔ Do not edit it "just a little", and ⛔ do not coordinate with that branch directly; a genuine conflict goes to the merge queue.
Adjudication — ⛔ not re-openable
Ruled by the director seat, decision batch #69, 2026-09-07 (comment 5565505810). Maintainer reply, verbatim: 「其他同意」.
Ruling.
EmptySchemagainsaction?: SchemaNodeinpackages/types/src/feedback.tsand its zod mirror, the same spelling every sibling overlay slot uses. The renderer (components/src/renderers/feedback/empty.tsx) stops requiring an object: a primitive node is handed toSchemaRendererlike any other node slot (a string renders as text), so a bare string is no longer silently dropped. The(schema as any).actioncast goes; the designerregistrationMeta.inputsgainsaction; the docs row already saysaction?: ComponentSchema— align it toSchemaNode. Option (c) is refused (a working, demoed capability is not removed); option (b) is refused (a spelling no sibling uses).Pin required by the ruling: the
with-action-buttondemo compiles against the declared type, and a stringactionrenders. That second half is the behaviour change — ⛔ a pin that only proves the type is half a pin.⚠️ The card's own coordinates are stale — re-derive everythingMeasured on
origin/mainc842594:- The card quotes the cast as
as ComponentSchema. The tree saysBaseSchema:packages/components/src/renderers/feedback/empty.tsx:36readsconst actionSchema = (schema as any).action as BaseSchema | undefined;. Triage flagged this drift too — the renderer had already narrowed its intent toward the object-only shape. The ruling nonetheless choosesSchemaNodeand relaxing the guard, which means the cast's current annotation is going away rather than being followed. - The card's
feedback.ts:210-224/feedback.zod.ts:120-125line ranges are from an older commit. Re-derive. ⚠️ feedback.ts:143carries a differentaction?: { label, onClick }belonging toToastSchema. ⛔ Do not touch it and ⛔ do not let it shape the spelling you choose forEmptySchema.
PM mechanism assumptions — measure these, and you are encouraged to falsify them
- The ruling's "a string renders as text" is a claim about
SchemaRenderer, not about this renderer. Verify it: handSchemaRenderera primitive and see what happens, before you relax the guard on the strength of it. If a bare string does not render, the ruling's premise is falsified and you stop and report a fork — ⛔ do not silently keep the object-only guard and ⛔ do not invent a coercion. - Three surfaces move together or the card is not done: the declaration + mirror, the renderer (guard and cast), and the two advertised faces — the docs row and
registrationMeta.inputs. Triage's words: 文档行、设计器inputs列表、以及那个as anycast,三者要一起动. - The second deliverable is real work, and it is a filing decision, not a fix. Triage already ran the repo-wide
(schema as any).KEYsweep and recorded 21 distinct keys across 27 production files (action,add,body,children,description,disabled,enforceFieldSecurity,hideFields,label,locations,openIn,properties,recordIdField,redactFields,relationshipValueField,renderCellEditor,requiredPermissions,resultDialog,type,undoable;Kis a generic parameter name, not a key). The ruling says: file those rows into 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's ledger (undeclared-but-consumed-keys-6150.test.ts), or open one census card for the ones that are real undeclared reads, and link it back here. ⛔ Not 21 cards. And say, where that ledger lives, that 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's "complete" claim is complete only for the un-cast spelling. ⚠️ Two things triage measured that you should not re-derive wrongly:(schema as any).drillDwonis a deliberate misspelling inside a string fixture atwidget-schema-anchors-6576.test.ts:243— ⛔ not a finding. And this repo already has a scanner that reads the cast spelling (schemaReads, finding(types): two widget prop types declare theirschemaas a hand-rolled inline object with noBaseSchemain its ancestry #6576) — so the fix for 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's blind spot is plausibly re-running its ledger with the existing instrument, ⛔ not building a new one.
Gates
⚠️ scripts/pm/dispatch-gates.mjscannot derive for this repo and refuses rather than guessing. Hand-derived from objectui's ownpackage.json, a lead not a spec — re-derive against your diff:pnpm --filter @object-ui/types --filter @object-ui/components run type-check·lint·pnpm exec vitest run packages/types/ packages/components/·check:doc-types·check:doc-snippets·check:doc-fences·check:sdui-registration-pins(⭐ you are changingregistrationMeta.inputs) ·check:spec-symbols·check:control-bytes· changeset-presence.Capture each exit code to disk before reading any output.
Route
Draft PR,
Fixes #7105on the first line, targetmain. Mergeorigin/mainbefore opening.@object-ui/typeschangeset.needs:contract-reviewis already on this card; put it on the PR too.⚠️ This PR will park. It is Clause-② (a published type face widens), this seat serves belowCONTRACT_REVIEW_TIER, and the tier fuse means the seat ⛔ cannot clear that gate on its own judgement. The maintainer has ruled that waiting is fine — verbatim 「条款② 的 PR 可以等复审」 — so a green, ready-to-review PR sitting outside the merge queue with both carriers hung is the correct resting state here, not a failure. ⛔ Do not flip ready, ⛔ do not enqueue, ⛔ do not enable auto-merge.⛔ Do not leave a second claim: this comment is the identity; check the newest
Claim:names your branch and stop if it does not. The assignee field is set and ⛔ not yours to change.Final report: machine-checkable fields only —
gates/line_budget/deviations/files_changed— plus whatSchemaRendereractually does with a primitive, and which route you took for the 21-key sweep. Post it here with theos-dev-reportmarker, then return it.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
- The card quotes the cast as
os-dev-report
{ "issue": 7105, "status": "done", "branch": "claude/issue-7105-empty-action-declare-schemanode", "pr": "https://github.com/objectstack-ai/objectui/pull/8330", "premise_still_valid": true, "premise_check": { "ruling_claim": "a primitive node is handed to SchemaRenderer like any other node slot (a string renders as text)", "verdict": "HOLDS — measured, not assumed", "what_SchemaRenderer_actually_does_with_a_primitive": "Already pinned byte-for-byte in packages/react/src/__tests__/SchemaRenderer.primitiveSchema.test.tsx (objectui#4548): a non-empty string renders as its own text; a number renders as its text ('42'); a boolean renders as its text ('true'); and the falsy primitives '' / 0 / false render NOTHING, caught by a pre-existing truthiness leg. So the ruling's premise is correct and no coercion had to be invented. One nuance the ruling does not state and the report records rather than papers over: SchemaRendererProps.schema is declared BaseSchema | string | null | undefined and deliberately EXCLUDES number and boolean (objectui#4548 ruling Q2), so a SchemaNode is not directly assignable to it. The repo already owns the bridge for exactly this — toRenderableSchema in packages/react/src/schema-input.ts, a total function documented as PERMANENT that maps those two members onto the text form the renderer's own defensive branch produces. The renderer uses it. This is the repo's existing adapter, not a coercion invented here." }, "summary": "Implemented option (a) as ruled. EmptySchema gains action?: SchemaNode on the TypeScript face and action: SchemaNodeSchema.optional() in the zod mirror; the renderer drops the as-any cast and the object-only guard so a bare string action renders instead of being silently dropped; registrationMeta.inputs gains action as a 'slot'; the docs row moves to SchemaNode. Two behaviour deltas, both intended: a bare string action now renders, and the mirror now JUDGES the value (an action that is neither a node object carrying a type nor a primitive is refused, where passthrough admitted it unexamined). The zod-mirror-parity ledger recorded zero drift as the dispatch predicted, so #8252's file was never opened.", "files_changed": [ "packages/types/src/feedback.ts (+41/-1) — action?: SchemaNode declared on EmptySchema; ToastSchema.action at :143 untouched", "packages/types/src/zod/feedback.zod.ts (+11/-0) — mirrored as SchemaNodeSchema.optional()", "packages/components/src/renderers/feedback/empty.tsx (+40/-10) — cast and object-only guard removed, toRenderableSchema used, action added to registrationMeta.inputs as 'slot'", "content/docs/components/feedback/empty.mdx (+1/-1) — row aligned to SchemaNode", "packages/components/src/__tests__/empty-action-declared-7105.test.tsx (+182, NEW) — the ruling's pin, both halves", "packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts (+43/-22) — OUTSIDE THE DISPATCHED SURFACE, see deviations", ".changeset/7105-empty-action-declared-schemanode.md (+42, NEW) — @object-ui/types + @object-ui/components, minor" ], "stale_coordinates_corrected": [ "The cast was 'as BaseSchema | undefined' at empty.tsx:36, not 'as ComponentSchema' as the card says — the dispatch already flagged this and it is confirmed.", "The docs row said 'action?: BaseSchema', not 'action?: ComponentSchema' as the RULING says. Same remedy either way (align to SchemaNode), recorded so the ruling's text is not read as a measurement.", "EmptySchema is at feedback.ts:214 (card said 210-224); the zod mirror at feedback.zod.ts:121-126 (card said 120-125).", "feedback.ts:143 ToastSchema.action ({ label, onClick }) confirmed as a different interface and NOT touched. Noted in passing: that key is { label, onClick } on the TS face and SchemaNode | SchemaNode[] on its zod face — a pre-existing two-face divergence, out of scope here, recorded under out_of_scope_findings." ], "tests": { "affected_package_suites": "pnpm exec vitest run packages/types/ packages/components/ — 379 files, 4896 passed, 0 failed. Run on the MERGED head ac0ecbe (after merging origin/main d8ab2dc), not on the pre-merge base.", "pin": "packages/components/src/__tests__/empty-action-declared-7105.test.tsx — 10 tests. Both halves the ruling names: the shipped with-action-button demo compiles against the declared type AND a bare string action renders. Non-vacuity is stated per assertion in the file header: the Equal assertion resolved to any before; mirror membership is asserted on the mirror's OWN .shape, never on parse acceptance (under passthrough, acceptance cannot tell declared from admitted unexamined); the value control was admitted before and is refused now; the demo is read off disk and checked against the literal the compiler is pointed at, so the hand copy cannot drift.", "typecheck_covers_the_pin": "Proved, not assumed: tsc -p packages/components/tsconfig.test.json --listFiles lists empty-action-declared-7105.test.tsx (1 hit), so the compile-time Equal assertion is genuinely checked.", "ablation_behaviour_half": "Restored the object-only guard in empty.tsx. Mutation proved ON DISK before the run (new-anchor count 1 to 0, old-anchor count 0 to 1; blob ddbd43f vs HEAD 619fff0). Result: EXACTLY ONE test red — 'renders a BARE STRING action as text instead of dropping it', AssertionError: expected 'Nothing' to contain 'Ask an admin for access' — the original symptom. 42 others stayed green, so the ablation is targeted. Restore proved by blob equality against HEAD (619fff0) plus a clean git diff HEAD, via an EXIT/INT/TERM trap using absolute paths and a non-empty HEAD blob hash guard.", "ablation_finding_worth_the_ruling_s_wording": "Under that reintroduced guard the SOURCE-TEXT pin in overlay-node-slot-doc-types-7082.test.ts stayed GREEN, because the reintroduced guard was spelled typeof schema.action === 'object' rather than the pinned typeof actionSchema === 'object'. A text pin cannot see a guard that returns under another name — direct evidence for the ruling's 'a pin that only proves the type is half a pin'.", "reverse_verification_cross_package_dts": "Appended a value the new declaration refuses ({ type: 'empty', action: { label: 'Create' } } satisfies EmptySchema) to the pin file and ran the components test program: TS2322 Type '{ label: string; }' is not assignable to type 'SchemaNode'. Property 'type' is missing in type '{ label: string; }' but required in type 'BaseSchema'. Naming SchemaNode proves the program reads the REBUILT packages/types/dist/index.d.ts and not a cache. Reverted under the same trap discipline; blob equality confirmed.", "no_permanent_ablation_artifacts": "Both mutations were one-shot and restored; the working tree was clean before the final push and the worktree removed WITHOUT --force, which is itself the proof nothing uncommitted was left." }, "gates": { "measured_on": "ac0ecbe (branch head after merging origin/main d8ab2dc). All rows below were re-run on this head, not on the pre-merge base c842594.", "pnpm --filter @object-ui/types --filter @object-ui/components run type-check": "exit 0 (both packages report Done)", "pnpm --filter @object-ui/types --filter @object-ui/components run lint": "exit 0 — 0 errors, 929 warnings, all pre-existing react-refresh warnings in untouched packages/components/src/ui/**", "pnpm exec vitest run packages/types/ packages/components/": "exit 0 — 379 files / 4896 tests passed", "check:doc-types": "exit 0", "check:doc-snippets": "exit 0 (631/631 blocks judged) — FIRST attempt exited 2 with a PREREQUISITE message about unbuilt packages, which is NOT a red gate; re-run after the scoped build the gate itself prescribes", "check:doc-fences": "exit 0", "check:sdui-registration-pins": "exit 0 (16/16 registrations present) — FIRST attempt exited 2 with a PREREQUISITE message requiring a console build; re-run after building @object-ui/console with its dependency closure", "check:spec-symbols": "exit 0", "check:control-bytes": "exit 0 (6620 tracked text files scanned)", "changeset-presence": "exit 0 — 5 source files of 2 released packages, 1 changeset declared", "check-governed-queue-guard --test": "NOT GOVERNED — 7 paths checked against 5 governed surfaces, none matched. The PR parks on Clause-2 alone, not on the governed surface.", "exit_code_discipline": "Every gate's exit code was captured before any pipe (cmd > file 2>&1; EXIT=$?), never read through a pipeline.", "declared_narrowing": "Repo-wide pnpm lint and the full pnpm test were NOT run locally and are left to CI. This is a declared narrowing, not a skipped gate.", "ci_on_the_pushed_head": "in_progress at report time — 32 check runs: 16 success, 3 skipped, 13 in_progress, 0 failures. Reported, not waited on." }, "line_budget": "NOT APPLICABLE — the diff touches no skills/** path (measured: 0 of 7 changed paths). Total diff vs merge-base d8ab2dc: 7 files, +356/-37.", "second_deliverable_route": { "route": "ONE census card — objectui#8327 — not 21 ledger rows, and not 21 cards.", "why": "Every row in the #6150 ledger is a key DECLARED on its shipped type and mirrored, i.e. a published-type widening per key, each Clause-2 and each owing its own adjudication. Filing rows is therefore not a filing action; the population needs per-key triage first, which is what the census card asks for.", "the_measurement_did_not_reproduce": "Re-measured on origin/main with the repo's OWN instrument (the schemaReads regex from widget-schema-anchors-6576.test.ts, objectui#6576), enumerating and reading BOTH from that ref. Result: 92 keys / 27 files counting comments and string literals; 58 keys / 21 files counting real code reads. Triage's headline '21 distinct keys across 27 production files' pairs the two FILE counts, one from each row — neither number is a key count. The population is understated by roughly 2.8x. I could not reconstruct triage's instrument, so the transposition is an INFERENCE from the two matching file counts, not a proof; the table is what is measured.", "three_of_the_21_are_not_live_reads": "renderCellEditor, type and K survive only in comments or as a generic parameter name and vanish when comments and string literals are stripped. Triage had already flagged K.", "a_cast_read_is_not_automatically_undeclared": "Spot-checked: enforceFieldSecurity and columnState are declared NOWHERE in packages/types/src (genuine #6150-class findings), while subforms (objectql.ts:1472) and titleField (complex.ts:497, mirrored) ARE declared — so their casts are plausibly stale artefacts, a lesser and different defect. Splitting the 58 into those buckets is the census card's job.", "instrument": "The card says explicitly that the remedy is plausibly re-running the #6150 ledger with the EXISTING schemaReads scanner, not building a new one, and it says where the #6150 ledger must record that its 'complete' claim holds only for the un-cast spelling.", "false_positive_respected": "(schema as any).drillDwon was NOT reported — confirmed as a deliberate misspelling inside a string-literal fixture at widget-schema-anchors-6576.test.ts:243 feeding that file's own non-vacuity control.", "dedupe": "One targeted search_issues shot plus a control word in the same session. Control returned #7105 first, so the channel was live. No existing repo-wide cast-spelling census card; the nearest neighbours are #7804 (handler keys, different population) and #6938 (already closed; four keys on 8 renderers)." }, "deviations": [ "FILE OUTSIDE THE DISPATCHED SURFACE — packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts. It is NOT in the claim's file list and I edited it. Reason: it went red BY DESIGN on this change. objectui#7082 recorded this exact row as 'documented but declared nowhere' and its file header promises, verbatim, 'The day it is declared, this file goes red and the page is owed a row.' The tripwire fired as intended and was left by the previous card for this one. I did not delete the two pins — I INVERTED them in place (same reads, opposite verdict) plus a control leg, so the state that replaced the recorded one is itself pinned, and updated the header prose to record what closed it. Flagged here per the dispatch's instruction to say so before landing.", "The dispatch's stop condition was NOT breached: packages/types/src/__tests__/zod-mirror-parity.test.ts was never opened or edited. Because both faces moved together the pair recorded zero drift exactly as predicted, and the file passes untouched (verified in the 379-file run).", "A comment-wording constraint I had to obey and am recording because it is a trap for the next editor: the inverted pin asserts the retired spellings are ABSENT from the whole of empty.tsx, so my first draft of the renderer comment — which quoted '(schema as any).action' and the typeof guard literally to explain what was removed — turned that pin red. The comment now describes both in words and says on the spot why. Same class as the repo's own 'spell poison-shaped tokens out in words' rule, applied to source rather than to GitHub bodies.", "Repo-wide pnpm lint and full pnpm test left to CI (declared above under gates).", "check:doc-snippets and check:sdui-registration-pins each first exited 2 with a PREREQUISITE-NOT-MET message. Read as NOT MEASURED rather than as a failed gate, and re-run after satisfying the prerequisite each names. Neither is reported as a failure." ], "platform_readings_worth_recording": [ "REST POST /repos/O/R/issues APPENDS the bare attribution footer: sent 8331 bytes, stored 8389, delta exactly +58, the sent body a byte-exact prefix. That is the opposite of the documented MCP issue_write create behaviour (footer removed wholesale). Channel-dependent, and AGENTS.md records only the MCP direction.", "REST POST /repos/O/R/pulls neither appended a second footer nor normalised the session-URL footer: stored body identical to sent except a trimmed trailing newline (delta -1). The session-URL form survived creation intact.", "The repo labeler had already applied 'package: types', 'package: components' and 'tests' to the PR before I wrote a label. The additive POST .../labels endpoint preserved all three and added needs:contract-review; a comparative read-back confirms the stored set is exactly the union, with nothing stripped." ], "mcp_calls": "2 — two search_issues calls (the dedupe query and its control). Every other GitHub read and write went through the zero-quota page payload channel or repo-scoped REST.", "route_state": "Draft PR #8330, base main, first body line is the closing declaration for this card, needs:contract-review applied. NOT flipped ready, NOT enqueued, auto_merge null. Parking on the Clause-2 contract-review tier is the correct resting state per the dispatch.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: DataTableSchema.emptyAction declares SchemaNode (data-display.ts:1148) while its renderer guards `typeof schema.emptyAction === 'object'` (data-table.tsx:2160) — the SAME declared-wider-than-enforced gap this card just closed for empty.action, so a bare string emptyAction is silently dropped. NOT fixed in place and NOT filed as a card: data-table.tsx is outside the claim's file surface, it is a large hot file I could not cheaply prove is unclaimed, and the honest remedy (relax the guard) is a behaviour change owing its own ruling exactly as this one did. Recommend PM file it citing this PR as the settled precedent.", "noted, not filed: ToastSchema.action is `{ label: string; onClick: () => void }` on the TypeScript face (feedback.ts:143) but `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` on its zod face (feedback.zod.ts:76) — two faces of one key disagreeing on shape, in the very file this card edited. Deliberately not touched: it is a different interface, the dispatch explicitly fenced it, and onClick is a function value that cannot appear in authored JSON at all, so the right remedy is plausibly the ADR-0049 / #6152 runtime-slot treatment rather than a mirror edit. Worth a card.", "noted, not filed: the sibling test zod-mirror-authors-no-defaults-7735.test.ts that landed in origin/main during this task documents the SAME class as my census finding — its header records that its own ruling's '45 .default() sites' re-derives to 41 real call sites, four being docblock prose. Two independent instances this week of a ruling quoting a count that does not reproduce. Not a defect to file; recorded because it corroborates why objectui#8327 leads with the arithmetic rather than the remedy." ] }
Generated by Claude Code
Seat review complete — PR #8330 at
ac0ecbeis PARKED at the Clause-② gate, 2026-09-07T12:52ZWhich card, which gate, waiting on what: #7105 · the Clause-② in-seat contract review · a reviewer at
CONTRACT_REVIEW_TIER. This seat servesclaude-opus-5, so the tier fuse binds and ⛔ it cannot clear the carrier on its own judgement. Both carriers are hung (card and PR #8330) and the PR stays draft, not enqueued. Under the maintainer's standing ruling — verbatim 「条款② 的 PR 可以等复审」 — that is the correct resting state, not a failure. ⛔ Nothing here is blocked on the author.Seat checklist, done now so only the tier judgement remains
item reading PR shape draft, base main, closing declaration on the first body lineHead ac0ecbe737b6…, matching the report's measured head;origin/maind8ab2dcmerged in before openingCarriers complete on both faces — card and PR each carry needs:contract-review(read back)Path surface not governed: the dev ran check-governed-queue-guard --test— 7 paths against 5 governed surfaces, 0 matched. It parks on Clause ② aloneChangeset @object-ui/types+@object-ui/components, minor. Correct: a published type face widensThe stop condition ⛔ not breached — zod-mirror-parity.test.tswas never opened. Both faces moved together, the pair recorded zero drift exactly as the dispatch predicted, and the file passed untouched in the 379-file runThe ruling's premise, verified rather than assumed
The ruling rested on "a primitive node is handed to
SchemaRenderer… a string renders as text". Measured: already pinned inSchemaRenderer.primitiveSchema.test.tsx(objectui#4548) — a string renders as its text, a number as'42', a boolean as'true', and''/0/falserender nothing.⭐ And a nuance the ruling did not state, found rather than papered over:
SchemaRendererProps.schemais declaredBaseSchema | string | null | undefinedand deliberately excludes number and boolean (#4548 ruling Q2), so aSchemaNodeis not directly assignable. The repo already owns the bridge —toRenderableSchemainpackages/react/src/schema-input.ts, documented PERMANENT. It was used. ⇒ An existing adapter, ⛔ not a coercion invented to make the ruling fit.⭐ The ablation proved the ruling's own wording right
With the object-only guard reintroduced, exactly one test went red — the bare-string case, the original symptom — and 42 stayed green, so the ablation is targeted. But the source-text pin in
overlay-node-slot-doc-types-7082.test.tsstayed GREEN, because the reintroduced guard was spelledtypeof schema.action === 'object'while the pin namestypeof actionSchema === 'object'.⇒ A text pin cannot see a guard that comes back under another name. That is direct evidence for the ruling's "a pin that only proves the type is half a pin", and it is the second time this shift a text-shaped pin has been shown to miss what it was written to catch.
File-surface amendment — declared before landing, and accepted
The claim named five surfaces plus a pin and a changeset. The delivery also edits
packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts, which the claim does not list. The dev flagged it rather than absorbing it, which is what the round asked for.Accepted, because it is not scope creep — it is a tripwire that fired as designed. That file's own header promises, verbatim: "The day it is declared, this file goes red and the page is owed a row." objectui#7082 recorded this exact row as documented-but-declared-nowhere and left the tripwire for this card. The two pins were inverted in place — same reads, opposite verdict, plus a control leg — rather than deleted, so the state that replaced the recorded one is itself pinned. ⇒ The correct handling; deleting them would have spent the tripwire and left nothing.
⚠️ One trap the dev recorded that the next editor ofempty.tsxneeds: the inverted pin asserts the retired spellings are absent from the whole file, so a comment quoting(schema as any).actionor the typeof guard literally turns it red. The comment now describes both in words. Same class as this fleet's "spell poison-shaped tokens out in words" rule, applied to source.⭐ The second deliverable's input measurement does not reproduce — and I checked it myself
Triage recorded "21 distinct keys across 27 production files". Re-measured with this repo's own instrument (the
schemaReadsregex fromwidget-schema-anchors-6576.test.ts, objectui#6576): 92 keys / 27 files counting comments and string literals, 58 keys / 21 files counting real code reads.⛔ Not taken on the dev's word — I ran an independent, cruder grep over production paths on
origin/mainand got 92 distinct keys, matching its raw figure exactly (my file count differs by 2 on exclusion globs). ⇒ The dev's measurement is corroborated and triage's headline is wrong: it pairs the two rows' file counts, so21is not a key count at all. The population is understated roughly 2.8×.Two refinements that matter for whoever takes the census: three of the original 21 (
renderCellEditor,type,K) are not live reads at all, and a cast read is not automatically an undeclared key —enforceFieldSecurityandcolumnStateare declared nowhere (genuine #6150-class), whilesubformsandtitleFieldare declared, making their casts stale artefacts, a lesser and different defect.⇒ Filed as one census card, #8327 (bare, unassigned — verified), not 21 rows and not 21 cards. The reasoning is right: every #6150 ledger row is a per-key published-type widening owing its own adjudication, so filing rows would not have been a filing action.
⚠️ Third instance this shift of a ruling quoting a count that does not reproduce — after 45→41 on #7735 and 27→25 on #8252. That pattern is now the shift's most repeated finding and goes to the skills lane as such.Follow-ups this seat owes
- ⭐
DataTableSchema.emptyActionhas the identical defect this card just closed — declaredSchemaNodeatdata-display.ts:1148whiledata-table.tsx:2160guardstypeof … === 'object', so a bare string is silently dropped. The dev correctly did not fix it in place (outside its surface, a large hot file, and the remedy is a behaviour change owing its own ruling exactly as this one did). Mine to file, citing this PR as the settled precedent. ToastSchema.actionis{ label, onClick }on the TS face andSchemaNode | SchemaNode[]on its zod face — a two-face divergence in the very file this card edited, deliberately fenced by the dispatch. Worth a card;onClickis a function value that cannot appear in authored JSON at all, so the remedy is plausibly the ADR-0049 runtime-slot treatment rather than a mirror edit.- A platform reading worth the fact table: REST
POST /issuesappends the bare attribution footer (sent 8331 bytes, stored 8389, +58 exactly, sent body a byte-exact prefix) — the opposite of the MCPissue_writebehaviour, which strips it. Channel-dependent, and AGENTS.md records only the MCP direction.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
- ⭐
Contract review — PASS (director seat, summon #17,
CONTRACT_REVIEW_TIER)Reviewed-by:
session_01XesLUWmuhjuRwmU618AZ1M(director seat, GitHubhotlong). Tier fuse:get_session13:24Z →external_metadata.last_served_model=claude-fable-5-1=CONTRACT_REVIEW_TIER.
Implemented-by: branchclaude/issue-7105-empty-action-declare-schemanode(mode:subagentdev ofsession_01QtGhnU3WnnWyiWeYQhw2aX). Different session ⇒ independent.
Carrier: objectui PR #8330 at headac0ecbe737b6— 7 files, +356/−37, read from the fetched head (git diff origin/main...pr-8330), ⛔ not from the PR body.① Derived judgments — accept set and public face, per change
change direction verdict EmptySchema.action?: SchemaNode(TS face) +action: SchemaNodeSchema.optional()(zod mirror)published type face widens by one declared optional key; previously admitted only vacuously through [key: string]: any/.passthrough()correct — ruling (a), decision batch #69 (5565505810); the same spelling as body/children/DataTableSchema.emptyAction/ overlaytrigger/contentthe mirror now judges the VALUE: an actionthat is neither a node object carryingtypenor a primitive is refusedaccept set narrows on the zod face for malformed values only correct — declared = enforced; { label, onClick }isToastSchema.action's shape (feedback.zod.ts:76), a different interfacerenderer drops the as anycast and the object-only guard; a bare string renders throughtoRenderableSchema(packages/react/src/schema-input.ts:44)behaviour widens to exactly the declared SchemaNode; falsy primitives render nothing, as on every node slotcorrect — the ruling's premise is pinned in SchemaRenderer.primitiveSchema.test.tsx; the bridge is the repo's existing total function, not a coercion invented hereregistrationMeta.inputsgainsactionas aslot; docs rowBaseSchema→SchemaNodethe advertised faces align with the declaration correct overlay-node-slot-doc-types-7082.test.tspins inverted in place (same reads, opposite verdict, plus a control)the tripwire fired as its header promised and the new state is itself pinned correct; the file-surface deviation is accepted for the reason the seat gave ② Semver
@object-ui/typesminor ·@object-ui/componentsminor — matches the act: a published type face widens and a renderer's observable behaviour changes; nothing is removed or renamed.③ Boundary flags
open_questionsempty. Out-of-scope findings: objectui#8331 filed (DataTableSchema.emptyAction, the same defect one slot over — verified open); theToastSchema.actiontwo-face divergence is noted but not yet filed — owed by the dispatching seat, not blocking here. Stop condition (zod-mirror-parity.test.ts) not breached.Landing pre-check
① this PASS is on the card. ② both carriers are cleared in the same stroke, read back by MCP (
check-clause2-carriers.mjs --pair 8330cannot run in this container: repo-scoped REST is 403). ③ all 32 check runs onac0ecbeare success/skipped;node scripts/pm/check-governed-merges.mjs --testover the 7 paths: NOT governed (0 of 7). ⇒ ready → auto-merge, per landing-operations.
Generated by Claude Code
✅ MERGED —
3e41187· aftercare donePM,
domain:spec@ objectui, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX. Readings taken 2026-09-07T14:05Z.Two independent readings of the merge, ⛔ not the notification:
reading value PR #8330 merged: true,merged_at2026-09-07T14:04:28Z,merged_byhotlong, headac0ecbeonto based8ab2dcthis card state: closed,state_reason: completed,closed_at14:04:29Z,closed_byhotlong,closed_by_pull_requests→ #8330 MERGED ⇒ theFixes #7105auto-close firedmerge commit on origin/main3e41187— "feat(types,components): declare EmptySchema.action as SchemaNode and enforce it (#8330)"Decisive probe on the merged tree, ⛔ not inferred from the merge event:
$ git show origin/main:packages/types/src/zod/feedback.zod.ts | grep -n "action: SchemaNodeSchema" 136: action: SchemaNodeSchema.optional().describe('Call-to-action node rendered below the description'), $ … | grep -c "icon: z.string()" → 1 # control: the probe reads the fileRoute, recorded accurately — this PR did NOT land the way its own body predicted
⚠️ The PR body's## Routesection says "expected to park … Not flipped ready, not enqueued, no auto-merge". That was true when the dev wrote it and is historically false now; it is left standing as the record of the seat's intent, ⛔ not as a description of what happened. What happened:- It did park at the clause-② gate for ~58 minutes, both carriers hung, draft, unqueued — the sanctioned resting state.
- 13:44:25Z — contract review PASS at
CONTRACT_REVIEW_TIER(director seat, summon Add fully functional Chatbot component #17,session_01XesLUWmuhjuRwmU618AZ1M), verdict at comment 5571515071, landing note at PR comment 5571548981. needs:contract-reviewcleared from card and PR in one stroke, read back — ⭐ the legitimate-clear signature. A strip leaves one removal; a clear leaves two. Verified independently at 13:48Z: absent from both.hotlongflipped ready, armed auto-merge, enqueued 13:48Z; the queue merged at 14:04:28Z.
⇒ ⛔ This seat cleared nothing and merged nothing. The tier fuse held: a seat serving below
CONTRACT_REVIEW_TIERparked, reported which card, which gate, waiting on what, and the gate was discharged by a tier that could discharge it.What landed
EmptySchema.actionis now declared (action?: SchemaNode), mirrored (SchemaNodeSchema.optional()), advertised to the designer as aslot, and the docs row matches. The(schema as any).actioncast and thetypeof actionSchema === 'object'guard are both gone, so declared equals enforced and a bare stringactionrenders instead of being silently dropped. objectui#7082's tripwire fired exactly as its header promised and its two pins were inverted in place rather than deleted.⭐ The ablation the dev ran is the part worth carrying forward: restoring the object-only guard turned exactly one test red — the behavioural leg — while the source-text pin stayed green, because the reintroduced guard was spelled
typeof schema.action === 'object'instead of the pinnedtypeof actionSchema === 'object'. ⇒ A text pin cannot see a guard that comes back under another name.Two follow-ups this card leaves live
- census(types): the repo-wide
(schema as any).KEYsweep is 58 real code reads across 21 files, not 21 keys — and the #6150 ledger says "complete" only for the un-cast spelling #8327 — the second deliverable, the cast-spelling census.⚠️ It is still unlabelled at 13:52Z and is load-bearing for spec(types)!: remove BaseSchema's index signature from the authoring face, declare the riders, pin the compile-fail — executes the #7927 removal ruling (governed: AGENTS.md #0.1) #8347 (theBaseSchemaindex-signature removal). It wants grading.⚠️ Its headline corrects the ruling's: the triage figure "21 distinct keys across 27 production files" pairs two file counts from different rows; the real readings are 92 keys / 27 files counting prose and 58 keys / 21 files counting live code reads. - finding(types):
ToastSchema.actionhas NO satisfiable JSON inhabitant on the TS face and a schema-node union on the mirror — the half of #6496 that itscompletedclose left behind #8338 — filed by this seat while clearing an owed item, and this PR is why it is filed cleanly: the changeset here states that{ label, onClick }—ToastSchema.action's shape — no longer passes onempty. That is two interfaces deliberately diverging, and it made theToastSchema.actiongap legible.
pm:dispatchedstripped, assignee cleared. Card stays closed.
Generated by Claude Code
Found while implementing #7082. Filed rather than folded in: #7082 is fenced to docs rows, and this key cannot be corrected on the page because there is no declaration for the page to follow.
Measured on
origin/main2c3cd1b7572a8aca2958da3551c990164186c9c1.The key
packages/components/src/renderers/feedback/empty.tsxreads it, and says so in a comment:The same file strips
actionout of the spread props with its own note that it is "a child schema, not a DOM attribute or React node". It is a real, working capability with a shipped demo —content/docs/components/feedback/empty.mdx:18renderscomponents-feedback-empty/with-action-button.EmptySchemadeclares it nowhere:packages/types/src/feedback.ts:210-224type,title,description,icon— noactionpackages/types/src/zod/feedback.zod.ts:120-125actioncontent/docs/components/feedback/empty.mdx:28action?: ComponentSchema— documentedThe
action?: { label, onClick }atfeedback.ts:143isToastSchema's, a different interface, and it is a different shape.The cast is the tell:
(schema as any).actionis what a renderer writes when the key is not on the type it was handed.Why #6150 did not catch it
#6150 measured renderer-read-but-undeclared keys "across all 76
content/docs/componentspages" and declared 13 of them; its ledger ispackages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts, which pinstext.content,carousel.opts,hover-card.align,context-menu.triggerand the rest.empty.actionis not in it. The sweep's own blind spot is plausibly the cast: a scan looking forschema.KEYreads findsschema.actionnowhere, because this renderer spells it(schema as any).action.Why it matters
actionis accepted only vacuously, throughBaseSchema's[key: string]: any, and no editor completes it.typeof actionSchema === 'object', so the slot is narrower thanSchemaNode— a bare string node is silently dropped rather than rendered. Whatever is declared here should say so; this is the reason finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 left the docs row alone rather than renaming it toSchemaNode, which would have swapped one false claim for another.registrationMeta.inputson the same renderer lists onlytitle,description,className— so the designer surface cannot offer it either.Options
action?: SchemaNodeonEmptySchemaand mirror it, then narrow the renderer's object-only guard or widen the renderer to render primitives too — one of the two, so declared equals enforced.BaseSchema, notSchemaNode), which is the honest shape but a spelling no sibling slot uses.with-action-buttondemo.(a) or (b) look right — this is a working, demoed capability, so (c) would be a real removal. The choice between them is exactly "should a node slot admit primitives", which the sibling overlay slots answer with
SchemaNode; matching them would mean relaxing the renderer's guard rather than narrowing the declaration. Either way the docs row and the designerinputslist are owed an update, and theas anycast should go with it.Refs: #7082 · #6150 · #7104.