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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…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>
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