Skip to content

fix(plugin-detail): record:highlights 的 fields 声明补上 readonly,让 manifest 能被作者读到 (#3407) - #3795

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3407-highlights-readonly-input
Aug 8, 2026
Merged

fix(plugin-detail): record:highlights 的 fields 声明补上 readonly,让 manifest 能被作者读到 (#3407)#3795
yinlianghui merged 1 commit into
mainfrom
claude/issue-3407-highlights-readonly-input

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3407
Part of objectstack-ai/objectstack#5176(spec 侧 PR objectstack-ai/objectstack#5607 已 MERGED)

摘要

record:highlights发布面补齐:fields input 的 description 现在写出完整条目形状
{name,label?,icon?,type?,readonly?} 并说明 readonly 的语义。registry inputs 正是
packages/sdui-parser/scripts/gen-manifest.ts 序列化进 sdui.manifest.json 的东西,所以这
一行就是 AI 作者能否从 manifest 得知该键存在的全部差别。

readonly 早已两侧兑现:renderer 归一化逐条拷贝(renderers/record-highlights.tsx:70
readonly: f?.readonly === true),HeaderHighlight 可编辑门据此拒绝内联编辑
(objectstack#5077);spec 已在 RecordHighlightsField 上声明(#5607)。缺的只有本仓 description。

⚠️ 前提核验:issue 的三条陈述有两条已失效,第三条需要改写落点

按 AGENTS.md「issue 是线索不是规格」,动码前逐条核到 origin/main(objectui 00b9451d8,
objectstack d42a92fc6):

issue 陈述 核验结果
该 block 是 "zero inputs" 不成立origin/mainrecord:highlights 已有 2 个 inputs(fieldslayout),自 #2113 / #3027 起就有
依据是 docs/audits/2026-06-react-blocks-conformance.md 该文件不存在git 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):

RecordHighlightsField 对象分支 keys: [ 'name', 'label', 'icon', 'type', 'readonly' ]
RecordHighlightsProps  顶层 keys:   [ 'fields', 'layout', 'aria' ]

RecordHighlightsProps.parse({ fields: ['amount'], readonly: true })
  -> {"fields":["amount"],"layout":"horizontal"}      # readonly 被静默剥掉,不报错
RecordHighlightsProps.parse({ fields: [{ name: 'amount', readonly: true }] })
  -> {"fields":[{"name":"amount","readonly":true}],"layout":"horizontal"}   # 逐条保留

若按 issue 字面加顶层 { name: 'readonly', type: 'boolean' },后果是发布一个平台默默丢弃的键:

  1. 生成的 sdui.manifest.jsonsdui-intrinsics.d.ts 会宣告 RecordHighlights 有 readonly prop;
  2. packages/sdui-parser/src/validate.ts 的 manifest 门遍历节点顶层 prop(Object.entries(node)
    comp.inputs),不下探 fields[] 条目,因此顶层 readonly 被判为「已知 prop」,零诊断;
  3. spec 是普通 z.object,parse 时把未知顶层键无声剥掉(上方实测);
  4. renderer 读的是逐条 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:highlightsinputsreadonly(boolean,含语义描述) — ✅ 达成,落点为
fields 的 description(理由见上)。描述措辞抄自 spec 原文
(RecordHighlightsField.readonly.describe()HeaderHighlight 门实际行为),未自造语义:

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 — it suppresses the inline-edit
affordance and the HeaderHighlight editability gate enforces it. Use it for
hook/automation-maintained columns that must not be hand-edited from the record header;
marking the OBJECT field readonly instead would also strip the hook's own write-back.

② 重新生成的 sdui.manifest.json 携带该键 — ✅ 实测。sdui.manifest.json 不是仓内提交
产物(全仓零命中),而是 gen-manifest.ts 的构建期产物,由 manifestFromConfigs 从 registry
现场序列化(packages/sdui-parser/src/index.ts:152 description: 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.tsx
1 处 description 改写 + 说明注释,无其他移动。

反向核验(方向事先预判:两个都应转红,各钉住改动的一半)

反向操作 预判 实测
description 退回旧文本 「every spec entry key is discoverable」转红 ✅ 红,undocumented = ['readonly']
按 issue 字面加顶层 readonly input 「declares no top-level input the spec does not accept」转红 ✅ 红,offSpec = ['readonly']

第二条尤其有意义:新增的门恰好拦住 issue 的字面方案。反向核验后已还原,grep REVERSE-VERIFICATION
零残留。

测试

新增 packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts —— 两个
方向都在运行时从 spec 推导,不复述今天的键表:RecordHighlightsField 对象分支的每个键都必须
出现在 fields description 里;本 block 不得声明 RecordHighlightsProps 不接受的顶层 input。
此前仓内没有任何东西交叉校验 registry inputs 与 spec(getPublicConfigs/manifestFromConfigs
的使用点里没有这类门),两个漂移方向都是静默的。

✓ record:highlights — registry inputs vs @objectstack/spec > is registered with a non-empty `inputs` surface
✓ … > the spec really carries `readonly` per ENTRY, not top-level
✓ … > a top-level `readonly` is silently stripped by the spec, so it must not be published
✓ … > every spec entry key is discoverable from the `fields` input description
✓ … > declares no top-level input the spec does not accept
Test Files 1 passed (1) | Tests 5 passed (5)

全包: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/** 未动。#3521packages/components/src/renderers/layout/containers.tsx,
与本单(packages/plugin-detail/)文件面不相交。


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 8, 2026 4:22pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CunB10T0.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 482.22KB 106.21KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.85KB 3.18KB
data-objectstack (index.js) 139.51KB 35.97KB
fields (index.js) 230.90KB 56.84KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 117.06KB 30.24KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 233.16KB 57.60KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.71KB 49.68KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。

对 issue 字面验收偏离的裁定:采纳 dev 的契约忠实落点。 三条实测把字面方案证死:(1) RecordHighlightsProps 顶层 keys 为 [fields,layout,aria],readonlyRecordHighlightsField 逐条键;(2) parse({fields:[…], readonly:true}) 静默剥掉顶层 readonly 且零诊断 —— 字面加顶层 input 就是把一个平台默默丢弃的键写进权威 manifest,恰是 issue 自引 objectstack#5435 的反向违例;(3) issue 引据的 docs/audits/2026-06-react-blocks-conformance.md 全历史零命中(从不存在),"zero inputs" 前提亦不成立(fields/layout 早在)。落点改为 fields description(与 record:path.stagesrecord:alert.action 同体例,实跑 gen 链核实该文案确实进入 manifest)达成 issue 的目的(AI 作者可从 manifest 得知该键),另加 5 条 spec 推导 parity 测试双向钉扎 —— 反向验证两方向先判后跑均吻合,且方向 (b) 证明新门恰好拦住字面方案。

git 实物核验(b126f43f):净 diff 3 文件(plugin-detail 落点与在飞 #3521 的 components 文件面不相交);19 项 CI 全部完成 0 失败;changeset patch 依据同类先例(#3309)成立,三道 changeset 门绿。

长期方向:dev 两轴分析 B(现在)→ C(ComponentInput 成员形状声明位,跨 types/core/sdui-parser)成立;C 的档案记在 #3797 判级评论,不进决策箱。衍生 #3797(4 个 block 发布 pin 版 spec 不接受的顶层 input)另行判级。dev 自查纠错一则入方法库:占位 GITHUB_TOKEN + .get('check_runs',[]) 回退把空响应读成 ALL_DONE —— 自查假绿工具第三例,结论作废后用已鉴权通道重取才作数。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 16:31
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 7b3e048 Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3407-highlights-readonly-input branch August 8, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record:highlights block 的 inputs 声明 readonly,使其进入 sdui.manifest.json(objectstack#5176 裁定 A 的 objectui 半边)

2 participants