从 #3405 拆出。那个 issue 的方案第 3 条当时标注「影响面比前两条大得多,建议单独评估、必要时拆出去做」,#3746 只把 ActionParamSchema 一个 收紧了。导致 #3405 的机制在全仓其余地方原样保留。
问题
zod 的默认是 .strip:schema 没声明的键被静默丢弃 ,解析照样成功。作者写了一个语义正确的键,得到的唯一反馈是一个行为不对的控件 —— 没有报错、没有告警、tsc 全绿。
这不是假想的失效模式,它已经真实发生过至少两次:
对 AI 作者 尤其致命:人看到控件不对会去翻;AI 拿到一个成功响应,然后报告「已完成」。这正是 ADR-0049 为安全属性、ADR-0054 为集成、ADR-0078 为实例完整性各自命名过的同一种不对称 —— 静默失效比硬报错更坏,因为它制造虚假的完成。
现状测量(origin/main,packages/spec/src/**/*.zod.ts)
z.object 站点: .strict()=31 .passthrough()=20 默认 strip=1885
按目录:
站点数
目录
性质
422
api/
多为线上/响应 形状
383
system/
运行时
340
kernel/
插件契约
177
ui/
可授权
149
data/
可授权
83
cloud/
运行时
80
automation/
可授权
72
ai/
混合
64
integration/
连接器载荷
34
identity/
混合
27
studio/
可授权
23
shared/
混合
20
security/
可授权
6
qa/
—
建议:先分类,不要一次性全改
1885 不是目标数。 有相当一部分本就该保持宽松 —— 外部 API 响应、连接器载荷、故意开放的用户数据形状,对它们 strict 会把上游加一个字段变成己方的解析崩溃。
真正的高价值目标是可授权面 :作者手写进 *.object.ts / 配置里的那些 —— ui + data + automation + security + studio ≈ 453 个站点 。那里的静默剥离直接损害作者,而 strict 的下游风险最低(输入是本仓与租户的源码,不是第三方响应)。
建议做法:
先分类 ,产出一份「可授权 / 线上 / 开放」三分账,判据是「这个 schema 的输入由谁写」。分类结果本身就该落盘(类似 liveness ledger),而不是一次性判断。
按 ADR-0054 的棘轮方式推进 :先做审计确认过的高价值形状,长尾挂在验证通过上、不挂日期。
每一步都要能实证零破坏 ,而不是靠推断(见下)。
已有机制,不要另造
这件事需要的零件已经全在仓库里 ,#3746 只是把它们接了起来:
shared/suggestions.zod.ts —— findClosestMatches / levenshteinDistance / formatSuggestion,以及 FIELD_TYPE_ALIASES 这个「别名表 + 编辑距离」的成熟范式
data/object.zod.ts —— UNKNOWN_KEY_GUIDANCE(退役键的墓碑 + 升级处方)与 suggestKey 的长度相对 距离上界
shared/visibility.ts —— strictVisibilityError,ADR-0089 D3a 给 view/page schema 做 strict 的先例
shared/error-map.zod.ts —— 已有通用的 unrecognized_keys 文案
ui/action.zod.ts(feat(spec): reject unknown keys on an action param instead of stripping them (#3405) #3746 )—— 本次的模板:语义别名表 + 长度相对的 findClosestMatches 兜底
关键要求:strict 必须配可修的错误信息,不能只是「大声」。 光报 "unrecognized key" 只是把静默失效换成了困惑;要点名那个键,并在能识别时给出正确拼法。#3746 里最有价值的一条别名不是错别字,而是 visibleWhen → visible —— ADR-0089 让 visibleWhen 成为 view/page 的正统拼法,在动作参数上借用它过去会把参数的能力开关整个剥掉、让它无条件渲染 ,一个静默失效的权限门。
#3746 的成本实证(这条支持推进)
单个 schema 收紧的实测代价接近零:
@objectstack/spec 258 文件 / 6716 用例通过,tsc --noEmit 干净
app-showcase / app-crm / app-todo 三个示例应用 validate 全过 —— 仓库里没有任何既有元数据带未声明的动作参数键
下游消费包回归全过(lint 467 / metadata 276 / platform-objects 223 / metadata-core 100 / metadata-protocol 70 / sdui-parser 6)
全部 content/docs/**/*.mdx 扫过:0 份文档 在教用户写会被新校验拒绝的键
踩过的坑(照做能省一轮 CI)
#3746 用了三次 CI 才绿,失败点都不在收紧逻辑本身:
.strict() 会让生成的参考文档漂移。 scripts/build-docs.ts 的 getFileDescription() 取模块里第一个 /** */ 块原样作为参考页描述 —— 在 schema 的文件级 JSDoc 之前 插入辅助代码块,会把公开文档页的内容换成你的内部注释。check:docs 会抓到,但别顺手提交重新生成的 mdx(那等于把回归发出去),要把代码块挪到那段 JSDoc 之后 。
packages/spec 有十个 check:* 闸门,check:docs 只是第一个。 一次跑完再推:docs / skill-refs / skill-docs / api-surface / spec-changes / upgrade-guide / liveness / react-blocks / react-conformance / skill-examples。check:api-surface 读的是构建产物 ,改完要先 build 才反映。
别导出 error map。 只在本模块用就设为模块私有,否则进公共 API 面、check:api-surface 判罚。
lazySchema 的注释里记着一个隐患 :ADR-0089 D3a 把 FormFieldSchema / PageComponentSchema 改成 .strict().transform(…) 管道时,触发过 zod toJSONSchema 遍历在 Proxy 上的 seen 查找失效(Cannot set properties of undefined (setting 'ref'))。批量收紧时这条要盯。
参考
从 #3405 拆出。那个 issue 的方案第 3 条当时标注「影响面比前两条大得多,建议单独评估、必要时拆出去做」,#3746 只把
ActionParamSchema一个 收紧了。导致 #3405 的机制在全仓其余地方原样保留。问题
zod 的默认是
.strip:schema 没声明的键被静默丢弃,解析照样成功。作者写了一个语义正确的键,得到的唯一反馈是一个行为不对的控件 —— 没有报错、没有告警、tsc全绿。这不是假想的失效模式,它已经真实发生过至少两次:
{ type: 'lookup', reference: 'sys_user' },reference被吃掉,动作参数弹窗降级成「粘贴记录 ID(UUID)」文本框。真人基本没法用(PLAT-DEF-005,天顺 EHR 质检派工)。workflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535 —— 对象级workflows: [...]被吃掉,作者以为接好了自动化,实际发布了一份死元数据。对 AI 作者尤其致命:人看到控件不对会去翻;AI 拿到一个成功响应,然后报告「已完成」。这正是 ADR-0049 为安全属性、ADR-0054 为集成、ADR-0078 为实例完整性各自命名过的同一种不对称 —— 静默失效比硬报错更坏,因为它制造虚假的完成。
现状测量(
origin/main,packages/spec/src/**/*.zod.ts)按目录:
api/system/kernel/ui/data/cloud/automation/ai/integration/identity/studio/shared/security/qa/建议:先分类,不要一次性全改
1885 不是目标数。 有相当一部分本就该保持宽松 —— 外部 API 响应、连接器载荷、故意开放的用户数据形状,对它们 strict 会把上游加一个字段变成己方的解析崩溃。
真正的高价值目标是可授权面:作者手写进
*.object.ts/ 配置里的那些 ——ui+data+automation+security+studio≈ 453 个站点。那里的静默剥离直接损害作者,而 strict 的下游风险最低(输入是本仓与租户的源码,不是第三方响应)。建议做法:
已有机制,不要另造
这件事需要的零件已经全在仓库里,#3746 只是把它们接了起来:
shared/suggestions.zod.ts——findClosestMatches/levenshteinDistance/formatSuggestion,以及FIELD_TYPE_ALIASES这个「别名表 + 编辑距离」的成熟范式data/object.zod.ts——UNKNOWN_KEY_GUIDANCE(退役键的墓碑 + 升级处方)与suggestKey的长度相对距离上界shared/visibility.ts——strictVisibilityError,ADR-0089 D3a 给 view/page schema 做 strict 的先例shared/error-map.zod.ts—— 已有通用的unrecognized_keys文案ui/action.zod.ts(feat(spec): reject unknown keys on an action param instead of stripping them (#3405) #3746)—— 本次的模板:语义别名表 + 长度相对的findClosestMatches兜底关键要求:strict 必须配可修的错误信息,不能只是「大声」。 光报 "unrecognized key" 只是把静默失效换成了困惑;要点名那个键,并在能识别时给出正确拼法。#3746 里最有价值的一条别名不是错别字,而是
visibleWhen → visible—— ADR-0089 让visibleWhen成为 view/page 的正统拼法,在动作参数上借用它过去会把参数的能力开关整个剥掉、让它无条件渲染,一个静默失效的权限门。#3746 的成本实证(这条支持推进)
单个 schema 收紧的实测代价接近零:
@objectstack/spec258 文件 / 6716 用例通过,tsc --noEmit干净validate全过 —— 仓库里没有任何既有元数据带未声明的动作参数键content/docs/**/*.mdx扫过:0 份文档在教用户写会被新校验拒绝的键踩过的坑(照做能省一轮 CI)
#3746 用了三次 CI 才绿,失败点都不在收紧逻辑本身:
.strict()会让生成的参考文档漂移。scripts/build-docs.ts的getFileDescription()取模块里第一个/** */块原样作为参考页描述 —— 在 schema 的文件级 JSDoc 之前插入辅助代码块,会把公开文档页的内容换成你的内部注释。check:docs会抓到,但别顺手提交重新生成的 mdx(那等于把回归发出去),要把代码块挪到那段 JSDoc 之后。packages/spec有十个check:*闸门,check:docs只是第一个。 一次跑完再推:docs / skill-refs / skill-docs / api-surface / spec-changes / upgrade-guide / liveness / react-blocks / react-conformance / skill-examples。check:api-surface读的是构建产物,改完要先build才反映。check:api-surface判罚。lazySchema的注释里记着一个隐患:ADR-0089 D3a 把FormFieldSchema/PageComponentSchema改成.strict().transform(…)管道时,触发过 zodtoJSONSchema遍历在 Proxy 上的seen查找失效(Cannot set properties of undefined (setting 'ref'))。批量收紧时这条要盯。参考
validate-functional-completenesslint 都不存在,目前全靠逐案例引用)workflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535(对象级workflows)docs/audits/2026-06-metadata-functional-completeness.md