Repository navigation
#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953
Description
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——evaluateValidationRules的merged与previous(校验规则 + 字段requiredWhen+ optionvisibleWhen)packages/objectql/src/hook-wrappers.ts—— 生命周期 hook 的record/previous
未物化的三处,各自都在对可空已声明字段求值 CEL:
-
字段
readonlyWhen——rule-validator.ts里的stripReadonlyWhenFields合并{ ...previous, ...data }后直接求值,从不物化。与同一个字段上的requiredWhen结论相反,而requiredWhen就在同一个文件里物化过。且它 fail-open(readonlyWhen for 'x' failed to evaluate — change allowed through),所以谓词 fault 时字段不再只读,改动照常写入。 -
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}插值都读它。 -
action
visible/disabled的客户端绑定 —— 绑定是客户端已取到的那条记录;objectui该路径上不存在任何物化步骤(ActionEngine/toPredicateInput/useCondition全链只读已有 record)。列表行只带视图投影列,所以同一条谓词在record_header与list_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
Activity