feat(lint,spec): 按 type 分派 ComponentPropsMap —— SDUI props 口袋终于有了 parse (#5068) - #5778
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 05:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:component-props-unknown-key—— props schema 未声明的键,props 包自身这层 + 底下每一个 strip 姿态的对象。走lintUnknownKeysAgainstSchema(本 PR 从packages/spec导出:就是lintUnknownAuthoringKeys每天跑在所有 metadata 集合上的那个 walker 的 per-item 函数体,提取出来而已)。所以 strip/strict/passthrough 的姿态规则、union 分支处理、(p)pipe 两个方向的坑(活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488/ViewItemSchema同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074)、改名建议 —— 全是单一实现,lint 侧一行都没重推。ChartAggregateSchema有活的承载键却没有任何 parse:react 页发布闸门手写重推了它的规则,未知键从不检查(#4001 批 15 的no gate首例) #5020 的教训原文就是「两份实现同一契约会各自漂移」,不再犯一次。component-props-invalid——safeParse的值判词:类型不对、必填缺失、枚举越界。一个键的两种报法收在同一个 id 下:今天 schema 是 strip 姿态,未声明键由 walker 报;将来
strictObject批次关上它们之后,同一件事由safeParse的unrecognized_keys报 —— 都路由到component-props-unknown-key。棘轮把覆盖在两半之间搬,但不会搬出作者的视野,这也是「关姿态」这件事从此有意义的原因。未注册 type 跳过是必须语义:
type是开放联合,光 example 语料就授权了 10 种本 map 不承载的类型、共 87 个节点(flex60 个、object-metric9、object-chart5、record:quick_actions5、record:line_items1 …)。第二层显式覆盖(#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实测确被兑现,并且不止它一处:element:record_pickerlabelFieldrecord-picker.tsx:81读 +:163渲染;:183注册表inputs公开为设计器输入element:record_pickerlabel:130/:143element:record_pickervalueField/emptyText:82/:152、:160page:cardchildrencontainers.tsx:627(schema.body ?? schema.children)page:section/footer/sidebarchildren/bodycontainers.tsx:757/:1407/:1432page:tabsitems[].value/items[].countcontainers.tsx:503-519;probeTargetsrecord:pathstages[].terminalrecord-path.tsx:68('won' | 'lost')反方向也测到了两处「声明了没人读」:
ElementRecordPickerPropsSchema.displayField(必填,objectui 没有任何 record picker 消费点读它)与searchFields/multiple。为什么
labelField最终没有在本 PR 补声明 —— 派发单预授权了「测得被兑现即同批补声明」,但实测出的形状比那句话预设的复杂一层:labelField与displayField是同一概念的两种拼法,被兑现的那个未声明、声明的那个必填且无人读。只补labelField: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内联多语言 maprecord:related_list title同上element:text content同上(I18nLabelSchema声明的,#5728 键表未覆盖)record:related_list add.label同上record:details sections[].label同上(#5611 新增的键)element:button action.params形状分叉(声明为参数定义数组,活消费者接静态载荷 map)element:record_picker displayField必填缺失page:card children未声明page:card visible未声明(把组件级可见性谓词写进了properties,靠 hoist 生效)element:record_picker label/labelField未声明page:tabs items[].key未声明且无人消费(renderer 读的是value)record:path stages[].terminal未声明42/52 是三个已发布平台页把内联多语言 map 写进声明为纯
z.string()的槽位 —— 今天 gate 掉它们,等于用平台自己都不遵守的声明去否掉平台自己的页面(#5020 在groupBy上做过同一判断,只是这里是语料规模)。这就是本 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', …),整个注册表)测量:接线前:
接线后:
五条方向全部与预测一致。探针随后转成
validate-component-props.test.ts的常设断言。ledger 流转与「三处记录」
component.zod.ts的 strictness ledger 行按 #5020 先例从no gate改authorable(判词手写,数字由gen:strictness-ledger重算:no gate桶 30 → 0,该类清空;authorable13 → 43)。component.test.ts那条「properties 一旦有 typed dispatch 即转红」的断言,写的时候预设的是方向 B。实际落地的是方向 A(闸门在 lint 侧),schema 层一个字没改 —— 所以它连同另外两条断言全部保持绿色(实测:component.test.ts117 passed)。三处记录已按实测改写:component.zod.ts文件头:新增「SDUI 组件 props 没有解析闸门:PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068: THE GATE IS WIRED」段,明写这次翻转没有做的三件事(载体形状未变、姿态未变、存储路径仍然不校验);component.test.tspin:describe 改名,断言注释改成「方向 B 被否决,所以这里保持绿;将来真转红意味着载体被重塑 —— 那是协议变更,不是 lint 变更」;no gate汇总条目。不留自相矛盾的记录。
验证
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 修)
page:tabs上授权items[].key,renderer 读的是items[].value—— 两个 tab 的稳定 URL token 从未生效 #5776 —— showcase 的project-detail在page:tabs上写items[].key,renderer 读items[].value,两个 tab 的稳定 URL token 从未生效;InlineActionSchema.params声明为参数定义数组,但type:'api'的活消费者(与 showcase 页)用的是静态载荷 map —— 一个键两种形状 #5777 ——InlineActionSchema.params一个键两种形状(参数定义数组 vs 静态载荷 map,ActionRunner两种都接);element:text.content的 8 处内联多语言 map,与该 issue 同族但键表未覆盖,方案 B(放宽I18nLabelSchema)覆盖不到它,需在同一次裁定里点名。后续
error 升级是独立一步,前置是:warning 期违例清零(#5728 / #5775 / #5776 / #5777)。存储路径(
saveMetaItem/ REST/meta)仍不校验 props 包 —— #4463 的第四面墙,本 PR 如实记录、未修。Generated by Claude Code