fix(plugin-detail): record:highlights 的 fields 声明补上 readonly,让 manifest 能被作者读到 (#3407) - #3795
Conversation
…st 能被作者读到
`readonly` 早已被兑现:renderer 归一化时逐条拷贝它,`HeaderHighlight` 的可编辑门
据此拒绝 chip 上的内联编辑(objectstack#5077);`@objectstack/spec` 也已在
`RecordHighlightsField` 上声明(objectstack#5176 / PR #5607)。缺的只是本仓这一侧的
**发布面**:`fields` input 的 description 仍把条目形状写成
`{name,label?,icon?,type?}`,而 registry `inputs` 正是 `gen-manifest.ts` 序列化进
`sdui.manifest.json` 的东西 —— 读 manifest 的 AI 作者被告知这个键不存在。
`readonly` 写在 `fields` 的 description **里**,而不是新增一个自己的 input,因为
契约就是这么放的:spec 的 `RecordHighlightsProps` 顶层恰好三个键
(`fields`/`layout`/`aria`),`readonly` 挂在每个**条目**上。顶层加
`{ name: 'readonly', type: 'boolean' }` 会发布一个平台默默丢弃的键 ——
生成的 `sdui.manifest.json` 与 `sdui-intrinsics.d.ts` 会宣告 `readonly` prop,
manifest 门只校验顶层 prop、不会报任何诊断,`RecordHighlightsProps` 是普通
`z.object`、parse 时把未知键无声剥掉,而逐条读 `field.readonly` 的 renderer 永远
看不到它。信了这个面的作者,最后得到的是机器维护的列仍可手改、且任何地方都没有
诊断说明原因。`ComponentInput` 本就是扁平的,所以数组对象型 input 用散文发布成员
键,`record:path.stages` 与 `record:alert.action` 一直如此。
新增的 spec-parity 测试两个方向都在运行时从 `@objectstack/spec` 推导,而不是复述
今天的键表:`RecordHighlightsField` 对象分支的每个键都必须出现在 `fields` 的
description 里;本 block 也不得声明 `RecordHighlightsProps` 不接受的顶层 input。
此前没有任何东西交叉校验 registry `inputs` 与 spec,两个漂移方向都是静默的。
反向核验(方向事先预判,两个都应转红,各钉住改动的一半):
- 把 description 退回旧文本 → 「every spec entry key is discoverable」转红,
`undocumented = ['readonly']`;
- 按 issue 字面加顶层 `readonly` input → 「declares no top-level input the spec
does not accept」转红,`offSpec = ['readonly']` —— 新门恰好拦住字面方案。
无运行时行为变化。
Fixes #3407
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。 对 issue 字面验收偏离的裁定:采纳 dev 的契约忠实落点。 三条实测把字面方案证死:(1) git 实物核验( 长期方向:dev 两轴分析 B(现在)→ C(ComponentInput 成员形状声明位,跨 types/core/sdui-parser)成立;C 的档案记在 #3797 判级评论,不进决策箱。衍生 #3797(4 个 block 发布 pin 版 spec 不接受的顶层 input)另行判级。dev 自查纠错一则入方法库:占位 GITHUB_TOKEN + Generated by Claude Code |
objectstack-ai#3797) (objectstack-ai#3806) 把 PR objectstack-ai#3795 在 `record:highlights` 上落的单块检查(「不得声明 spec 不接受的顶层 input」)推广到所有「`ComponentPropsMap` 有条目且本仓注册了非空 `inputs`」的 block, 期望值运行时从 spec 自身 shape 推导(不复述键表),现有偏离全部进**显式豁免名单**, 每条带理由 + 追踪单号。 门放在 `apps/console/src/__tests__/`,因为它需要的是产出发布物那份完整注册图: `dev/manifest-dump.tsx` 用 `src/register-plugins.ts` + `@object-ui/components` 搭 manifest,本文件就导入这一对(与隔壁 `public-contract.test.ts` 同法),不自己攒一份 会自说自话的清单。覆盖面不限于 public tier —— `renderers/layout/page.tsx:462` 用 `getKnownTypes()` 现搭运行时 JSX 页面校验 manifest,所以非 public 的 block(如 `element:record_picker`)的 `inputs` 同样是活的 prop 白名单。 四个 OFF-SPEC block 逐块以代码判定,结论都落在 spec 一侧,因此**本仓四个 block 的 `inputs` 一个字节都没动**: - `page:header` 的 `recordChrome`/`showStar`/`showCopyId` —— 渲染器实读 (`containers.tsx:979/980/981`),消费于 `:1453` 的布局分支与 `:1531/:1532` 的 RecordTitleChip;作者可达且非宿主注入(宿主注入走 RecordContext 且故意不声明), `preview-samples.ts:68` 与 `buildDefaultPageSchema.ts:413` 今天就在写。 (issue 猜「可能是 objectstack-ai#3226/PR objectstack-ai#3265 有意保留的 renderer-only prop」已证否:objectstack-ai#3265 动的是 `packages/layout` 的遗留别名 `page-header`,canonical 这个没被碰过。) - `page:accordion.variant` —— 渲染器实读 `containers.tsx:734`,`:735` 据此改每个 面板的边框类,spec 完全没有对应键。 - `page:tabs.tabStyle` —— spec 已用 `type` 声明同一概念,所以是「一义两拼」;但两条 本地动作都被证否:撤掉它会删掉扁平 SDUI 载体唯一能表达的拼法 (`SchemaRenderer.tsx:251-270` 故意不上提 `properties.type`),改成发布 `type` 又 会声明一个自家 parser 结构上无法校验的键(`validate.ts` 的 `BASE_PROPS` 含 `'type'`)。收敛方向是 spec 侧的契约决定,不在这里猜。 - `element:record_picker` 的 `labelField`/`valueField`/`label` —— 上游 objectstack#5775 已经解决,且正是往本仓渲染器实读的 `labelField` 收敛 (`displayField`/`searchFields`/`multiple` 已转 `retiredKey()` 墓碑)。本仓这三条 flag 纯粹是 pin 落后:npm 上最新已发布的 `@objectstack/spec` 是 `17.0.0-rc.5`, 早于 #5775。 前三项转 objectstack#6776(跨仓转移协议,`pm:queue`,带 `Part of objectui#3797`); 八条豁免全部挂单号。豁免会自己过期:一旦 spec 声明了某个被豁免的键, `carries no stale exemption` 就红,强制删条目,名单不会烂成永久 allowlist。 Fixes objectstack-ai#3797 Co-authored-by: Claude <noreply@anthropic.com>
…Section IDs (objectstack-ai#3807) (objectstack-ai#3820) `inputs` 是发布出去的编写契约(gen-manifest.ts → sdui.manifest.json 保存门 + sdui-intrinsics.d.ts),而 record:details.sections 的说明写的是 `Section IDs to show (required when layout is "custom")` —— 17.x 以前的形状。 pin 版 @objectstack/spec@17.0.0-rc.5 的 RecordDetailsProps.sections 是对象数组 `{ name?, label?, columns?, fields }`;objectstack#5611 把 z.array(z.string()) 那条拼法删掉而不是 union 进来(既无 producer 也无 consumer)。 照旧说明写 `sections: ['contact_info','address']` 的作者四层都拿不到诊断: manifest 门只看顶层键名 + 粗类型(字符串数组是合法 array),上游 validateComponentProps 是 advisory 级,spec 只在真走 parse 的路径上才拒,而 RecordDetailsRenderer 对每个条目读 s.name / s.label / s.fields —— 字符串上三者 全 undefined,该 section 一个字段都不渲染。layout: 'custom' 时 sections 是正文 唯一来源,结果就是一张没有报错的空白详情页。 新说明逐键派生自 spec 各成员的 .describe() 与渲染器实读:fields 必填按序; label 是标题(省略即无标题无边框);name 是 snake_case 稳定标识与 i18n 锚点 (sectionLabel → objects.{object}._sections.{name}.label,useObjectLabel.ts:419); columns(1-4)是本 section 的字段栅格宽度(DetailSection 的 applyDetailAutoLayout(visibleFields, section.columns)),省略则由渲染器推导; 并明确写出字符串条目不被接受。渲染器另外还认的 title / showBorder / hideEmpty 故意不写进说明:spec 的 section 对象没声明它们,parse 时静默剥掉,发布它们等于 教作者写契约丢弃的键(与本文件下方"顶层 readonly 不声明"同一条理由)。 recordDetailsInputs.spec-parity.test.ts 是 PR objectstack-ai#3795(record:highlights)那条 sibling,两个方向都在运行时从 spec schema 派生:每个 spec 成员键都能从说明里 发现;本 block 不声明 spec 不接受的顶层 input。另外三条把这次的判断钉住 —— 退役拼法真的被 spec 按值拒(safeParse 红)、对象形保留、渲染器独有的三个 section 键确实会被 parse 剥掉且不得出现在说明里(词边界匹配,因为 untitled 内含 title)。 围栏内核对结论:同处 fields 的说明与 spec 的 "Explicit field list to display (optional, overrides highlightFields)" 语义一致, 元素是纯字符串、没有成员形状可发布,故不改;渲染器对 {name}/{field} 条目的容忍 不是第二套契约(spec 按值拒),测试里也钉了这一点。hideFields 未声明属 objectstack-ai#3808。 仅说明文本变化,无运行时行为改动。 Co-authored-by: Claude <noreply@anthropic.com>
…y 门的反方向推到全仓 (objectstack-ai#3808) (objectstack-ai#3841) * fix(plugin-detail,components): publish the four spec keys the renderers already honoured, and push the parity gate's reverse direction repo-wide (objectstack-ai#3808) PR objectstack-ai#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 objectstack-ai#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 (objectstack-ai#3829), three ADR-0087 tombstones, `page:tabs.type` (#6776 upstream), two `targetVariable` declarative hints (objectstack-ai#3834), and `element:record_picker.filter` (objectstack-ai#3830 — a real A-class gap that fell out of objectstack-ai#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 objectstack-ai#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> * fix(console): type the binding-reach probe's mount result, so `dataCallsFor` returning html type-checks (objectstack-ai#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> --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #3407
Part of objectstack-ai/objectstack#5176(spec 侧 PR objectstack-ai/objectstack#5607 已 MERGED)
摘要
把
record:highlights的发布面补齐:fieldsinput 的 description 现在写出完整条目形状{name,label?,icon?,type?,readonly?}并说明readonly的语义。registryinputs正是packages/sdui-parser/scripts/gen-manifest.ts序列化进sdui.manifest.json的东西,所以这一行就是 AI 作者能否从 manifest 得知该键存在的全部差别。
readonly早已两侧兑现:renderer 归一化逐条拷贝(renderers/record-highlights.tsx:70readonly: f?.readonly === true),HeaderHighlight可编辑门据此拒绝内联编辑(objectstack#5077);spec 已在
RecordHighlightsField上声明(#5607)。缺的只有本仓 description。按 AGENTS.md「issue 是线索不是规格」,动码前逐条核到
origin/main(objectui00b9451d8,objectstack
d42a92fc6):origin/main上record:highlights已有 2 个 inputs(fields、layout),自 #2113 / #3027 起就有docs/audits/2026-06-react-blocks-conformance.mdgit log --oneline --all -- 'docs/audits/2026-06-react-blocks-conformance.md'零命中,任何分支任何历史都没有过;docs/audits/现存只有 3 个文件,均无此名inputs中声明readonly键(boolean)readonly在 spec 里是fields[]的逐条键,不是顶层 prop第三条是本 PR 最关键的判断,证据(运行本仓 pin 的
@objectstack/spec@17.0.0-rc.5):若按 issue 字面加顶层
{ name: 'readonly', type: 'boolean' },后果是发布一个平台默默丢弃的键:sdui.manifest.json与sdui-intrinsics.d.ts会宣告 RecordHighlights 有readonlyprop;packages/sdui-parser/src/validate.ts的 manifest 门只遍历节点顶层 prop(Object.entries(node)对
comp.inputs),不下探fields[]条目,因此顶层readonly被判为「已知 prop」,零诊断;z.object,parse 时把未知顶层键无声剥掉(上方实测);field.readonly,永远看不到顶层的那个。净结果:作者信了平台自己的 manifest,写下的键被丢弃,他想保护的机器维护列仍然可手改,
而且任何地方都没有诊断说明原因 —— 这正是 spec 侧注释里声明
readonly的理由所反对的那件事(「an undeclared key is silently stripped here, which turns a machine-owned column editable
again with no diagnostic anywhere」),只是上移了一层。也正是 issue 自己引的
objectstack#5435「平台权威不得指向自己闸门会拒绝的键」的反向违例。
因此采用契约忠实的落点:
readonly写在fields的 description 里。ComponentInput本就是扁平的(
name= "must match schema property",无任何嵌套机制),所以数组对象型 input 用散文发布成员键 —— 这是本仓既有体例,非变通:
record:path.stages写'Explicit stage definitions [{ value, label }]',record:alert.action写'{ actionName, label?, variant? }',fields自己原本也写'bare names or {name,label?,icon?,type?}',只是漏了
readonly?。issue 的目的(「AI 作者无法从 manifest 得知该键存在」)由此达成,且不污染契约。
验收逐条证据
①
record:highlights的inputs含readonly(boolean,含语义描述) — ✅ 达成,落点为fields的 description(理由见上)。描述措辞抄自 spec 原文(
RecordHighlightsField.readonly.describe()与HeaderHighlight门实际行为),未自造语义:② 重新生成的
sdui.manifest.json携带该键 — ✅ 实测。sdui.manifest.json不是仓内提交产物(全仓零命中),而是
gen-manifest.ts的构建期产物,由manifestFromConfigs从 registry现场序列化(
packages/sdui-parser/src/index.ts:152description: i.description原样带出)。按gen-manifest.ts同样的调用形状(getPublicConfigs()→manifestFromConfigs)实跑,record:highlights条目:{ "type": "record:highlights", "namespace": "record", "inputs": [ { "name": "fields", "type": "array", "required": true, "description": "Key fields to highlight (1-7), bare names or {name,label?,icon?,type?,readonly?}. Set readonly: true on an entry to render that chip read-only — …" }, { "name": "layout", "type": "enum", "enum": ["horizontal","vertical"], "description": "Layout orientation for highlight fields" } ] }readonly可从 manifest 读到 =true;顶层 input 名单仍为['fields','layout'],与 spec 顶层可接受键一致(
aria按本文件既有注释的理由继续不声明)。③ conformance 审计中该 block 不再是 "zero inputs" —⚠️ 空条:该审计文件从不存在(证据见
上表),且该 block 在
origin/main上本就不是 zero inputs。故无条目可改,也未新建审计文档(超出本单范围)。若维护者确实想要这份 react-blocks conformance 审计,建议单开一单。
生成物 diff 审查
无生成物 diff 可审:
sdui.manifest.json/sdui-intrinsics.d.ts/sdui-blocks.md三者均不在仓内提交(
git ls-tree -r origin/main | grep -E 'sdui\.manifest|sdui-intrinsics|sdui-blocks'零命中),由构建期生成。因此未手改任何生成物;上面 ② 的 manifest 是用官方生成路径实跑打印
出来核对的,不落盘、不提交。源码 diff 仅本键增量:
packages/plugin-detail/src/index.tsx1 处 description 改写 + 说明注释,无其他移动。
反向核验(方向事先预判:两个都应转红,各钉住改动的一半)
undocumented = ['readonly']readonlyinputoffSpec = ['readonly']第二条尤其有意义:新增的门恰好拦住 issue 的字面方案。反向核验后已还原,
grep REVERSE-VERIFICATION零残留。
测试
新增
packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts—— 两个方向都在运行时从 spec 推导,不复述今天的键表:
RecordHighlightsField对象分支的每个键都必须出现在
fieldsdescription 里;本 block 不得声明RecordHighlightsProps不接受的顶层 input。此前仓内没有任何东西交叉校验 registry
inputs与 spec(getPublicConfigs/manifestFromConfigs的使用点里没有这类门),两个漂移方向都是静默的。
全包:
pnpm --workspace-concurrency=2 --filter @object-ui/plugin-detail test→
Test Files 55 passed (55) / Tests 466 passed (466)(基线 54/461,+1 文件 +5 测试;vitest list已确认新文件被默认收集)。type-check→ 干净(tsc --noEmit && tsc -p tsconfig.typetests.json零输出)。lint→ 0 errors(707 warnings 全为未触及文件的既有基线;单独 lint 本次两文件亦 0 errors,27 warnings 均为 barrel 文件既有的
react-refresh/only-export-components)。控制字节:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'三个改动文件零命中。Changeset
.changeset/highlights-readonly-authoring-surface-3407.md,@object-ui/plugin-detail: patch。依据:改动落在
packages/plugin-detail/src/内(check-changeset-presence.mjs的欠账判据),且
sdui.manifest.json是发版包的对外发布面 —— 作者可读到的键集合变了,属用户可见;但无运行时行为变化、纯 additive 文档面,故取 patch(与
.changeset/bulk-action-param-options-open-3309.md这类「类型/声明面放宽、无运行时变化」的同类先例同档)。
未触碰
content/docs/releases/**未动。#3521 在packages/components/src/renderers/layout/containers.tsx,与本单(
packages/plugin-detail/)文件面不相交。Generated by Claude Code