Repository navigation
validate-expressions.ts 的 field-formula 校验读 f.formula —— spec 声明的是 expression,这段从未对任何 spec 合法 stack 跑过 #5026
Description
#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角色 +recordscope,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,理由两条轴:
- 长期正确性:这是 declared ≠ enforced 的镜像 —— enforced-against-nothing。规则文件里留着一段读不存在的键的校验,下一位作者(人或 AI)会相信
field.formula是真实的authoring 面并照着写,正是 validateOrgAxisRedLines 仍留着三条 spec 合法 stack 到不了的别名分支(objects[].rowLevelSecurity / .rls / permissionSets)—— #4984 修了 sharing 那一半 #5009 在 object-level RLS 上付过的代价。 - 让 AI 写的元数据难写错:字段公式是 AI 生成元数据里最容易出裸引用(
amount * probability而不是record.amount * record.probability)的槽位之一,而这条检查恰恰是为此存在的(Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928)。让它真的跑起来,比留一条装饰性 gate 强得多。
但 1 是覆盖面扩大、可能对现有真实元数据判红,不该由一个"删死代码"的 PR 顺手带上,所以在这里等 maintainer 拍板。
参考
- validateOrgAxisRedLines 读的 sharing-rule 键是 spec 拒收的:ADR-0105 D6 ① 在 criteria 路径上从不触发 #4984 / validateOrgAxisRedLines 仍留着三条 spec 合法 stack 到不了的别名分支(objects[].rowLevelSecurity / .rls / permissionSets)—— #4984 修了 sharing 那一半 #5009 / validate-expressions / validate-security-posture 也有同形的 spec 不声明键的
??别名读法(#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)
Activity
xuyushun441-sys commentedon Aug 4, 2026
认领(PM 分派)
session_018iARDqtrhQgz6fVHDeDkbQclaude/issue-5026-field-formula-activatedomain:devx(落点packages/lint/src/validate-expressions.ts;validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017/PR fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键??别名读法 (#5017) #5046 已合入,TRACKED_UNDECLARED_READS记账待清)注意本单性质:启用一条从未跑过的检查,不是删死代码 —— 可能对现有元数据判出真红,逐条核实定性,不放宽。
Generated by Claude Code
xuyushun441-sys commentedon Aug 4, 2026
审核通过 — PR #5118 已取消草稿并挂上 auto-merge(SQUASH);沉睡的检查激活,别名家族的记账清零
"零新红"是判别结果,不是门没开 —— 这句话有证据支撑:反向验证 1/1b 证明坏
expression(未知字段 / 语法错)确实判红并点名定位,第 2 条证明好的判绿;八个真实公式槽全绿是门会判别的结论。这正是激活型任务与清死代码型任务在验收上的区别,dev 把它讲清了。诊断定位串的改名超出议题字面,但理由成立且我认可:
field 'Y' formula留着就是错拼法的传播路径 —— 下一位作者(人或 AI)照着诊断去写formula:再被 schema 拒。全仓核实无消费者断言旧串,回退成本一行。这与 #4992 的expressionText不读meta.rationale同属一类判断:诊断文本本身是语料,会教人。fixture 扫尾 7 处 re-spell + 三类"核查过不需改"逐条列明(含与在飞 #5096 的边界确认);
TRACKED_UNDECLARED_READS从一条到空表,且空表本身被断言钉住(只缩不长的 ratchet 走到了终点);变异验证确认 #5046 的 declared-key guard 真能接住回归(undeclared 恰为['formula'])。衍生 #5116 的两层价值:表层是文档/博客里两条裸引用公式样例(实测判红,在激活后的规则下是真错);深层是它揭示的缺口 ——
check:skill-examples只跑 tsc,"能编译但 CEL 写错"的样例目前没有任何门,这正是那两条能长期存活的原因。已标 bug 入队。CI 23 项无红;lint 1169 + cli 600 全绿。落点纯
packages/lint。Generated by Claude Code
fix(lint): field-formula check reads the declared `expression`, activ…
fix(docs,lint): 两处裸引用公式样例改回 canonical,并补上公式样例的 CEL 语义门 (objectstack-a…