Skip to content

#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953

Description

@xuyushun441-sys

Filed unassigned from #4811 / PR #4951,那里为了给 null-guard 闸门定判据把这几处逐一实测了一遍。本单只记录发现,不含修法承诺。

背景:为什么全量性是个契约,而不是实现细节

实测 @marcbachmann/cel-js,同一条谓词在两种绑定下语义恰好相反:

谓词 全量绑定 {a: null} 稀疏绑定 {}
has(record.a) true false
record.a < record.b FAULT no such overload FAULT No such key: a
record.a != null false FAULT No such key: a

#4649/#1871 之所以引入 materializeDeclaredFields,正是为了让 record.x == null / != null 这类写法可用:在全量绑定下它返回布尔值,在稀疏绑定下它自身就 fault

所以「记录是否全量」不是某个求值点的内部选择 —— 它决定了作者被允许写什么。同一个 record.x != null,在一处是正确守卫,在另一处是必然 fault。

发现:全量化只做在两个接缝上

已物化:

  • packages/objectql/src/validation/rule-validator.ts —— evaluateValidationRulesmergedprevious(校验规则 + 字段 requiredWhen + option visibleWhen)
  • packages/objectql/src/hook-wrappers.ts —— 生命周期 hook 的 record / previous

未物化的三处,各自都在对可空已声明字段求值 CEL:

  1. 字段 readonlyWhen —— rule-validator.ts 里的 stripReadonlyWhenFields 合并 { ...previous, ...data } 后直接求值,从不物化。与同一个字段上的 requiredWhen 结论相反,而 requiredWhen 就在同一个文件里物化过。且它 fail-open(readonlyWhen for 'x' failed to evaluate — change allowed through),所以谓词 fault 时字段不再只读,改动照常写入。

  2. flow 触发记录 —— packages/triggers/trigger-record-change/src/record-change-trigger.ts 把记录播种为 { ...(inputDoc ?? {}), ...after }。注释本身就写明「fields the driver did not echo back」,即明确知道 after 不保证带全列。start / edge condition 与 {record.x} 插值都读它。

  3. action visible / disabled 的客户端绑定 —— 绑定是客户端已取到的那条记录;objectui 该路径上不存在任何物化步骤(ActionEngine / toPredicateInput / useCondition 全链只读已有 record)。列表行只带视图投影列,所以同一条谓词在 record_headerlist_item 上看到的键集都不同。

后果

  • 作者无法用一条写法同时正确覆盖两类绑定:全量处要 != null,稀疏处 has() 才对、!= null 会 fault。而元数据里没有任何东西标明某个 slot 属于哪一类。
  • 这直接卡住了 null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 #4811:null-guard 闸门只能接全量绑定的面,上面三处因此被排除(理由与实测表记在 packages/lint/src/validate-null-guards.ts 的台账里)。把这三处补齐,那三面就可以一并纳入闸门。
  • readonlyWhen 与 flow condition 两处都是 fail-open / 静默分支,属于「看起来生效、实则什么都没做」这一族。

需要的决定(不是实现细节)

三处是否都应当补上 materializeDeclaredFields,从而把「谓词看到的记录 = 对象声明的形状」变成平台层面的统一保证?还是其中某几处刻意保持稀疏(例如 action 绑定跨进程,全量化意味着 REST 读取要补齐所有声明列)?

无论结论如何,结论本身需要被写下来并可引用 —— 现状是两个接缝物化了、三个没有,而没有任何文档或注释说这是有意的。

复现

const { evaluate } = require('@marcbachmann/cel-js');
evaluate('record.a != null', { record: { a: null } });  // false
evaluate('record.a != null', { record: {} });           // throws: No such key: a

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions