fix(plugin-detail,components): 补齐渲染器已兑现的四个 spec 键的 inputs 声明,并把 parity 门的反方向推到全仓 (#3808) - #3841
Conversation
…rs already honoured, and push the parity gate's reverse direction repo-wide (#3808) PR #3806 landed the repo-wide parity gate in ONE direction ("a block may not declare a top-level input its spec props schema does not accept"). PR #3795's single-block version had two, and the other one — "a key the spec declares must be discoverable from `inputs`" — never made it out of that one block. This adds it, over the same `covered` set and the same exemption discipline, and fixes the four keys it finds. A class, verified at the renderer read sites rather than assumed: - `record:details.hideFields` (`renderers/record-details.tsx:147`) - `record:related_list.relationshipValueField` (`record-related-list.tsx:95`) - `record:related_list.add` (`record-related-list.tsx:186`, `:231`) - `element:text_input.defaultValue` (`text-input.tsx:73/76/119`) All four were spec-declared, renderer-honoured, and absent from `inputs`, which is the objectui#3407 failure on four more keys: the manifest and the generated `.d.ts` omit the key, `sdui-parser`'s prop walk returns `unknown-prop` on an author who writes it, and the renderer honours it anyway. `element:text_input` is non-public so its gap was not in `sdui.manifest.json` at all — it was in the JSX-page compiler's whitelist, which `page.tsx:462` builds from `getKnownTypes()` plus these same `inputs`. Descriptions are derived from what each renderer DOES, not from restating the spec, because the two differ in three places that matter: `hideFields` teaches bare names only (the renderer tolerates entry objects, the spec rejects them); `add.picker.labelField` publishes the renderer's `name` default, not the spec's "object title field"; and `add.picker.filter` is named as a KNOWN GAP rather than documented as a restriction (spec declares it, nothing reads it) following the `record:activity.showSubscriptionToggle` precedent. Nine spec keys stay deliberately unpublished, each with a written reason and an open issue, and a stale-entry test deletes an entry once it stops describing anything: two the renderers do not read at all (#3829), three ADR-0087 tombstones, `page:tabs.type` (#6776 upstream), two `targetVariable` declarative hints (#3834), and `element:record_picker.filter` (#3830 — a real A-class gap that fell out of #3808's own three-class triage, so out of this PR's scope). Two console probes generate fixtures from the declarations, so `add` becoming a declared `object` input handed them `{}` — which is not a valid `add` (the spec makes `picker` required) and crashes `record:related_list` outright (`RelatedList.tsx:1299` dereferences `add.picker.object` where `:378`/`:390` optional-chain it, filed as #3838). Both samples are now spec-valid, and the ledgered branch of `public-block-binding-reach` gained a crash guard because "made no data call" is its pass condition and a crashed block satisfies it. Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
ℹ️ Console Performance Budget — not measuredThis run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget. This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.
See the workflow run for details. 📦 Bundle Size Report
Size Limits
|
…llsFor` returning html type-checks (#3808) CI Type Check and Bundle Analysis both went red on four errors in one file, and both from the same cause: the previous commit made `dataCallsFor` return `{ calls, html }` while its annotation still said `Promise<string[]>`. public-block-binding-reach.test.tsx(335,12): TS2353 'calls' does not exist in type 'string[]' public-block-binding-reach.test.tsx(356,15): TS2339 Property 'calls' does not exist on type 'string[]' public-block-binding-reach.test.tsx(356,22): TS2339 Property 'html' does not exist on type 'string[]' public-block-binding-reach.test.tsx(357,37): TS7006 Parameter 'c' implicitly has an 'any' type The fourth is a cascade of the first three — `c` infers as `string` again once `calls` is a `string[]` rather than an error type. Fixed by giving the function a named return type instead of widening anything: a `Mount` interface with `calls: string[]` and `html: string`, deliberately the same shape and field names as the sibling probe's own `Mount` (`record-block-record-reach.test.tsx:310-313`), which has captured both halves from the start for exactly the reason this file now needs the html — a crash is invisible in the call log, because `SchemaRenderer` catches the throw and paints an error card, so a crashed block simply makes no calls. No `as any`, no signature loosening. Bundle Analysis was not an independent failure: its first step is `pnpm --filter @object-ui/console build`, and that build begins with `tsc`, so it was failing on these same four errors before reaching a bundle at all. Verified with the CI commands rather than the narrower per-package ones that missed this — the previous commit's `type-check` run predated these edits and was never repeated, which is precisely how four type errors reached CI: pnpm type-check -> 78 successful, 78 total pnpm --filter @object-ui/console build -> exit 0 (tsc + vite build + build:plugin) console entry gzip 28.1 KB against the 350 KB budget -> PASS vitest, the gate + both probes + three per-block parity files -> 95 passed (6 files) Co-authored-by: Claude <noreply@anthropic.com>
|
返工记账(实施 agent 自述,便于 review 时核对):首推的 CI 两项红是同一个原因,且是我的流程错误,不是环境问题。 4 条类型错上一 commit 让 第 4 条是前 3 条的连带: Bundle Analysis 不是独立失败
修法给函数一个命名返回类型,而不是放宽任何东西: 我漏了什么(根因)上一 commit 的 type-check 是在编辑 本次改用 CI 同命令复验: 行为面零变化:这次只动类型注解与注释,断言、fixture、豁免名单一字未改。 Generated by Claude Code |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge(返工 1 轮)。 返工核验:修法干净 —— 命名 interface 照抄兄弟探针自身的 首推验收要点一并落档(前跳已核):A 类扩为四键( 方法库入账:①「末次编辑之后必须重跑 type-check,且用 CI 同命令(全仓链)」—— vitest esbuild 转译不做类型检查,测试绿掩盖类型红;②豁免名单依赖 #3829/#3830/#3834 保持 open(门只校验单号存在不校验状态)—— 关闭其中任一须同时处理键本身。 Generated by Claude Code |
…key objectstack-ai#3808's triage dropped (objectstack-ai#3830) (objectstack-ai#4112) `filter` appears in objectui#3808's raw key dump for this block and then in none of its A / B / C lists, so the change that added the repo-wide parity gate exempted it by name instead of declaring it. It is the fourth gap of exactly the same shape as the four that PR objectstack-ai#3841 fixed: `@objectstack/spec` declares `ElementRecordPickerProps.filter`, the renderer has read it all along (`composed?.filter ?? props.filter` -> `query.$filter`), and the registry `inputs` never mentioned it. `element:record_picker` is not in `PUBLIC_BLOCKS` ("record picking is a field widget, not a page block"), so the gap was not in `sdui.manifest.json` — it was in the JSX-page compiler's prop whitelist, which `renderers/layout/page.tsx` builds from `getKnownTypes()` plus these same `inputs`. Verified end to end rather than argued: with the declaration reverted, `compile()` over the live registration returns `<element:record_picker> has no prop "filter"` on a key the renderer then filters the entire candidate set by. The description is derived from what the renderer DOES, because the one thing an author cannot read off the spec is which of the two places they may write a filter wins: a node-level `dataSource` filter (itself AND-combined with any saved `view` it names) is taken and this top-level `filter` is DROPPED, not merged. `type: 'object'`, from the spec's actual shape on the resolved pin and not the `'array'` the issue's landing sketch guessed. `filter` is `FilterConditionSchema`, i.e. `z.record(z.string(), z.unknown())` intersected with the `$and`/`$or`/`$not` group, so a rule array is rejected — measured with `safeParse`, and `sdui-parser`'s `checkType` object arm draws exactly the same partition. This is the one key in the family where `ComponentInput`'s coarse typing costs nothing, so unlike `element:text_input.defaultValue` there is no narrowing to disclose in the description (data point for objectui#3832, not a widening of this card). The gate's explicit exemption for this key is deleted in the same change, as its own `carries no stale unpublished-key exemption` assertion demands, and the key joins objectstack-ai#3808's four in the by-name "declared, not merely not-failing" pin — now five. Reverse verification (declaration reverted, exemption removal kept): element:record_picker publishes every top-level key its spec props schema declares -> AssertionError: expected [ 'filter' ] to deeply equal [] the five A-class keys objectui#3808 / objectstack-ai#3830 declared -> does not publish filter a JSX page writing `filter` -> `has no prop "filter"` (control: `searchFields` still reported, so the probe is not silently vacuous) 7 failed | 50 passed, restored to 57 passed. Verified: vitest packages/components/ + 4 console gates -> 116 files, 1111 tests passed vitest packages/sdui-parser/ + RefComponentWidget -> 5 files, 24 passed type-check @object-ui/components + @object-ui/console -> Done (after building the console dependency closure; the first run's TS2882s were stale artifacts) eslint on the three touched files -> 0 errors (12 pre-existing warnings in the untouched effect body) check-control-bytes / check-changeset-presence / -no-major / -fixed -> OK Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #3808
PR #3806 把全仓 parity 门只推了一个方向(「registry 不得声明 spec 不接受的顶层 input」);PR #3795 的单块版有两个,另一个「spec 声明的键必须能从
inputs被发现」没跟出来。本 PR 把反方向补上(同一个covered集合、同一套豁免纪律、同一个文件),并修掉它找出来的 A 类缺口。A 类逐键对照(渲染器读点实读,非按 spec 措辞推断)
pin 版
@objectstack/spec@17.0.0-rc.5,基线origin/main@c85268256。inputsrecord:details.hideFieldsz.array(z.string())(objectstack#5611)renderers/record-details.tsx:147record:related_list.relationshipValueField'id'renderers/record-related-list.tsx:95defaultValue: 'id'record:related_list.addpicker必填renderers/record-related-list.tsx:186/:231element:text_input.defaultValuestring | numberrenderers/basic/text-input.tsx:73/76/119四条都是 #3407 的同一失效:manifest 与生成的
.d.ts不提这个键 → 设计器面板里不存在;sdui-parser/src/validate.ts:74在comp.inputs里找不到它 → 对照写了这个键的作者报unknown-prop;渲染器照样兑现。三个答案。element:text_input不在PUBLIC_BLOCKS(public-blocks.ts:80有成文理由),所以它的缺口不在sdui.manifest.json,而在 JSX 页面编译器的白名单 ——renderers/layout/page.tsx:462用getKnownTypes()+ 同一份inputs现搭那份 manifest。manifest 链实跑(不落盘)确认:record:related_list.add的核对结论:A 类,同修#3808 正文写「也可能属于这一类,没细查」。细查结论是属于,而且是四条里最要紧的一条:
add是构建 junction 指派列表的唯一途径,不声明就等于那个功能只能靠写一个不可发现的键来用。三个成员键渲染器全兑现(RelatedList.tsx:378/:390/:724),第四个不兑现。description 从渲染器取材而非照抄 spec,三处刻意的差异:
picker.labelField的默认值写name(RelatedList.tsx:390),不写 spec.describe()说的「the object title field」—— 后者没有任何代码实现;picker.filter写成 KNOWN GAP:spec 声明「Restrict which records the picker offers」,全仓零读点(RelatedList把picker.object/labelField传给 record picker 对话框,从不填它的baseFilter)。按record:activity.showSubscriptionToggle先例把 gap 说出口 —— 静默把它列进成员清单,等于告诉作者候选被限定了,而实际上对话框提供全部记录。接线另开record:related_list.add.picker.filter全仓零读点:作者限定了 Add 选择器的候选范围,对话框照样提供该对象的全部记录 #3831;hideFields只教裸字段名:渲染器容忍{name}/{field}条目,spec 是z.array(z.string())按值拒绝,教那种拼法就是发布一种契约拒收的方言(与同文件fields同一道围栏)。反方向门:豁免名单全文(9 条,逐条理由 + tracking issue)
aria走全局排除(理由统一:无障碍逃生口,不是布局选项,15 个 block 同一个理由,plugin-detail/src/index.tsx:335-337已成文),并有非空 + 「确实是某个 covered block 的 spec 键」双重非空洞断言。其余逐条:B 类 —— spec 声明、渲染器零读点(2 条,⛔ 不许无脑补 input)→ #3829
page:header.iconcontainers.tsx:822-1570里icon只出现在每个 action 自己的action.icon(:1321/:1365)与导航项item.icon(:604);block 自己的icon零读点page:card.actionsPageCardRenderer(containers.tsx:666-695)只渲染 title / body(或 children)/ footer补 input 会发布一个平台默默丢掉的键 —— 正是 #3797 修的方向;接线是视觉决定(且
page:card.actions会伸进renderers/action/**,PR #3825 刚动过);第三条路是showSubscriptionToggle先例(声明 + KNOWN GAP)。三种形状、一个公开契约,另立 #3829。载体冲突(1 条)→ objectstack#6776
page:tabs.typepage:tabs.tabStyle豁免是同一事实的两面:扁平载体里type是分发键(SchemaRenderer.tsx:251-270明确拒绝 hoist),且validate.ts的BASE_PROPS含'type',声明了也校验不到ADR-0087 墓碑(3 条)→ objectstack#5775 + #3809
element:record_picker的displayField/searchFields/multiple。顺带纠正 #3808 正文的一处错误:它写这三个键「pin 升上来后会从 spec 的接受集消失」。按 #3809 的机制这是错的 —— ADR-0087 D2 的退役是替换成z.never().optional(),不是删条目,Object.keys(shape)照样包含它们。所以这三条豁免不会随 pin 升级自动过期,只会随 #3809 的墓碑识别落地而过期,豁免理由里逐条写明了。同一个盲区在两个方向的症状相反(正方向假绿、反方向假红:门会要求本仓去声明一个按名被拒的键,照做则正方向立刻红),已写进门的 LIMIT 段落并在 #3809 补了评论。
declarative hint(2 条)→ #3834
element:record_picker.targetVariable/element:text_input.targetVariable。grep -rn targetVariable packages/ apps/零命中 —— 不是「读了没用上」,是这个标识符全仓没出现过;真正的绑定是usePageVariableBinding(schema?.id)按组件 id 反查PageVariableSchema.source,与 spec 自己的 describe 一致。发布与否是判断题(发布的风险很具体:只写targetVariable不写变量source的作者会得到一个什么都不写入的输入)。新发现的第 4 个 A 类,超出本单派发范围(1 条)→ #3830
element:record_picker.filter。渲染器实读(record-picker.tsx:78的ds.filter ?? props.filter→:103的query.$filter)、spec 已声明、inputs不提 —— 与本 PR 修的四条完全同形。它在 #3808 的原始 key dump 里出现过,却没有被归入 A / B / C 任何一类(正文自称 15 条、逐行相加实为 13 条,C 类小计写 5 条实为 6 条,掉的就是它),因此落在派发范围之外。按 Prime Directive #10 另开 #3830 + 带理由豁免,而不是在没人为它做过 review 的 PR 里扩面。emptyText/sort/limit(本仓实读、pin 版 spec 未声明、objectstack#5775 已声明)在墓碑那段的注释里记档:pin 一升它们变成新的 A 类,门会报红要它们 —— 这个红是对的、是想要的,就是 pin 升级自带的提醒。反向验证(方向先判后跑,四次)
1. 去掉任一新声明 → 门红(预判:红。命中)
删
record:details.hideFields的声明:三处一起红:派生的全仓门、按名钉住的那条、块级 parity。这一跑还抓出一个空绿:同文件的「
hideFields只教裸名」断言在声明被删后仍然绿,因为not.toContain('{')对''恒真。已加expect(description).not.toBe('')前置,并在注释里写明是这次反向验证发现的。2. 豁免塞无理由条目 → 豁免纪律断言红(预判:恰好一条红。命中)
把
page:tabs.type的理由改成不含 issue 号:3. 给已声明的键塞豁免 → 陈旧豁免 + 按名钉住 两条红(预判:2 条。命中)
第二条是刻意的:它把「用豁免代替声明」这条退路堵死 —— 派生断言本身对「补进豁免名单」也会绿,而那正是本单要排除的动作。
4.
public-block-binding-reach的崩溃守卫 —— 预判红,实测绿,按实情记账把那里的
add样本退回{},预判它会触发新加的崩溃守卫。实测 16 个全绿。原因查清了:renderers/record-related-list.tsx:185传的是dataSource={ctx?.dataSource},这个探针不带 RecordContext,于是RelatedList收到dataSource: undefined,:1293的add && dataSource短路,裸取根本到不了。所以那条守卫今天是防御性的、不是承重的,已在注释里如实写明(而不是留一句「它会红」)。守卫的判据本身可用 —— 把它同时加到两个分支上时,它报出了 #3840 里那两个崩溃。崩溃本身是真的,在兄弟探针里实测到(那个探针带 RecordContext + dataSource):
两个 console 探针的 fixture 处置
两者都按声明自动生成 fixture,
type: 'object'的通用样本是{}。add一旦成为声明的object输入,它们就拿到了{}—— 而{}不是合法的add(spec 里picker必填)。按 fixture 三分法这属于「补声明」:重新拼写暴露出 fixture 本来就不 spec 合法,补上缺失的必填键。record-block-record-reach.test.tsx:样本补成{ picker: { object: PROBE_CHILD_OBJECT } }。头注释里这是该文件记录的第五次「a plausible value for EVERY input is not a plausible CONFIGURATION」;public-block-binding-reach.test.tsx:同样补成 spec 合法值,并给 ledgered 分支加崩溃守卫 —— 那一支的通过条件是「没有数据调用」,而崩掉的块恰好满足它,所以本 PR 若只补样本会留下一类空绿。守卫刻意只加在 ledgered 分支:object-form/object-master-detail-form在这个 fixture 下确实会渲染成错误卡(reading 'map'),但它们发出了真实数据调用、断言是挣来的,那是既有状态,记入 [finding]object-form与object-master-detail-form在 binding-reach 探针的自动 fixture 下渲染成错误卡(reading 'map')—— 断言仍成立,但两个分支的崩溃守卫因此没法一起开 #3840,不靠加宽守卫顺手裹进本 PR。渲染器那处裸取(
RelatedList.tsx:1299取add.picker.object,而同文件:378/:390是可选链)按 AGENTS.md #0.1 没有在本 PR 加宽容 —— 修法有三条(一致化守卫 / 显式诊断 / 产出端校验),定的是诊断契约与校验时机,另立 #3838。element:text_input.defaultValue的一处刻意收窄spec 是联合
string | number,ComponentInput.type是单一粗类型、表达不了。选了'string'(文本输入的常态,且 DOM 值本来就String(...)强转),number arm 写进 description 并有断言钉住。这是真收窄:sdui-parser的checkType会对defaultValue={42}报type-mismatchwarning,而 spec 接受这个值。同族标本(page:header.title/record:alert.title的内联翻译映射,今天就会被误报)与四条可能方向记入 #3832;本 PR 不在checkType侧加任何宽容。验证
反方向门跑下来 42 条断言全绿(原 12 条 + 新 6 类)。
围栏
content/docs/releases/**;⛔ 未触components/renderers/action/**(PR fix(components): 动作面成员动作的 visible 门按「已声明」判定,visible: false 不再渲染 (#3812) #3825 在闸);未改containers.tsx(B 类只进豁免)。packages/types的 mirror(RecordDetailsComponentProps缺hideFields那半落在 Migrate the remaining ListView legacy vocabulary to spec-canonical keys, and audit ObjectView/DetailView (#2231 phases 4–5) #2890 /record:details的layout发布了auto|custom语义,渲染器唯一的读点只认 spec 已退役的inline|compact—— auto/custom 从未被实装 #3818 的完成范围里,渲染器走(schema as any)读,本 PR 无需类型面改动)。@object-ui/plugin-detail+@object-ui/componentspatch(未声明major)。本 PR 过程中另立的 issue(均未认领,交 PM triage)
page:header.icon/page:card.actions:spec 声明、渲染器零读点,接线 / 声明+KNOWN GAP / 上游退役 三择一element:record_picker.filter是第 4 个 A 类缺口,#3808 的三类清单把它漏出了分类add.picker.filter全仓零读点,作者限定了候选范围而对话框提供全部记录(baseFilter是现成槽位)ComponentInput.type表达不了 spec 联合类型,page:header.title的内联翻译映射今天就被 manifest 门误报findingtargetVariable全仓零读点,发布与否是未定判断题add漏了add.picker→ 整段相关列表变错误卡(RelatedList.tsx:1299裸取)findingobject-form/object-master-detail-form在 binding-reach 探针 fixture 下渲染成错误卡,两分支崩溃守卫因此没法一起开另在 #3809 补了评论:同一个墓碑盲区在反方向的症状是假红,且
page:card.body那个标本在 pin 升级时会同时是一个假绿(body)和一个真红(children),一次处置解决两个。Generated by Claude Code