从 #4001 / #4207 的 hook 步骤延伸。packages/spec/src/data/hook-body.zod.ts:134-140 的 schema 头注释自己承认 了这个缺口:
Write-set opacity — accepted static-analysis gap. source is opaque to static analysis: no lint verifies that the fields the body writes (ctx.input.x = …, ctx.api.object('y').update(id, { x })) exist on the target object(s). Only the read side (hook.condition) and the capability surface are statically checked.
问题
L2 body(language: 'javascript',QuickJS 沙箱)里把字段名写错——或写了一个语义正确但 schema 未声明的字段——的后果是:运行成功、无任何诊断、数据没落 。这是 #4001 「静默失效制造虚假完成」在运行时表达式层的精确镜像,也是 AI 写业务逻辑最典型的失误形态(#3405 的 reference、#1535 的 workflows 是元数据形状层的同款)。
校验版图目前的不对称:
面
现状
读侧:hook.condition(bare CEL)
✅ 注册期 + os validate 双点校验(ADR-0032)
读侧:flow 表达式槽
✅ #4027 路径 ledger + reconciliation 测试
能力面:capabilities 声明
✅ 静态检查
写侧:flow update_record 的 fields
✅ 结构化 config,validate-readonly-flow-writes 连 readonly 都查
写侧:L2 body 的 source
❌ 全盲
现有零件(全在仓库里,不要另造)
@objectstack/lint 是 ADR-0019 纯 (stack) => Finding[] 规则包,已挂 os validate / os build,35 个 validate-* pass 先例。
解析器不需要新依赖 :typescript 和 sucrase 已是 @objectstack/lint 的 runtime 依赖(validate-react-pages 在用);CLI 侧另有 ts-morph / esbuild。
validate-jsx-pages.ts 先例:作者产物 source 「parsed, never executed」,作者时 fail loudly(ADR-0078)。
validate-readonly-flow-writes.ts 先例:写集对照目标对象、目标「when statically knowable」否则静默放行——bail-out 姿态和目标对象解析辅助都可复用。
shared/suggestions.zod.ts 的 findClosestMatches:未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的「strict 必须配可修错误」同样适用于 warning——未知字段要给 did-you-mean。
方案草案
新 pass validate-hook-body-writes.ts(命名可议):
范围 :hook body.language: 'javascript' 的 source。L1 expression 无写能力;flow 的 script 节点是内建 side-effect(config.actionType: 'email'|'slack'),不是 L2 JS body——都不在范围。
提取 (typescript parser,只 parse 不执行不 type-check),v1 白名单三种写模式:
ctx.input.<ident> = … / ctx.input['<字面量>'] = … → 目标 = hook 的 object
Object.assign(ctx.input, { <字面量键>: … }) → 同上
ctx.api.object('<字面量>').update(…, { <字面量键>: … }) / .create({ … }) → 目标 = 该字面量对象名
对照 :stack 内 ObjectSchema 声明字段 + 系统字段;未知字段 → warning 带 findClosestMatches 建议。
bail-out(静态不可知即沉默,零误报优先) :计算键、非字面量 spread、动态对象名、object: '*' 通配 hook 的 ctx.input 写(无单一目标对象;通配 hook 本就该有更高 review bar,另行讨论)。误报是这类 lint 的死因,漏报只是缺口暂时留着——不对称,宁漏勿误。
severity :v1 全 advisory warning,不阻断;和 lintUnknownAuthoringKeys(排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移 #3786 /config.storage authored with driver/root is silently ignored — a host asking for S3 gets local disk #4167 )姿态一致,有现场数据后再谈 ratchet。
挂载 :@objectstack/lint → os validate / os build。不进 defineStack 运行时——TS parser 不在那条路径的依赖里,且那层的 warning 是元数据形状层,别混。
设计上待决
明确不做(v1)
不执行 body、不 type-check、不做数据流/别名分析——纯语法层白名单。
不做 completeness(create 缺必填字段)——那是 ADR-0078 的另一战场。
不动 QuickJS 沙箱运行时行为。
参考
从 #4001 / #4207 的 hook 步骤延伸。
packages/spec/src/data/hook-body.zod.ts:134-140的 schema 头注释自己承认了这个缺口:问题
L2 body(
language: 'javascript',QuickJS 沙箱)里把字段名写错——或写了一个语义正确但 schema 未声明的字段——的后果是:运行成功、无任何诊断、数据没落。这是 #4001「静默失效制造虚假完成」在运行时表达式层的精确镜像,也是 AI 写业务逻辑最典型的失误形态(#3405 的reference、#1535 的workflows是元数据形状层的同款)。校验版图目前的不对称:
hook.condition(bare CEL)os validate双点校验(ADR-0032)capabilities声明update_record的fieldsvalidate-readonly-flow-writes连 readonly 都查source现有零件(全在仓库里,不要另造)
@objectstack/lint是 ADR-0019 纯(stack) => Finding[]规则包,已挂os validate/os build,35 个validate-*pass 先例。typescript和sucrase已是@objectstack/lint的 runtime 依赖(validate-react-pages在用);CLI 侧另有 ts-morph / esbuild。validate-jsx-pages.ts先例:作者产物source「parsed, never executed」,作者时 fail loudly(ADR-0078)。validate-readonly-flow-writes.ts先例:写集对照目标对象、目标「when statically knowable」否则静默放行——bail-out 姿态和目标对象解析辅助都可复用。shared/suggestions.zod.ts的findClosestMatches:未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的「strict 必须配可修错误」同样适用于 warning——未知字段要给 did-you-mean。方案草案
新 pass
validate-hook-body-writes.ts(命名可议):body.language: 'javascript'的source。L1 expression 无写能力;flow 的script节点是内建 side-effect(config.actionType: 'email'|'slack'),不是 L2 JS body——都不在范围。ctx.input.<ident> = …/ctx.input['<字面量>'] = …→ 目标 = hook 的objectObject.assign(ctx.input, { <字面量键>: … })→ 同上ctx.api.object('<字面量>').update(…, { <字面量键>: … })/.create({ … })→ 目标 = 该字面量对象名findClosestMatches建议。object: '*'通配 hook 的ctx.input写(无单一目标对象;通配 hook 本就该有更高 review bar,另行讨论)。误报是这类 lint 的死因,漏报只是缺口暂时留着——不对称,宁漏勿误。lintUnknownAuthoringKeys(排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移 #3786/config.storageauthored withdriver/rootis silently ignored — a host asking for S3 gets local disk #4167)姿态一致,有现场数据后再谈 ratchet。@objectstack/lint→os validate/os build。不进defineStack运行时——TS parser 不在那条路径的依赖里,且那层的 warning 是元数据形状层,别混。设计上待决
configSchemaand the executor's wire payload are two unchecked lists — nothing validates flow node config keys at author time #4027)?建议要:「声明了模式但没人验证」正是 Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528 的死法。const doc = ctx.input; doc.x = 1)v1 收不收?常见写法,提取成本低,但开了口子就有数据流分析的滑坡——建议 v1 明确记为 known miss,advisory 姿态下漏报可接受。hook-body.zod.ts:134的「accepted gap」段改写为指向本 lint,避免注释与现实反向漂移。明确不做(v1)
create缺必填字段)——那是 ADR-0078 的另一战场。参考
configSchemaand the executor's wire payload are two unchecked lists — nothing validates flow node config keys at author time #4027(表达式路径 ledger)、Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528(声明未验证的死法)、动作参数弹窗:内联 lookup 参数无法声明引用目标(配置被静默剥离 + 文案谎报「即将上线」) #3405 / Object-levelworkflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535(静默失效先例)packages/lint/src/validate-readonly-flow-writes.ts、validate-jsx-pages.ts、validate-expressions.tspackages/spec/src/automation/flow-node-expression-paths.ts