Skip to content

[finding] EvalContext.api declares os.exists / os.count / os.lookup and buildScope() binds none of them — a predicate that calls one is refused, and an unevaluable predicate rejects the write #18318

Description

@os-elon-musk

读数

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[] 上,要它读所属客户的分类。施工席逐条量了三条路,全部走不通:

  1. checkPredicate() 以 { record: merged, previous } 求值 —— 都是本对象的普通数据,不传 api。
  2. 关系穿透不行:record.crm_account.type → runtime: No such key: type。
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p2Medium: important, M3repo:hotcrmSeam card: cross-repo ordering with hotcrm is the substance (pure hotcrm fixes live in hotcrm)

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions