Skip to content

L2 hook body 写集不透明:把「写了目标对象上不存在的字段」从 accepted gap 变成作者时 lint 告警 #4271

Description

@os-zhuang

#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 写业务逻辑最典型的失误形态(#3405reference#1535workflows 是元数据形状层的同款)。

校验版图目前的不对称:

现状
读侧:hook.condition(bare CEL) ✅ 注册期 + os validate 双点校验(ADR-0032)
读侧:flow 表达式槽 #4027 路径 ledger + reconciliation 测试
能力面:capabilities 声明 ✅ 静态检查
写侧:flow update_recordfields ✅ 结构化 config,validate-readonly-flow-writes 连 readonly 都查
写侧:L2 body 的 source 全盲

现有零件(全在仓库里,不要另造)

  • @objectstack/lint 是 ADR-0019 纯 (stack) => Finding[] 规则包,已挂 os validate / os build,35 个 validate-* pass 先例。
  • 解析器不需要新依赖:typescriptsucrase 已是 @objectstack/lintruntime 依赖(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.tsfindClosestMatches:未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的「strict 必须配可修错误」同样适用于 warning——未知字段要给 did-you-mean。

方案草案

新 pass validate-hook-body-writes.ts(命名可议):

  1. 范围:hook body.language: 'javascript'source。L1 expression 无写能力;flow 的 script 节点是内建 side-effect(config.actionType: 'email'|'slack'),不是 L2 JS body——都不在范围。
  2. 提取(typescript parser,只 parse 不执行不 type-check),v1 白名单三种写模式:
    • ctx.input.<ident> = … / ctx.input['<字面量>'] = … → 目标 = hook 的 object
    • Object.assign(ctx.input, { <字面量键>: … }) → 同上
    • ctx.api.object('<字面量>').update(…, { <字面量键>: … }) / .create({ … }) → 目标 = 该字面量对象名
  3. 对照:stack 内 ObjectSchema 声明字段 + 系统字段;未知字段 → warningfindClosestMatches 建议。
  4. bail-out(静态不可知即沉默,零误报优先):计算键、非字面量 spread、动态对象名、object: '*' 通配 hook 的 ctx.input 写(无单一目标对象;通配 hook 本就该有更高 review bar,另行讨论)。误报是这类 lint 的死因,漏报只是缺口暂时留着——不对称,宁漏勿误。
  5. 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。
  6. 挂载:@objectstack/lintos validate / os build不进 defineStack 运行时——TS parser 不在那条路径的依赖里,且那层的 warning 是元数据形状层,别混。

设计上待决

明确不做(v1)

  • 不执行 body、不 type-check、不做数据流/别名分析——纯语法层白名单。
  • 不做 completeness(create 缺必填字段)——那是 ADR-0078 的另一战场。
  • 不动 QuickJS 沙箱运行时行为。

参考

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions