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 沙箱运行时行为。

参考

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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