Skip to content

feat(lint,spec): 按 type 分派 ComponentPropsMap —— SDUI props 口袋终于有了 parse (#5068) - #5778

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5068-component-props-gate
Aug 6, 2026
Merged

feat(lint,spec): 按 type 分派 ComponentPropsMap —— SDUI props 口袋终于有了 parse (#5068)#5778
os-zhuang merged 1 commit into
mainfrom
claude/issue-5068-component-props-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5068

按维护者 2026-08-05 的裁定走方向 A:在 publish / lint 侧按 type 分派 ComponentPropsMap 解析,未注册 type 跳过,本 PR 只落 warning 级。方向 B(把 properties 改判别式 schema)未做 —— 已被否决,page.zod.ts 一个字没动。

做了什么

新规则 packages/lint/src/validate-component-props.ts,进 authoring-rules.ts 共享注册表(一条 entry,os validate / os build / os lint 同时生效,不在命令里手接线)。一次分派、两个诊断 id:

一个键的两种报法收在同一个 id 下:今天 schema 是 strip 姿态,未声明键由 walker 报;将来 strictObject 批次关上它们之后,同一件事由 safeParseunrecognized_keys 报 —— 都路由到 component-props-unknown-key。棘轮把覆盖在两半之间搬,但不会搬出作者的视野,这也是「关姿态」这件事从此有意义的原因。

未注册 type 跳过是必须语义:type 是开放联合,光 example 语料就授权了 10 种本 map 不承载的类型、共 87 个节点(flex 60 个、object-metric 9、object-chart 5、record:quick_actions 5、record:line_items 1 …)。

第二层显式覆盖(#5607 的更正):readonly 挂在 RecordHighlightsField 联合体的对象成员上,在 fields[] 数组项里,authorable-surface walk 严格一层到不了。本闸门到得了,两个方向都钉了测试 —— 声明过的 readonly 必须静默,拼错的 readOnly 必须报出来并指名正确拼法。只钉一个方向没用:静默有可能是「走到了并认可」,也可能是「walk 停在 fields 上」,两者要靠另一半区分。

顺带把 #5020 的 zod 拒绝渲染器(union 塌缩解包 + 回显作者写的值)提取成 zod-issue-format.ts,两个闸门共用一份,不是抄一份。

硬前置:未声明键扫描(2026-08-04 评论要求)

开闸前在 objectui(只读)实测「renderer 真实消费但 ComponentPropsMap 未声明」的键。#5176(readonly)与 #5611(sections/hideFields)已清,record picker 的 labelField 实测确被兑现,并且不止它一处:

type objectui 消费点 语料授权 处置
element:record_picker labelField record-picker.tsx:81 读 + :163 渲染;:183 注册表 inputs 公开为设计器输入 是(showcase) ⚠️ 未补声明 —— 见下
element:record_picker label :130 / :143 同上
element:record_picker valueField / emptyText :82/:152:160 同上
page:card children containers.tsx:627(schema.body ?? schema.children) 是(2 处) 记录,#5775
page:section/footer/sidebar children/body containers.tsx:757/:1407/:1432 记录,#5775
page:tabs items[].value / items[].count containers.tsx:503-519;probeTargets 记录,#5775
record:path stages[].terminal record-path.tsx:68('won' | 'lost') 记录,#5775

反方向也测到了两处「声明了没人读」:ElementRecordPickerPropsSchema.displayField(必填,objectui 没有任何 record picker 消费点读它)与 searchFields / multiple

为什么 labelField 最终没有在本 PR 补声明 —— 派发单预授权了「测得被兑现即同批补声明」,但实测出的形状比那句话预设的复杂一层:labelFielddisplayField 是同一概念的两种拼法,被兑现的那个未声明、声明的那个必填且无人读。只补 labelField:

  1. 会让 schema 里同时站着两种拼法 —— Prime Directive Add comprehensive test suite for Zod schema validation #12 明令禁止的方言;
  2. 并不能让闸门闭嘴 —— 只写 labelField 的合规页面仍会因缺必填 displayField 报 warning(showcase 的 page-variables.page.ts 今天正是这一条)。

即这不是「补一个键」而是「哪个拼法是正统」的契约裁定(照 #5611 先例应当是 labelField 转正 + displayField 走 ADR-0087 退休,但那是 breaking)。按「不猜契约」的规矩,零改动、如实记录,选项与代价写在 #5775。warning 级下的风险为零:闸门只报告,不剥离,properties 在 schema 层仍是原样保留 —— #5077 那类「接闸即静默剥离」的腐蚀要到 error/strip 阶段才可能发生。

warning 期违例清单(error 升级的验收基线)

对全部 25 个已授权页面(example 语料 + 3 个平台页)跑闸门:52 条,全部 warning

分类 条数 归属
page:tabs items[].label 内联多语言 map 14 #5728(裁定中)
record:related_list title 同上 13 #5728
element:text content 同上(⚠️ 该键不是 I18nLabelSchema 声明的,#5728 键表未覆盖) 8 已评论进 #5728
record:related_list add.label 同上 5 #5728
record:details sections[].label 同上(#5611 新增的键) 2 #5728
element:button action.params 形状分叉(声明为参数定义数组,活消费者接静态载荷 map) 1 #5777
element:record_picker displayField 必填缺失 1 #5775
page:card children 未声明 2 #5775
page:card visible 未声明(把组件级可见性谓词写进了 properties,靠 hoist 生效) 1 #5775
element:record_picker label / labelField 未声明 2 #5775
page:tabs items[].key 未声明且无人消费(renderer 读的是 value) 2 #5776
record:path stages[].terminal 未声明 1 #5775

42/52 是三个已发布平台页把内联多语言 map 写进声明为纯 z.string() 的槽位 —— 今天 gate 掉它们,等于用平台自己都不遵守的声明去否掉平台自己的页面(#5020groupBy 上做过同一判断,只是这里是语料规模)。这就是本 PR 只落 warning 的实测理由。

另外注意 element:record_picker 的必填 object 没有进清单:该组件通过 dataSource.object 绑定,而 ElementDataSourceSchema 就是声明来覆盖 props 侧简写的,objectui 的 element renderer 也是 ds.object ?? props.object 这个优先级(page-walk.ts 编码的也是它)。报它是错判而不是严格,所以显式抑制并钉了测试(两个方向:有 dataSource 时不报、没有时照报)。

先证红(方向预先写死,再跑)

预测:接线前 —— 探针 1/2/4 应该报而实际一条不报(这就是洞);探针 3(未注册 type)与 5(第二层已声明的 readonly)两态都必须静默。经由同一扇门(runAuthoringRules('validate', …),整个注册表)测量:

接线前:

[PROBE] 1. undeclared key on page:header                      component-props findings: 0
[PROBE] 2. wrong value type on record:related_list            component-props findings: 0
[PROBE] 3. unregistered type (must stay silent)               component-props findings: 0
[PROBE] 4. undeclared key at layer 2 (highlights fields[])    component-props findings: 0
[PROBE] 5. declared `readonly` at layer 2 (#5176)             component-props findings: 0
  other findings: (none)   ← 每一条都是「整个注册表零输出」

接线后:

[PROBE] 1  warning component-props-unknown-key  …properties.titel
   `titel` is not a prop `page:header` declares … Did you mean `title`?
[PROBE] 2  warning component-props-invalid      …properties.limit
   limit: Invalid input: expected number, received string
[PROBE] 3  component-props findings: 0          ← 两态一致,预测命中
[PROBE] 4  warning component-props-unknown-key  …properties.fields.0.readOnly
   `readOnly` is not a prop `record:highlights` declares … Did you mean `readonly`?
[PROBE] 5  component-props findings: 0          ← 两态一致,预测命中

五条方向全部与预测一致。探针随后转成 validate-component-props.test.ts 的常设断言。

ledger 流转与「三处记录」

component.zod.ts 的 strictness ledger 行按 #5020 先例从 no gateauthorable(判词手写,数字由 gen:strictness-ledger 重算:no gate 桶 30 → 0,该类清空;authorable 13 → 43)。

⚠️ 三处常设记录里有一处的措辞需要更正,而更正来自实测而不是预测:component.test.ts 那条「properties 一旦有 typed dispatch 即转红」的断言,写的时候预设的是方向 B。实际落地的是方向 A(闸门在 lint 侧),schema 层一个字没改 —— 所以它连同另外两条断言全部保持绿色(实测:component.test.ts 117 passed)。三处记录已按实测改写:

  1. component.zod.ts 文件头:新增「SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068: THE GATE IS WIRED」段,明写这次翻转没有做的三件事(载体形状未变、姿态未变、存储路径仍然不校验);
  2. component.test.ts pin:describe 改名,断言注释改成「方向 B 被否决,所以这里保持绿;将来真转红意味着载体被重塑 —— 那是协议变更,不是 lint 变更」;
  3. ledger 两行 + no gate 汇总条目。

不留自相矛盾的记录。

验证

pnpm --filter @objectstack/lint test       → 61 files / 1406 tests passed
pnpm --filter @objectstack/spec test       → 317 files / 8106 tests passed
pnpm --filter @objectstack/cli test        → 83 files / 825 tests passed
pnpm --filter @objectstack/{spec,lint,cli} typecheck → Done ×3
pnpm --filter @objectstack/spec check:generated      → All 10 generated artifacts are up to date
node scripts/check-nul-bytes.mjs           → OK (5649 files, no raw control bytes)

CLI 套件是按「规则消费半径」扫的(#5046 的教训:规则在哪儿跑,fixture 就在哪儿),第一次跑 46 个文件红 —— 全是新工作树里依赖未构建(Failed to resolve entry for package …),pnpm --filter '@objectstack/cli^...' build 之后 83/83 全绿,与本改动无关。

authorable-surface.base.json 的重锚漂移照 #5358 剔除(未进提交):gen:schema 会把 baseRev 重锚到合并基,顺带带进别的 PR 的键。

范围外发现(均已归档,未在本 PR 修)

后续

error 升级是独立一步,前置是:warning 期违例清零(#5728 / #5775 / #5776 / #5777)。存储路径(saveMetaItem / REST /meta)仍不校验 props 包 —— #4463 的第四面墙,本 PR 如实记录、未修。


Generated by Claude Code

…s bag gets its parse (#5068)

`PageComponent.properties` is `z.record(z.string(), z.unknown())`, and
ADR-0089 D3a strictness does not recurse into it, so the 31 typed prop
schemas in `ComponentPropsMap` were parsed by nothing (#4001 batch 17's
`no gate` verdict). objectui's SchemaRenderer hoists the bag and spreads
every key it carries, so a misspelled prop is neither rejected nor
dropped — it reaches the renderer and is ignored there.

New advisory rule `validate-component-props` (packages/lint), wired into
the shared authoring registry so `os validate` / `os build` / `os lint`
all run it. It dispatches on the component's `type` and emits two ids:
`component-props-unknown-key` (via the spec's own authoring-key walker,
newly exported as `lintUnknownKeysAgainstSchema` so the posture rules are
not re-derived) and `component-props-invalid` (via `safeParse`). A strict
props schema routes its `unrecognized_keys` to the first id, so a future
`strictObject` batch moves coverage between the halves without moving it
out of the author's view.

Unregistered `type`s are skipped: `type` is an open union and the example
corpus alone authors 87 nodes of ten types this map does not carry.

Warning-level only, deliberately. The live corpus violates the
declarations in 52 places, 42 of which are open contract questions
(#5728's inline i18n label maps, #5775's declared-but-unread record-picker
props), so gating today would fail the platform's own pages. The
inventory is the acceptance baseline for the error upgrade.

`component.zod.ts` moves `no gate` -> `authorable` in the strictness
ledger. The carrier itself is unchanged (direction B declined), so
`component.test.ts`'s three standing assertions stay green — measured,
and their prose updated to say which dispatch landed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercel Bot commented Aug 6, 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)
objectstack Ignored Ignored Aug 6, 2026 4:58am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 05:14
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 73e576f Aug 6, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5068-component-props-gate branch August 6, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

2 participants