读数
packages/formula/src/types.ts:66-75 在 EvalContext 上声明了一块 kernel API,并用文档注释点名了它支持的三个 CEL 函数:
/**
* Optional kernel API for `os.exists / os.count / os.lookup`.
* Implemented opportunistically by call sites that have a query engine.
*/
api?: {
exists?: (object: string, predicate: Expression) => boolean;
count?: (object: string, predicate: Expression) => number;
lookup?: (object: string, id: string) => Record<string, unknown> | null;
};
而 packages/formula/src/stdlib.ts:309-326 里构造 os 作用域的那段,只读三个字段:
const os: Record<string, unknown> = {};
if (ctx.user !== undefined) { … os.user = currentUser; }
if (ctx.org !== undefined) os.org = ctx.org;
if (ctx.env !== undefined) os.env = ctx.env;
if (Object.keys(os).length > 0) scope.os = os;
ctx.api 在整个 stdlib.ts 里一次都没有被读取:
$ grep -n "os\.exists\|os\.count\|ctx\.api" packages/formula/src/stdlib.ts
(无输出)
⇒ 三个函数一个都没绑。 调用方即使老老实实传了 api,作用域里也不会出现它们。
为什么这是 (c) 类而不只是一个未实现的可选项
⚠️ 这块声明是会被当真的。它有类型、有字段名、有一句说明「Implemented opportunistically by call sites that have a query engine」—— 读到它的人(或 AI)会合理地得出结论:只要调用方有查询引擎,os.lookup(...) 在谓词里就能用。
实际写下去拿到的是运行时错误:
runtime: found no matching overload for 'dyn.lookup(string, dyn)'
而且这个错误不是惰性的。 下游实测:一条不可求值的谓词会拒绝这次写入(objectstack#4649 的形状)。⇒ 照着这块声明写一条校验规则,结果是该对象上的每一次写入、插入更新全部被拒。
「声明存在、实现为零、失败形态是拒写」——这三条凑齐,就是让作者写出运行时静默拒收的元数据的那个陷阱。
下游实例(本条是怎么被发现的)
hotcrm #1915(REQ-0003)的需求记录第 128 行把一道能力闸钉在 crm_opportunity 的 validations[] 上,要它读所属客户的分类。施工席逐条量了三条路,全部走不通:
checkPredicate() 以 { record: merged, previous } 求值 —— 都是本对象的普通数据,不传 api。
- 关系穿透不行:
record.crm_account.type → runtime: No such key: type。
os.lookup("crm_account", …) 也不行 —— 就是本卡这条。
⇒ 那张卡的规格前提被证伪,闸门无法按记录指定的构造写出来。一个平台侧的死声明,直接让一条下游需求的落地方案作废。
⛔ 本卡不预判的事
⛔ 不预判该实现还是该删。两条都成立,取舍要量:
- 实现 —— 在
buildScope() 里绑上 ctx.api 提供的三个函数。⚠️ 但谓词求值里做 I/O 是个真问题:同步 CEL 求值 vs 可能异步的查询、N+1、以及 RLS 谓词里做 lookup 的权限语义。这三条都要先答。
- 删掉声明 —— 若判定谓词面不该能读别的对象,那就把这块
api 从 EvalContext 上移除,并在移除处写明理由。⚠️ 这同样是修复:今天的害处来自「声明存在」,不来自「实现缺失」。
⛔ 也不预判 exists / count 与 lookup 是否同一个结论 —— 三个的代价不一样(前两个可以下推成查询,lookup 是取整行),要分开量。
查重词
EvalContext api lookup · buildScope os scope · os.exists os.count · declared but never bound · no matching overload dyn.lookup
Generated by Claude Code
读数
packages/formula/src/types.ts:66-75在EvalContext上声明了一块 kernel API,并用文档注释点名了它支持的三个 CEL 函数:而
packages/formula/src/stdlib.ts:309-326里构造os作用域的那段,只读三个字段:ctx.api在整个stdlib.ts里一次都没有被读取:⇒ 三个函数一个都没绑。 调用方即使老老实实传了
api,作用域里也不会出现它们。为什么这是 (c) 类而不只是一个未实现的可选项
os.lookup(...)在谓词里就能用。实际写下去拿到的是运行时错误:
而且这个错误不是惰性的。 下游实测:一条不可求值的谓词会拒绝这次写入(objectstack#4649 的形状)。⇒ 照着这块声明写一条校验规则,结果是该对象上的每一次写入、插入更新全部被拒。
「声明存在、实现为零、失败形态是拒写」——这三条凑齐,就是让作者写出运行时静默拒收的元数据的那个陷阱。
下游实例(本条是怎么被发现的)
hotcrm #1915(REQ-0003)的需求记录第 128 行把一道能力闸钉在
crm_opportunity的validations[]上,要它读所属客户的分类。施工席逐条量了三条路,全部走不通:checkPredicate()以{ record: merged, previous }求值 —— 都是本对象的普通数据,不传api。record.crm_account.type→runtime: No such key: type。os.lookup("crm_account", …)也不行 —— 就是本卡这条。⇒ 那张卡的规格前提被证伪,闸门无法按记录指定的构造写出来。一个平台侧的死声明,直接让一条下游需求的落地方案作废。
⛔ 本卡不预判的事
⛔ 不预判该实现还是该删。两条都成立,取舍要量:
buildScope()里绑上ctx.api提供的三个函数。api从EvalContext上移除,并在移除处写明理由。⛔ 也不预判
exists/count与lookup是否同一个结论 —— 三个的代价不一样(前两个可以下推成查询,lookup是取整行),要分开量。查重词
EvalContext api lookup · buildScope os scope · os.exists os.count · declared but never bound · no matching overload dyn.lookup
Generated by Claude Code