#5017 清理这两个文件里的 ?? 别名链时顺带发现的,形状同族但不是 ?? 链 ,所以按 Prime Directive #10 单独记账,不在 #5017 的 PR 范围内。
事实
packages/lint/src/validate-expressions.ts 的字段公式校验(#5017 分支上约 570-590 行,if (f.formula) { … } 整段)读的是 f.formula:
if ( f . formula ) {
const res = validateExpression ( 'value' , f . formula as ..., { objectName , fields , fieldTypes , scope : 'record' } ) ;
// → `object 'X' · field 'Y' formula` 的 error / warning
}
FieldSchema.shape 没有 formula 键 。canonical 是 expression,而 formula 正是 field.zod.ts:333 里按名拒绝的别名:
formula: 'expression', calculation: 'expression', compute: 'expression',
实测(worktree 上 ObjectStackSchema.safeParse):
object w/ field.formula parses? false
["objects.0.fields.a: Unrecognized key(s) on this field: `formula`. …
Did you mean `formula` → `expression`?"]
object w/ field.expression parses? true
{"type":"formula","expression":{"dialect":"cel","source":"record.x + 1"},"returnType":"number", …}
该规则以 input: 'parsed' 注册(authoring-rules.ts),所以在 compile/build/validate 路径上看到的是解析产物 —— f.formula 恒为 undefined。
和 #5017 那五条的差别:这条不是"收敛为 canonical",是"canonical 从来没被读过"
#5017 的每一条都有 canonical limb 在链里,删掉别名 limb 对任何能解析的 stack 零行为变化。这条不同:代码里根本没有 f.expression 的读法。所以
现状:字段公式校验对任何 spec 合法 stack 都是完全不执行 的;
改成读 f.expression:等于启用一条从未跑过的检查 ,是覆盖面扩大,可能对现有真实元数据判红。
真实元数据确实全部用 canonical 拼法,即这段今天一条都没覆盖到:
位置
拼法
examples/app-showcase/src/data/objects/project.object.ts:67
expression: cel\(record.budget == null ? 0 : record.budget) - …``
examples/app-showcase/src/data/objects/invoice.object.ts:243
expression: cel\record.quantity * record.unit_price``
examples/app-showcase/src/data/objects/field-zoo.object.ts:132
expression: cel\…``
examples/app-crm/src/objects/{lead,opportunity,contact}.object.ts
Field.formula({ expression: … })
validate-expressions.test.ts 里现存的 7 处 formula: fixture({ type: 'formula', formula: '…' })也都不是 spec 合法的 —— 这段分支今天全部的覆盖来自不能解析的 fixture。
需要决定的(所以没顺手改)
直接把 f.formula 换成 f.expression :恢复本意的覆盖,但要先验证真实元数据零新红(value 角色 + record scope,Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928 裸引用告警会一起生效),并把 7 处 fixture 改成 canonical 拼法。
删掉整段 :与 validateOrgAxisRedLines 仍留着三条 spec 合法 stack 到不了的别名分支(objects[].rowLevelSecurity / .rls / permissionSets)—— #4984 修了 sharing 那一半 #5009 删 object-level RLS 同款处置。但这里和那里不同 —— 字段公式是 真实存在的授权/计算面,只是键名写错了,删掉会丢掉一条本应存在的 gate。
顺带要一并决定的:validate-null-guards.ts 里 Field.formula 被明确列为 excluded surface(理由是 value 角色天然可空、guard ? value : null 是祝福写法)。那条 ledger 说的是"不给 null-guard gate",不是"不给语法/字段存在性校验" —— 改动 1 不影响它,但注释里的 surface 名要跟着从 formula 改成 expression。
倾向 1 ,理由两条轴:
但 1 是覆盖面扩大、可能对现有真实元数据判红,不该由一个"删死代码"的 PR 顺手带上,所以在这里等 maintainer 拍板。
参考
#5017 清理这两个文件里的
??别名链时顺带发现的,形状同族但不是??链,所以按 Prime Directive #10 单独记账,不在 #5017 的 PR 范围内。事实
packages/lint/src/validate-expressions.ts的字段公式校验(#5017 分支上约 570-590 行,if (f.formula) { … }整段)读的是f.formula:FieldSchema.shape没有formula键。canonical 是expression,而formula正是field.zod.ts:333里按名拒绝的别名:实测(worktree 上
ObjectStackSchema.safeParse):该规则以
input: 'parsed'注册(authoring-rules.ts),所以在 compile/build/validate 路径上看到的是解析产物 ——f.formula恒为undefined。和 #5017 那五条的差别:这条不是"收敛为 canonical",是"canonical 从来没被读过"
#5017 的每一条都有 canonical limb 在链里,删掉别名 limb 对任何能解析的 stack 零行为变化。这条不同:代码里根本没有
f.expression的读法。所以f.expression:等于启用一条从未跑过的检查,是覆盖面扩大,可能对现有真实元数据判红。真实元数据确实全部用 canonical 拼法,即这段今天一条都没覆盖到:
examples/app-showcase/src/data/objects/project.object.ts:67expression: cel\(record.budget == null ? 0 : record.budget) - …``examples/app-showcase/src/data/objects/invoice.object.ts:243expression: cel\record.quantity * record.unit_price``examples/app-showcase/src/data/objects/field-zoo.object.ts:132expression: cel\…``examples/app-crm/src/objects/{lead,opportunity,contact}.object.tsField.formula({ expression: … })validate-expressions.test.ts里现存的 7 处formula:fixture({ type: 'formula', formula: '…' })也都不是 spec 合法的 —— 这段分支今天全部的覆盖来自不能解析的 fixture。需要决定的(所以没顺手改)
f.formula换成f.expression:恢复本意的覆盖,但要先验证真实元数据零新红(value角色 +recordscope,Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928 裸引用告警会一起生效),并把 7 处 fixture 改成 canonical 拼法。validate-null-guards.ts里Field.formula被明确列为 excluded surface(理由是value角色天然可空、guard ? value : null是祝福写法)。那条 ledger 说的是"不给 null-guard gate",不是"不给语法/字段存在性校验" —— 改动 1 不影响它,但注释里的 surface 名要跟着从formula改成expression。倾向 1,理由两条轴:
field.formula是真实的authoring 面并照着写,正是 validateOrgAxisRedLines 仍留着三条 spec 合法 stack 到不了的别名分支(objects[].rowLevelSecurity / .rls / permissionSets)—— #4984 修了 sharing 那一半 #5009 在 object-level RLS 上付过的代价。amount * probability而不是record.amount * record.probability)的槽位之一,而这条检查恰恰是为此存在的(Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928)。让它真的跑起来,比留一条装饰性 gate 强得多。但 1 是覆盖面扩大、可能对现有真实元数据判红,不该由一个"删死代码"的 PR 顺手带上,所以在这里等 maintainer 拍板。
参考
??别名读法(#5009 建议 3 的核对结果) #5017(同族的??别名链)、未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001(strict + 别名按名拒绝)、Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928(裸引用/类型健全性)、has(x)reads as a null guard and is not one — a publish-time lint should reject un-guarded nullable comparisons in CEL predicates #4763(null-guard gate)