在做 objectui#3276(修 validation 样例写反的 condition、删掉它 3 个被静默剥掉的键)时,按 PM 的要求把所有样例机械过了一遍,找出「作者写了、但 parse 之后根本不存在」的键。#3276 的 scope 只有 validation 和 skill,剩下的记这里(Prime Directive #10)。
怎么测的
把每个样例按 preview-samples-spec-valid.test.ts 的方式嵌进 ObjectStackSchema,safeParse 之后把输入对象和输出对象逐层 key 比对,凡是输入有、输出没有的就是被剥掉的:
diff(at(stack, path), at(result.data, path))
这正是 #3257 那个守卫在 LIMIT 注释里自己声明测不到的部分 —— views / jobs / emailTemplates 不是 .strict(),未知键被剥而不是被拒,所以这三个样例现在全都「PASS」。
结果(修完 #3276 之后的当前 main)
validation、skill、report、app、action、flow、agent、tool、permission、position、datasource 干净;object/page/dashboard/translation 本来就 parse 失败(KNOWN_STALE),不适用。剩下三个:
| 样例 |
被剥掉的键 |
spec 现状 |
view |
name、label、object、list.object |
ViewSchema 是个容器,只有 list / form / listViews / formViews(+ protection)。它没有任何身份键 |
job |
concurrency、timeoutMs |
JobSchema 没有 concurrency;超时键叫 timeout(同样是毫秒),timeoutMs 是拼错的 |
email_template |
from、to |
EmailTemplateDefinitionSchema 的发件人覆盖键叫 fromOverride;to 压根不是授权期元数据 —— 收件人是 IEmailService.sendTemplate({ template, to, … }) 的运行时入参 |
逐条分析
1. job.timeoutMs —— 和 #3266 修过的 datasource.interval 是同一个错
样例写 timeoutMs: 600000,spec 的键是 timeout('Per-attempt time limit in milliseconds')。照抄的人得到一个没有超时的 job,而且没有任何反馈 —— 键名错了就是被剥掉。这和 #3266 修 healthCheck.interval → intervalMs 完全同型,只是方向相反(那次是漏了 Ms,这次是多了 Ms)。
concurrency: 1 则是 JobSchema 上不存在的概念,没有对应键可改,建议直接删。
2. email_template.from / to —— 一个拼错、一个层级错
顺带:样例的 subject / bodyHtml 用的是 ${var} 占位符,而 spec 的 docstring 写的是 {{var.path}}。这个不影响 parse(都是字符串),但如果 {{}} 才是渲染器认的语法,样例现在教的是错的 —— 需要确认后一并改。
3. view —— 不是「多写了键」,是映射本身存疑,和 KNOWN_STALE 里的 translation 同型
ObjectStackSchema.views 是 z.array( ViewSchema ),而 ViewSchema 是个没有身份键的容器。于是样例的 name: 'open_orders' / label: 'Open Orders' / object: 'sales_order' 三个键全部蒸发,解析结果只剩 { list: { type, columns } } —— 一个不知道自己叫什么、也不知道挂在哪个对象上的列表视图。
spec 自己在 defineView 的 docstring 里警告过相邻的坑:
ViewSchema strips unknown top-level keys, so a flat list view ({ name, label, type, columns, … }) would parse to an empty container — zero views register and the Console silently renders nothing.
我们的样例是「半扁平」:因为有 list 所以不是空容器,视图能注册,但身份键照样丢光。
这个不能靠删键修。真正的问题是:一个放在 views[] 数组里的容器,凭什么绑定到某个对象?要么 stack 层的 views 应该是 Record< objectName, ViewSchema > 而不是数组,要么样例根本该表达成对象上的 listViews: { open_orders: {…} }。先定这个契约,再改样例 —— 和 KNOWN_STALE 里 translation 那行留的判断是同一类。
⚠️ 注意 view 现在在 SPEC_CLEAN 里。它「通过」只是因为剥掉之后剩下的东西恰好合法,不代表样例是对的。
建议
job:timeoutMs → timeout,删 concurrency;
email_template:from → fromOverride(按 EmailAddressInlineSchema 的形态写),删 to;顺带确认占位符语法是 ${} 还是 {{}};
view:先定契约(数组容器怎么绑对象),别急着改样例;
- 加一个机械的「静默剥键」检查(下详)。
关于第 4 条:这不是语义断言,别和 #3276 拒掉的那件事混了
#3276 明确不让把规则语义的断言(「condition 的方向对不对」)塞进 preview-samples-spec-valid.test.ts —— 那个守卫的职责是「能不能发布」,同意。
但「作者写的键 parse 后还在不在」是结构性的,不是语义性的:它不需要理解规则的意思,只要比对 parse 前后的 key 集合,而且它恰好补上了那个守卫在自己 LIMIT 注释里承认的盲区。上面 5 个键、加上 #3276 修掉的 4 个(validation 的 object/field/expression、skill 的 type),9 个全是它一次跑出来的。
要做的话,建议单独一个 test(可以同文件、独立 it,也可以独立文件),带自己的 ledger —— view 那三行在契约定下来之前只能挂账,不能当成 pass。做成什么形态由接手的人定,这里只提供依据和现成的比对逻辑。
相关
⚠️ 未指派 —— 由 #3276 顺带发现并记录,不在那个 PR 里修:scope fence 是 validation / skill 两个样例。
在做 objectui#3276(修
validation样例写反的condition、删掉它 3 个被静默剥掉的键)时,按 PM 的要求把所有样例机械过了一遍,找出「作者写了、但 parse 之后根本不存在」的键。#3276 的 scope 只有validation和skill,剩下的记这里(Prime Directive #10)。怎么测的
把每个样例按
preview-samples-spec-valid.test.ts的方式嵌进ObjectStackSchema,safeParse之后把输入对象和输出对象逐层 key 比对,凡是输入有、输出没有的就是被剥掉的:这正是 #3257 那个守卫在 LIMIT 注释里自己声明测不到的部分 ——
views/jobs/emailTemplates不是.strict(),未知键被剥而不是被拒,所以这三个样例现在全都「PASS」。结果(修完 #3276 之后的当前 main)
validation、skill、report、app、action、flow、agent、tool、permission、position、datasource干净;object/page/dashboard/translation本来就 parse 失败(KNOWN_STALE),不适用。剩下三个:viewname、label、object、list.objectViewSchema是个容器,只有list/form/listViews/formViews(+protection)。它没有任何身份键jobconcurrency、timeoutMsJobSchema没有concurrency;超时键叫timeout(同样是毫秒),timeoutMs是拼错的email_templatefrom、toEmailTemplateDefinitionSchema的发件人覆盖键叫fromOverride;to压根不是授权期元数据 —— 收件人是IEmailService.sendTemplate({ template, to, … })的运行时入参逐条分析
1.
job.timeoutMs—— 和 #3266 修过的datasource.interval是同一个错样例写
timeoutMs: 600000,spec 的键是timeout('Per-attempt time limit in milliseconds')。照抄的人得到一个没有超时的 job,而且没有任何反馈 —— 键名错了就是被剥掉。这和 #3266 修healthCheck.interval→intervalMs完全同型,只是方向相反(那次是漏了Ms,这次是多了Ms)。concurrency: 1则是JobSchema上不存在的概念,没有对应键可改,建议直接删。2.
email_template.from/to—— 一个拼错、一个层级错from: 'sales@example.com'→ 应为fromOverride(EmailAddressInlineSchema,注意形态可能不是裸字符串,要照 schema 写);to: '${contact.email}'→ 删掉。收件人在发送时给,模板定义不带to。这和 consolevalidation预览样例的condition语义写反了(spec 里 condition 为 TRUE 表示校验失败),另带两个会被静默剥掉的键 #3276 里validation.object被删是同一个道理:宿主/调用方才是它的作用域。顺带:样例的
subject/bodyHtml用的是${var}占位符,而 spec 的 docstring 写的是{{var.path}}。这个不影响 parse(都是字符串),但如果{{}}才是渲染器认的语法,样例现在教的是错的 —— 需要确认后一并改。3.
view—— 不是「多写了键」,是映射本身存疑,和 KNOWN_STALE 里的translation同型ObjectStackSchema.views是z.array( ViewSchema ),而ViewSchema是个没有身份键的容器。于是样例的name: 'open_orders'/label: 'Open Orders'/object: 'sales_order'三个键全部蒸发,解析结果只剩{ list: { type, columns } }—— 一个不知道自己叫什么、也不知道挂在哪个对象上的列表视图。spec 自己在
defineView的 docstring 里警告过相邻的坑:我们的样例是「半扁平」:因为有
list所以不是空容器,视图能注册,但身份键照样丢光。这个不能靠删键修。真正的问题是:一个放在
views[]数组里的容器,凭什么绑定到某个对象?要么 stack 层的views应该是Record< objectName, ViewSchema >而不是数组,要么样例根本该表达成对象上的listViews: { open_orders: {…} }。先定这个契约,再改样例 —— 和 KNOWN_STALE 里translation那行留的判断是同一类。view现在在SPEC_CLEAN里。它「通过」只是因为剥掉之后剩下的东西恰好合法,不代表样例是对的。建议
job:timeoutMs→timeout,删concurrency;email_template:from→fromOverride(按EmailAddressInlineSchema的形态写),删to;顺带确认占位符语法是${}还是{{}};view:先定契约(数组容器怎么绑对象),别急着改样例;关于第 4 条:这不是语义断言,别和 #3276 拒掉的那件事混了
#3276 明确不让把规则语义的断言(「condition 的方向对不对」)塞进
preview-samples-spec-valid.test.ts—— 那个守卫的职责是「能不能发布」,同意。但「作者写的键 parse 后还在不在」是结构性的,不是语义性的:它不需要理解规则的意思,只要比对 parse 前后的 key 集合,而且它恰好补上了那个守卫在自己 LIMIT 注释里承认的盲区。上面 5 个键、加上 #3276 修掉的 4 个(
validation的object/field/expression、skill的type),9 个全是它一次跑出来的。要做的话,建议单独一个 test(可以同文件、独立
it,也可以独立文件),带自己的 ledger ——view那三行在契约定下来之前只能挂账,不能当成 pass。做成什么形态由接手的人定,这里只提供依据和现成的比对逻辑。相关
validation/skill侧已修,本单是同一次审计的其余部分)datasource.interval→intervalMs,同型)validation/skill两个样例。