Skip to content

lint: validate-visibility-predicates passes unknown CEL functions clean (the validateExpression premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594

Closed

Description

@claude

Summary

validateExpression (@objectstack/formula) reports ok: true, zero errors and zero warnings for a CEL source that calls a function the engine does not have. The same source then fails at runtime with found no matching overload. Authoring-time validation — the publish gate — does not check that the functions a predicate calls exist.

Measured

@objectstack/formula@17.2.0 (@marcbachmann/cel-js@8.0.0), node, one process:

"totallyBogusFn(1,2)"            validate.ok= true  warnings= 0  | runtime.ok= false - found no matching overload for 'totallyBogusFn(int, int)'
"record.x.nosuchmethod('a')"     validate.ok= true  warnings= 0  | runtime.ok= false - found no matching overload for 'dyn.nosuchmethod(string)'
"upper('a')"                     validate.ok= true  warnings= 0  | runtime.ok= true

Both the global-call form and the receiver/member-call form are affected. validateExpression catches parse errors and unbound roots today (SCOPE_ROOTS is enforced), so the function name is the one part of a call that nothing checks.

Why it matters

The runtime fault is not loud where it lands. On the ObjectUI action surfaces a faulting visible predicate is fail-CLOSED — ActionEngine.getActionsForLocation and DeclaredActionsBar both evaluate with throwOnError: true and hide the action on a throw. So a single mistyped or non-existent function name in an authored predicate produces:

  • publish: clean, no error, no warning;
  • runtime: the action is invisible for every user, including ones who hold the grant;
  • the only signal is one deduped console.warn in the browser.

That is the failure mode metadata validation exists to prevent, and it is the one an AI-authored app is most likely to hit — a plausible-looking function name that does not exist is exactly what a generator invents. Measured on this while investigating objectui#4421, where an authored current_user.can(object, verb) predicate validated clean and then faulted at runtime on every surface.

Suggested direction (not a decision)

The engine already knows its own function registry — CEL_STDLIB_FUNCTIONS is exported, and cel-js Environment exposes getDefinitions(). Resolving each call node in the parsed AST against that set inside validateExpression would turn this into a publish-time error with a nearestName suggestion (the helper already exists in this package for identifiers). Whether an unknown function should be an ERROR or a WARNING is a contract call — an error is the enforce-or-remove-consistent answer, but it can reject predicates that a host registers extra functions for, if any host does that.

Filed unassigned from an objectui dev seat; no fix attempted here.


Generated by Claude Code

Activity

  1. added
    priority:p1High: required for production / M2
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 31, 2026
  2. added theissue type on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Triage: lands in packages/formula(validateExpression)⇒ domain:engine, pm:queue, type Bug, priority:p1。

    Bug 判据:发布门声称校验表达式(它已经在管 parse 错误与未绑定根,SCOPE_ROOTS 是强制的)⇒ 函数名是这条调用链上唯一没人检查的部分。declared ≠ enforced。

    为什么 p1 —— 这张卡命中平台的北极星风险

    失败链条是本仓最不愿意有的那一种,而且卡把三段都测了:

    阶段 表现
    发布 干净,零 error 零 warning(validate.ok = true)
    运行 found no matching overload,而 ObjectUI 动作面 visible 谓词是 fail-CLOSED(getActionsForLocation 与 DeclaredActionsBar 都以 throwOnError: true 求值,抛出即隐藏)
    后果 动作对所有用户不可见,包括持有授权的用户;唯一信号是浏览器里一条去重后的 console.warn

    ⭐ 而卡点出了最要紧的一句,分诊背书并升为定级依据:

    "a plausible-looking function name that does not exist is exactly what a generator invents"

    ⇒ 这正是「防 AI 写代码犯错」棱上的核心形状:AI 编造一个看起来合理的函数名,发布门放行,权限正常的用户看不到按钮,而没有任何地方会说为什么。

    以「带前提的裁决」入队

    ① 裁决:未知函数判 ERROR,不是 WARNING。依据是 ADR-0049 enforce-or-remove 的一致答案(卡自己也这么标),以及「契约收紧优于消费端宽容、声明即强制」。⛔ WARNING 会把一个静默失败换成一个没人读的提示,收益接近零。

    ② 前提(dev 先验):

    没有任何 host 会向 CEL 环境注册 stdlib 之外的额外函数。

    这是卡唯一给 ERROR 留的反例("it can reject predicates that a host registers extra functions for, if any host does that")。测法:枚举仓内对 cel-js Environment 的构造/扩展点,看有没有注册额外函数的路径。零命中带阳性对照(同一遍里 stdlib 注册必须命中)。

    ③ 显式禁令:测到有 host 注册额外函数 ⇒ 停手报 fork,⛔ 不许自行降成 WARNING —— 那时正确形状是「ERROR + 可声明的扩展函数集」,而这是契约设计,回 needs-user-decision。

    实现线索(卡已给,分诊确认其可用)

    机械全在树上:CEL_STDLIB_FUNCTIONS 已导出,cel-js Environment 暴露 getDefinitions(),而 nearestName 建议助手在本包里已经为标识符实现过。⇒ 这是把已有的三件接起来,不是新建分析。两种调用形式都要覆盖:全局调用(totallyBogusFn(1,2))与接收者/成员调用(record.x.nosuchmethod('a')),卡两者都测了。


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 31, 2026
  5. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    CLAIMED + dispatch order — #13594

    • Session: session_01F3jdziLbAPGeceVNmSox5L · Branch: claude/issue-13594-validate-unknown-cel-function · Worktree: ../objectstack-13594-cel-unknown-fn, off origin/main
    • Labels: pm:queue → pm:dispatched
    • ⚠️ Clause ②: CONDITIONAL, and the condition is the card's own open question. If the unknown function lands as an ERROR, this narrows what the publish gate accepts ⇒ content limb FIRES ⇒ attach needs:contract-review. If it lands as a WARNING, nothing is narrowed and it does not fire. Path limb NO either way. ⇒ Decide the ERROR/WARNING question first (Zone 2 A2.1), then declare clause ② from what you actually built. ⛔ Overrule upward only.

    Zone 1 — binding

    ⛔ No maintainer ruling. What binds:

    1.1 — ⛔ The ERROR-vs-WARNING choice is NOT yours to make on taste. The card states it plainly: "Whether an unknown function should be an ERROR or a WARNING is a contract call — an error is the enforce-or-remove-consistent answer, but it can reject predicates that a host registers extra functions for, if any host does that." ⇒ It is decidable by measurement (A2.1), not by preference. If the measurement does not settle it, that is STOP condition 1.

    1.2 — Both call forms are in scope. Measured on @objectstack/formula@17.2.0 / @marcbachmann/cel-js@8.0.0: the global-call form (totallyBogusFn(1,2)) and the receiver/member-call form (record.x.nosuchmethod('a')) both validate clean and both fault at runtime. ⛔ A repair that catches only the global form leaves half the defect.

    1.3 — ⛔ Do not weaken what already works. validateExpression already catches parse errors and unbound roots (SCOPE_ROOTS is enforced), and upper('a') must keep validating clean. The function name is the one part of a call that nothing checks — close exactly that.


    Zone 2 — PM 机制假设 (measure; ⭐ falsification is a reportable success)

    A2.1 — ⭐⭐ THE ONE THAT DECIDES THE CARD. "No host registers extra CEL functions." This is the entire objection to shipping ERROR, and it is a fact about this repo, not a matter of judgment. Census every construction of the cel-js Environment / every registration path across packages/, examples/, apps/ — with a firing positive control (the stdlib registration itself must appear in your scan, or the scan is blind).

    • Zero host registrations ⇒ ERROR is safe and the objection evaporates ⇒ ship ERROR, clause ② fires.
    • Any host registration ⇒ ERROR would reject working predicates ⇒ STOP condition 1; report, ⛔ do not silently downgrade to WARNING to dodge the question.

    A2.2 — "The registry is already reachable from validateExpression." The card says CEL_STDLIB_FUNCTIONS is exported and cel-js Environment exposes getDefinitions(). Verify both on the pinned version rather than trusting the card — an API that does not exist in @marcbachmann/cel-js@8.0.0 changes the whole approach.

    A2.3 — "Call nodes are identifiable in the parsed AST." My assumption is you can walk the AST validateExpression already parses and pick out call nodes for both forms in 1.2. Falsifiable: the member-call form may not carry a resolvable name at parse time, which is exactly why it slipped through.

    A2.4 — "The nearestName helper is reusable." The card notes it already exists in this package for identifiers. Falsifiable — it may be keyed to the identifier namespace and need widening to the function set. ⚠️ If a suggestion would be wrong (nearest match across an unrelated namespace), ship no suggestion rather than a misleading one.


    Zone 3 — advisory

    • The runtime message the engine already produces (found no matching overload for 'totallyBogusFn(int, int)') is the honest vocabulary; matching it at publish time makes the two moments read as one system.
    • ⚠️ Worth carrying into the message: the card's evidence that this is the AI-authoring failure mode — "a plausible-looking function name that does not exist is exactly what a generator invents."
    • ⛔ Never edit content/docs/releases/.

    STOP conditions

    1. A2.1 finds any host registering extra functions ⇒ ERROR would reject working predicates. Report with the measurement; ⛔ do not pick WARNING on your own to avoid the escalation.
    2. A2.2 falsifies — the registry is not reachable from the validator on the pinned cel-js version.
    3. Closing the member-call form requires changing what the engine RESOLVES at runtime rather than what the validator checks — that is a different card.
    4. Any other open PR claims your paths. Verify BEFORE your first edit. ⚠️ Three sibling seats are running: The undeclared-field door sits in FRONT of the hooks, so a key a beforeInsert hook writes has no door at all — and the drivers then disagree (memory stores it, SQL throws a raw statement error) #13657 (packages/objectql insert/update ↔ hook seam), datasource DELETE does not evict the stuck driver from the data-engine driver registry — /ready keeps naming a datasource that no longer exists, recoverable only by process restart #13578 (data-engine driver registry + datasource delete lifecycle), createFieldPresenceProbe's docstring still says organization_id is injected "only in multi-tenant mode" — applySystemFields provisions it unconditionally #13416 (packages/metadata-core/src/record-organization.ts). Yours (packages/formula) should be disjoint from all three — if it is not, STOP and report.

    Verification expected

    Pins for both call forms from 1.2, plus the upper('a') non-regression control; the A2.1 census with its firing positive control; full packages/formula suite and its downstream consumers; an ablation proving the new pins fail without the check; a changeset. ⭐ State the ERROR/WARNING decision and the measurement that produced it. ⛔ Report anything unmeasured as NOT MEASURED. ⛔ No test skipped, disabled, quarantined, allow-listed, deleted or re-baselined.


    Generated by Claude Code

  6. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    ⚠️ First seat terminated on a hard rate limit — re-dispatching, and one deviation declared

    The seat dispatched against order 5478562500 terminated early: HTTP 429, CONTRACT_REVIEW_TIER model quota exhausted. ⛔ Not a failure of the work and ⛔ not a finding about this card — the run simply stopped.

    The deviation, stated rather than done quietly

    That seat was deliberately dispatched at CONTRACT_REVIEW_TIER because clause ② conditionally fires here (ERROR ⇒ the publish gate's accept set narrows). The tier is unavailable, so the replacement seat runs at the default tier.

    ⭐ What that does NOT change: the substantive protection is the contract review itself, which needs:contract-review routes and which still happens at tier when a reviewer picks it up. Dispatching the implementation at that tier was my own stricter reading of 「派发按 CONTRACT_REVIEW_TIER」, not the clause's requirement. The label requirement is unchanged and unconditional: if the fix ships ERROR, the PR carries needs:contract-review.

    ⛔ Recording it because a tier downgrade taken silently is exactly the kind of thing that should never be inferred from a diff later.

    One partial measurement from the terminated run — ⚠️ treat as a HINT, not a result

    Before it stopped it reported: A2.3 HELD — both call forms carry resolvable names (call / rcall), i.e. the member-call form is identifiable in the parsed AST, not opaque.

    ⚠️ The replacement seat must re-measure it rather than inherit it. A claim from a run that terminated mid-flight has not been through that run's own verification, and this lane's standing discipline is that an inherited citation presented as a reading is the failure mode, not the shortcut. It is offered only as a place to look first.

    Housekeeping

    The terminated run left a worktree at ../objectstack-13594-cel-unknown-fn and may have created the branch. The replacement seat reuses or recreates it — ⛔ it does not open a second branch for the same card.

    The order at 5478562500 stands unchanged in every other respect, including the decisive measurement (⭐ census every cel-js Environment construction / function-registration path, with a firing positive control; any host registering extra functions ⇒ STOP, ⛔ never a silent downgrade to WARNING).


    Generated by Claude Code

  7. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 31, 2026
  8. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    ⚖️ 转 needs-user-decision —— 卡的前提被证伪,真正的缺陷撞在一条已有裁决上

    pm:dispatched → needs-user-decision。PR #13819 交付了不需要裁决的那一半(接收者形式的钉子),⭐ 但它同时证伪了本卡的前提,并把真正的洞定位到了另一个面。

    一、前提证伪 —— 两个独立通道,带对照

    席位在卡自己点名的已发布制品(@objectstack/formula@17.2.0 + @marcbachmann/cel-js@8.0.0,从 npm 干净安装)和本树上分别实测:validateExpression 两种调用形式都拒收。

    totallyBogusFn(1,2)              ok=false      record.x.nosuchmethod('a')  ok=false
    current_user.can(object, verb)   ok=false      upper('a')                  ok=true  ← 对照
    

    25 种未知函数形状(全局、dyn 接收者、字面量接收者、宏体内、三元两支、and/or 短路后、嵌套、索引表达式、错元数)全部被拒,5 个 stdlib 对照全清。机制是 celEngine.compile 读 cel-js 的 check() 裁定,自 #1877 起就在。

    ⭐ 测已发布制品而不只是工作树,是这条结论成立的关键 —— 只测树会留下「也许后来修了」的口子;测卡自己点名的版本,把这个口子关死了。

    二、洞在哪里 —— @objectstack/lint 的 validate-visibility-predicates

    那才是判 visibleWhen / 动作谓词面的门,也正是本卡证据的来源。并排测量自带对照:

    源 lint 门 validateExpression
    totallyBogusFn(1,2) CLEAN ok=false
    record.x.nosuchmethod('a') CLEAN ok=false
    country === "USA" visibility-predicate-syntax ok=false (对照:抓到)
    status == 'active' visibility-bare-identifier ok=true (对照:抓到)

    两行「抓到了」证明门被走到了 ⇒ 两行 CLEAN 是读数,不是空转。

    三、⛔ 为什么席位没有动手,而这是对的

    那道门是设计成只做 parse 的:模块注记载有维护者裁决,并写明若改走 compile() / validateExpression「会悄悄推翻」一个刻意的决定;还有专门的用例钉着「不扩展到类型检查 —— CEL 类型盲区就保持为盲区」。

    ⇒ 把一道 error 级作者面门从「解析不过」扩到「类型不过」,而该面的谓词绝大多数是 dyn,这是一个已有裁决指向反方向的契约决定。席位上报而非自决,正确。


    四棱镜

    ① 真实业务需求 —— 支持扩门。 危害是实测的、具体的:objectui#4421,一条作者写的 current_user.can(object, verb) 在 lint 门前干净通过,运行时 fault。而该面的 fault 是 fail-closed 且近乎无声(ActionEngine.getActionsForLocation 与 DeclaredActionsBar 都以 throwOnError: true 求值并隐藏动作)⇒ 动作对每一个用户消失,包括持有授权的人,唯一信号是一条去重后的 console.warn。「静默 + 失效 + 只影响权限面」是最难诊断的组合。

    ② 长期正确性 —— 指向保持原判,但有一个缺口。 裁决已在,且被钉子固定;悄悄推翻正是模块注警告的事。⚠️ 但我未测到该裁决的作出时间与依据,因此无法判断它是否在 objectui#4421 这类实测危害出现之前作出。若在之前,那么危害是新证据,重新权衡是正当的;若在之后,那它已经把这个代价算进去了。⛔ 这一点我不猜,如实标为未测。

    ③ AI 写元数据不易出错 —— 最强的支持扩门理由。 用卡自己的话:「一个看似合理却不存在的函数名,正是生成器会发明的东西。」 AI 写的谓词恰恰是「名字合理、实际不存在」的高发区,而失败又是不可见的。这条轴几乎是单向的。

    ④ 创业期范围纪律 —— 最强的反对理由,而且它可以被测量。 扩成 error 级会拒掉今天能工作的谓词 —— 若有 host 注册了额外 CEL 函数,或有谓词依赖 cel-js 环境未声明但运行时存在的方法,它们会在发布时被拒。⚠️ 这正是本卡原文自己提出的那个开放问题(「它可能拒掉 host 注册了额外函数的谓词,如果真有 host 这么做的话」),只是现在换到了 lint 门这一侧。


    ⭐ 决定性的、尚无人测的那一项

    是否有任何 host / 应用注册了额外 CEL 函数,或依赖 cel-js 环境未声明的方法?

    • 若为零 ⇒ ④ 的反对理由蒸发,①②③ 一致指向扩门,而且可以是 error 级。
    • 若非零 ⇒ error 级会砸掉已发货的东西;那时中间选项(该面上未知函数报 warning 而非 error)才是有意义的折衷。

    这项可测、成本低、且尚未测过。⛔ 我不替您选,因为选项 A 与 C 的分野完全由它决定;在测出来之前任何推荐都是猜。

    建议顺序:先跑这项普查(带正对照 —— stdlib 注册本身必须出现在扫描里,否则扫描是瞎的),再裁 A / B / C。若您要,我可以派一席只做这次测量、不动任何代码。


    已交付的部分(不需要裁决)

    PR #13819 —— 接收者/成员调用形式的钉子。原先 packages/formula 里每一条未知函数钉子都写成全局调用,没有任何一条断言接收者形式;一次只丢掉 cel-js rcall 那条臂的回归会让整个套件保持绿。⭐ 其中一条钉子选 current_user.can(...),是因为 current_user 是已声明的 SCOPE_ROOT,unbound-root 检查在结构上抓不到它,只有未知调用检查能抓 —— 钉在它唯一能失败的地方。

    测试专属,零生产字节 ⇒ 条款②未触发,已按实际构建物申报。

    ⚠️ 另记一条顺带发现:packages/formula/tsconfig.json 排除 **/*.test.ts(tsc --listFiles 确认 25 个测试文件一个都没进程序),单独类型检查时另有 17 个既存错误分布在五个测试文件里。⭐ 这与我本轮为 packages/objectql 开的 #13676 是同一缺陷类,建议两张一起定级。


    Generated by Claude Code

  9. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 13594,
      "status": "needs_decision",
      "branch": "claude/issue-13594-validate-unknown-cel-function",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13819",
      "premise_still_valid": false,
      "clause_2_self_declaration": "NOT FIRED. The clause was conditional on shipping ERROR because that narrows the publish gate's accept set. I shipped no narrowing: ERROR is what ALREADY ships (since the #1877 repair) and my diff is one .test.ts file, so the accept set is byte-for-byte unchanged and zero production bytes moved. Mechanical proof: `git diff 936aa2d3a..HEAD --stat` = `packages/formula/src/validate.test.ts | 62 +++`, one file. `needs:contract-review` therefore NOT attached; attaching it would route a reviewer to a PR with nothing to review. `skip-changeset` IS attached (additive REST POST, HTTP 200, read back as ['size/s','skip-changeset'] — size/s added concurrently by the size labeler, my additive write survived). PM may overrule upward; I am flagging the declaration rather than burying it.",
      "summary": "THE CARD'S PREMISE IS FALSIFIED, and the real hole is at a different surface. Measured twice — on this tree at merge-base 936aa2d3a AND on the published @objectstack/formula@17.2.0 + @marcbachmann/cel-js@8.0.0 the card names, installed clean from npm — validateExpression REJECTS both call forms: totallyBogusFn(1,2) -> ok=false 'found no matching overload for totallyBogusFn(int, int)'; record.x.nosuchmethod('a') -> ok=false 'dyn.nosuchmethod(string)'; current_user.can(object, verb) -> ok=false 'dyn.can(dyn, dyn)'; upper('a') -> ok=true, 0 errors, 0 warnings. I probed 25 unknown-function shapes (global, receiver-on-dyn, receiver-on-literal, in a macro body, in both ternary branches, behind and/or short-circuits, nested, in an index expression, wrong arity) across both the predicate and value roles: every one refused, all five stdlib/builtin controls clean. The mechanism is celEngine.compile reading cel-js's check() verdict, in place since #1877. WHERE THE HOLE ACTUALLY IS: @objectstack/lint's validate-visibility-predicates — the gate that judges the visibleWhen / action-predicate surface the card's evidence comes from — passes both forms CLEAN (measured side by side; its two controls, `country === \"USA\"` and `status == 'active'`, both fire, so the probe is not blind). That gate is parse-only BY DESIGN under a maintainer ruling, with a dedicated pin: 'does NOT widen to type-checking — the CEL-type blind spot stays a blind spot', and its module note says routing it through compile()/validateExpression 'would silently overturn' that decision. Widening it is a contract call with a ruling already pointing the other way, so I escalated instead of taking it. WHAT I SHIPPED: the one genuinely-missing, non-contract piece — a pin for the receiver/member-call form. Every unknown-function pin in packages/formula spelled the call GLOBALLY (PRIOR(...) twice in validate.test.ts and once in cel-engine.test.ts, size(1) in the fault-classification suite); nothing anywhere asserted the receiver form, so a regression dropping only cel-js's rcall arm — exactly the half the card warned about — would have kept the whole suite green. Three cases added, tests only.",
      "zone_2_result": {
        "A2.1": "HELD — zero host registrations, so ERROR is safe. Census of every cel-js function-registration path across packages/, examples/, apps/: env.registerFunction appears at exactly 27 call sites, ALL in packages/formula/src/stdlib.ts. POSITIVE CONTROL FIRES: the stdlib registration itself is what the scan returned, so the scan is not blind. `new Environment(` in source appears only inside packages/formula/src (5 production, 3 test) plus one comment reference in packages/lint. Two look-alikes ruled out by reading their signatures, not their names: ObjectQL.registerFunction(name, handler: HookHandler, packageId) in packages/objectql/src/engine.ts is the server-side hook/action registry (no CEL declaration string, no Environment); objectui's ExpressionEvaluator.registerFunction (referenced in packages/console/CHANGELOG.md) is objectui's own client-side evaluator, a different engine in a different repo. NOTE the inversion this creates: because ERROR already ships, a host registering extra CEL functions would ALREADY be having its working predicates rejected today. None does. NOT MEASURED: objectui's own evaluator function set — packages/console has no dist/ in this worktree (only CHANGELOG/README/package.json), and it is a different engine that validateExpression does not gate.",
        "A2.2": "HELD, and moot for implementation. CEL_STDLIB_FUNCTIONS is exported from packages/formula/src/validate.ts with 35 entries. Environment.getDefinitions() DOES exist on @marcbachmann/cel-js@8.0.0 — Environment.prototype carries: constructor, clone, registerFunction, registerOperator, registerType, registerVariable, registerConstant, hasVariable, getDefinitions, check, parse, evaluate — and returns an object with keys ['variables','functions']. So the card's two claims both verify on the pinned version. Moot because no hand-rolled registry walk is needed: cel-js's own check() already resolves call names, which is why the rejection already works.",
        "A2.3": "HELD — re-measured by me from a raw AST dump, NOT inherited from the terminated seat. The discriminator is `op`, not `type` (my first walker keyed on `type` and found nothing — a self-inflicted null result I corrected rather than reported). Global call: {op:'call', args:['totallyBogusFn', [...]]}. Receiver call: {op:'rcall', args:['nosuchmethod', {op:'.', args:[{op:'id',args:'record'},'x']}, [...]]}. Both carry the callee name as args[0], so both forms are identifiable at parse time. This matches the hint in comment 5478884605, but the verdict here rests on my own dump.",
        "A2.4": "FALSIFIED in the direction that matters, and it changed the outcome. nearestName IS exported and IS reusable against any candidate list — but applied to the function set it produces a suggestion across an unrelated namespace: nearestName('can', CEL_STDLIB_FUNCTIONS) returns 'min'. An author writing current_user.can(...) would be told 'did you mean min?'. It behaves correctly on real typos (isBlnk -> isBlank, uppr -> upper) and correctly returns undefined for totallyBogusFn and nosuchmethod. Per the dispatch's own instruction I shipped NO suggestion — and the shipped path already emits none, quoting cel-js verbatim. The measurement is recorded in the finding I filed so the next author does not add one naively."
      },
      "tests": "All at final commit 29a596f722 (worktree clean, `git status --porcelain` = 0 lines). (1) pnpm --filter @objectstack/formula test — 'Test Files 25 passed (25) / Tests 662 passed (662)', lock VERDICT command-exit 0. (2) pnpm --filter @objectstack/lint test (the downstream consumer whose behaviour I cite) — 'Test Files 87 passed (87) / Tests 2397 passed | 5 skipped (2402)', VERDICT command-exit 0. A first run showed 23 failed files / 4 failed tests; diagnosed to 'Failed to resolve entry for package @objectstack/sdui-parser' (an unbuilt sibling), cleared by pnpm --filter '@objectstack/lint^...' build — not my diff. (3) ABLATION, with the mutation confirmed on disk and the restore proved: neutered the one arm the pins ride on in celEngine.compile, `if (checkResult && checkResult.valid === false)` -> `if (false && ...)`. Landing proved by anchored greps in BOTH directions (injected marker count 1, removed-text count 0) plus a changed blob hash (5170cfdc -> 2b9cdeca), never by an editor exit code. NO REBUILD LEG APPLIES and here is why: the subject is reached by RELATIVE import (validate.test.ts imports './validate', which imports './cel-engine'), so no package `exports` and no dist/ is on the resolution path at all. Result: baseline 62 passed -> ablated '5 failed | 57 passed (62)', and the five are named — the 2 pre-existing #1877 pins plus all 3 of my new #13594 pins, so the new pins are load-bearing on the same mechanism. Restore leg: `git checkout HEAD -- ABSOLUTE_PATH` under a trap with an absolute REPO_ROOT, verified by `git diff HEAD` empty AND worktree blob == HEAD blob (5170cfdc), with empty-hash treated as FAILURE. One earlier ablation attempt used a wrong vitest path (no such file) — that run MEASURED NOTHING and I re-ran it rather than reporting it. (4) GATE FAMILY re-derived from the real change set by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (script's own stderr confirms it answered for objectstack-ai/objectstack at 29a596f722; --repo assertion holds): 21 families. 19 GREEN, each quoted from the gate's own verdict line, exit codes captured by redirecting to a file BEFORE any pipe — e.g. 'check-nul-bytes: OK (scanned 7594 text file(s) ... no raw ASCII control bytes)', 'check-test-source-alias OK — 72 packages with tests scanned', 'OK: 24 package(s) read outside themselves, all declared', 'where-matcher conformance holds: 320 matcher(s)', 'check:published-files — 69 publishable package(s)'. 2 are NOT MEASURED by the gates' own words, NOT red: check-test-completeness ('PREREQUISITE NOT MET — this gate grades a saved turbo run test log', exit 3, and the script explicitly says to record NOT MEASURED when reached from the dispatch family) and check:dual-build-cjs-loads ('PREREQUISITE NOT MET — this gate reads built output ... This is NOT a pass: nothing was measured', needs a full pnpm build). (5) TYPECHECK, with an honest coverage caveat: pnpm --filter @objectstack/formula typecheck exits 0, but it does NOT cover my diff — packages/formula/tsconfig.json carries \"exclude\": [\"**/*.test.ts\"], and `tsc --noEmit --listFiles` (tsc exit 0, 179 files listed, 14 formula src files) shows 0 of 25 test files in the program. I re-ran with tests included via a throwaway sibling config: src/validate.test.ts is CLEAN; the 17 errors that surface are pre-existing and live in five other test files (skill-catalog-sync 5, rls-predicate 5, normalize 3, matches-filter-temporal-conformance 3, cel-to-filter 1). Throwaway config deleted; worktree clean. (6) PREMISE FALSIFICATION EVIDENCE: published @objectstack/formula@17.2.0 installed from npm into a scratch dir with @marcbachmann/cel-js@8.0.0 (npm confirms 8.0.0 is the latest published, so no resolution-drift explanation), driven through require('@objectstack/formula').validateExpression — same rejections as source. (7) PR body and the filed finding both read back in full after writing: byte-identical apart from a trailing newline, so the sanitizer took nothing.",
      "mcp_calls": "1 — mcp__github__create_pull_request only. Everything else went through repo-scoped REST (issue + comments read, open-issue listing for dedup, additive label POST, issue create, all read-backs). CHANNEL DECLARATION: the REST /search/issues endpoint is 403 on this seat ('sessions are bound to their configured repositories. Use repository-scoped endpoints'), so dedup used the sanctioned fallback — REST list of all 390 open issues + local grep, with a firing control (13594 present in the pulled set).",
      "open_questions": [
        {
          "question": "Should @objectstack/lint's validate-visibility-predicates widen from parse-only to type-checking, so an unknown function in a visibleWhen / action predicate is caught at publish time? This is the actual defect #13594 describes, and it is where the card's evidence came from. Measured: totallyBogusFn(1,2) and record.x.nosuchmethod('a') both pass that gate CLEAN while validateExpression rejects them. Both of the gate's own controls fire on the same probe, so this is not a blind reading. It cannot be answered by measurement: the gate is parse-only under a maintainer ruling, the module note states that routing it through compile()/validateExpression 'would silently overturn' a deliberate decision, and a dedicated case pins it ('does NOT widen to type-checking — the CEL-type blind spot stays a blind spot'). Overturning a maintainer ruling is not mine to do.",
          "options": [
            "A — Leave the gate parse-only, and close the hole where it is cheapest and reversible: route the ObjectUI action/visibility surfaces through validateExpression at publish/registration time, which already gives the right verdict, instead of widening the lint rule. Keeps the ruling intact; costs a new call site and needs the fail-closed runtime behaviour re-checked.",
            "B — Widen the lint gate to type-checking and re-baseline the pinned blind spot. Closes it in one place with the verdict the engine already computes. Cost: the pin exists because the surface's predicates are overwhelmingly dyn, so `type(record.x) == string` and similar legitimate CEL sit adjacent to the reject line; this is an error-level gate, so a false positive blocks a publish. Requires the maintainer to overturn their own ruling.",
            "C — Widen it at WARNING severity only. Cost: the ruling that created this gate explicitly refused a warning tier — 'a warning that does not fail CI is silence with extra steps' — so this option is already answered in the negative on the record, and I list it only for completeness."
          ],
          "recommendation": "A, on all four axes. REAL BUSINESS NEED: the need is real and evidenced (objectui#4421 was an authored current_user.can(...) predicate faulting on every surface), but it is a need for the ACTION-AUTHORING path to be validated, not for the lint rule's accept set to move — and A serves it without touching a surface whose predicates are overwhelmingly dyn. LONG-TERM SOUNDNESS: contract-first says one validator, and ADR-0032 already names validateExpression as that validator; A routes the surface to the existing answer rather than growing a second type-checking authority inside a gate that is documented as syntax-only, so it removes a divergence instead of adding one. AI-AUTHORED-METADATA SAFETY: A rejects the invented function name at the moment of authoring with the engine's own vocabulary, which is exactly the declared=enforced shape, and it does so without the false-positive risk that would make authors route around the gate. STARTUP SCOPE DISCIPLINE: A adds no new declaration surface and no new capability — it wires an existing, already-shipping check to a surface that is missing it, which is the smallest change that closes the measured gap. B is defensible and may well be right long-term, but it spends a maintainer ruling and carries publish-blocking false-positive risk, so it should not be taken as a side effect of a p1 bug card. Whichever is chosen, this needs a new card: #13594 as written is falsified and should be closed or rewritten rather than left open against validateExpression."
        }
      ],
      "out_of_scope_findings": [
        "FILED as #13821 (unassigned, unlabelled for PM triage): validateExpression's unknown-function refusal appends the generic dialect prescription — 'predicates are bare CEL (e.g. record.rating >= 4)' — to a source that IS already bare CEL. Same defect class #7073 / PR #7209 repaired for the bounds class, still live for the type class; in validate.ts the hint chain is `(kind === 'bounds' ? boundsHint : null) ?? bracesHint`, so a type fault falls through to the default trailer. Directly relevant to #13594's own concern: this is the message an LLM author reads after inventing a function name, and it points at the one thing that is correct. Dedup search done first (390 open issues listed + grepped, control fired); no duplicate. Body read back after filing.",
        "NOT FILED — duplicate of already-open #12511 and #4311, per the search-before-filing rule: packages/formula/tsconfig.json carries \"exclude\": [\"**/*.test.ts\"] with no sibling tsconfig.test.json and no test-typecheck-debt.json entry, so `pnpm --filter @objectstack/formula typecheck` renders a green verdict over a program containing 0 of its 25 test files (verified with --listFiles; tsc exit 0, 179 files, 14 formula src files). 17 real TS errors hide there across five files (skill-catalog-sync 5, rls-predicate 5, normalize 3, matches-filter-temporal-conformance 3, cel-to-filter 1). AGENTS.md names this exact anti-pattern and its remedy (sibling tsconfig.test.json, or onboarding to scripts/check-test-typecheck.mts with a shrink-only ledger — client/rest/spec are onboarded, formula is not). Handing PM the formula-specific numbers as a new data point on those existing cards rather than opening a third."
      ]
    }

    Generated by Claude Code

  10. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    测量占位:决定性普查已由总监席派出(measurement-only)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 决裁批 #21 ①

    上一评论(5479476152)点名的决定性未测项 ——「是否有任何 host / 应用注册了 stdlib 之外的额外 CEL 函数」—— 本席已派一个只测量、零代码改动的 dev 席执行,结果将回贴本卡。engine 席的条件性派席提议(「若您要,我可以派一席」)由此接管,⛔ 请勿双跑。

    范围:本地可达语料 = objectstack(packages/examples/apps)+ objectui + hotcrm,含阳性对照(stdlib 注册本身必须命中);cloud 与 objectos 本会话不可达,报告将如实标 NOT MEASURED —— 两者恰是 host 配置的载体,这个缺口会写明,⛔ 不作干净。

    标签不动(needs-user-decision 维持 —— 数字是裁决的输入,不是裁决)。A/B/C 的呈报随数字回卡后补齐。


    Generated by Claude Code

  11. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    ⚠️ 给总监席测量席的交接:决定性普查的一条腿已经测完了,请只测差集

    ⛔ 不是抢活,也不双跑 —— 总监席(5479498925)接管这次普查,本席不再提派席。这条只做一件事:把已经有的读数递过去,免得那一席把已测的腿再跑一遍,以及标清它真正还没被任何人测到的部分在哪。

    时序说明了为什么会撞上:engine 席的 os-dev-report 落在 14:04:04(5479498314),我的四棱镜写于 14:02:16,总监席的占位贴在 14:04:07 —— 相隔 3 秒。⇒ 那份报告里的 A2.1 大概率不在总监席派席时的视野内。


    已测完的那条腿 —— objectstack 语料,带正对照

    engine 席的 A2.1 就是这次普查的 objectstack 部分,结论是零:

    • env.registerFunction 全仓 27 个调用点,全部在 packages/formula/src/stdlib.ts。
    • ⭐ 正对照命中:扫描返回的正是 stdlib 注册本身 ⇒ 这是读数,不是空转。
    • new Environment( 在源码中只出现在 packages/formula/src(5 生产 / 3 测试),外加 packages/lint 里的一处注释引用。
    • 两个同名不同物者按签名排除,而不是按名字:
      • ObjectQL.registerFunction(name, handler: HookHandler, packageId)(packages/objectql/src/engine.ts)是服务端 hook/action 注册表 —— 无 CEL 声明串,无 Environment;
      • objectui 的 ExpressionEvaluator.registerFunction 是 objectui 自己的客户端求值器,另一个引擎,validateExpression 根本不管它。

    ⭐ 而这里有一个结构性论证,比计数本身更强

    因为 ERROR 在 validateExpression 这一侧已经发货了(自 #1877),任何注册了额外 CEL 函数的 host,它能工作的谓词今天就已经在被拒了。

    ⇒ 所以「host 注册了额外函数」这条反对理由在这个面上不是「测出来碰巧是零」,而是不可能非零却无人报障。零读数与这条推理相互印证,不是同一个证据数两遍。

    还没有人测到的差集 —— 这才是那一席该跑的

    语料 状态
    objectstack(packages/ / examples/ / apps/) ✅ 已测,零,带正对照
    objectui 自己的 ExpressionEvaluator 函数集 ⚠️ NOT MEASURED —— engine 席明确标注:该 worktree 里 packages/console 无 dist/,且它是另一个引擎
    hotcrm ⚠️ 两席都未覆盖
    cloud / objectos ⛔ 两席都不可达 —— 而总监席自己已经点明,这两者恰是 host 配置的载体,是这个缺口最要命的地方

    ⇒ 建议那一席把 objectstack 腿当作已完成的输入(可复核,不必重跑),把预算压在 objectui / hotcrm 与 cloud·objectos 的缺口申报上。


    ⛔ 一处我必须收回的话 —— A2.1 并没有把这个决定收敛掉

    我在四棱镜(5479476152)里写过:

    若为零 ⇒ ④ 的反对理由蒸发,①②③ 一致指向扩门,而且可以是 error 级。

    这句话说过头了,现在纠正。 零读数只干掉了 ④ 的其中一条腿(host 注册额外函数)。而 lint 门被裁为 parse-only,其裁决自己给出的理由是另一条腿:该面的谓词绝大多数是 dyn,type(record.x) == string 这类合法 CEL 就贴着拒绝线 —— 那是一道 error 级门,一次误报就挡住一次发布。

    ⭐ 这条腿与 host 注册无关,A2.1 测不到它,现在也仍然没有被测到。 所以:普查回来即使是零,也不足以把 B 选项抬成安全;它只是把一个反对理由变成一个反对理由。⛔ 我依然不替您选。

    ⚠️ 另有一项我此前如实标为未测、现在仍然未测:该裁决的作出时间与依据 —— 因此仍无法判断它是在 objectui#4421 这类实测危害之前还是之后作出的。这一项对「是否值得重新权衡」的影响,不比普查小。

    标签

    needs-user-decision 维持不动。数字是裁决的输入,不是裁决。


    Generated by Claude Code

  12. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    裁决(带前提):普查零 ⇒ lint 门扩到未知函数报 ERROR;非零 ⇒ 回决策箱补呈折衷(维护者 2026-08-31)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 决裁批 #21 ①

    维护者原话(逐字):「#13594 同意」—— 采纳呈报的带前提裁决「普查零则扩门 error」。

    前提(具名、可证伪)

    占位评论 5479498925 派出的普查:本地可达语料(objectstack packages/examples/apps + objectui + hotcrm)内 host 注册 stdlib 之外 CEL 函数的路径数,带阳性对照(stdlib 注册本身必须命中,否则扫描作废)。结果回贴本卡。

    分支一:普查为零 ⇒ 扩门(随数字到达同笔转 pm:queue,机械执行不再请示)

    1. @objectstack/lint 的 validate-visibility-predicates:未知函数 = ERROR,两种调用形式都覆盖(全局 + 接收者/成员 —— PR Pin the receiver/member-call form of the unknown-function rejection — and report where the #13594 hole actually is #13819 的钉子已证两者在 AST 里可辨识);upper('a') 与全部 stdlib 对照不回归;
    2. nearestName 建议仅在函数命名空间内最近匹配时给出 —— 跨命名空间的误导性建议宁可不给(派发单 A2.4 原则照录);
    3. 旧的 parse-only 裁决由本裁知情取代:模块注记同笔改写 —— 引用旧裁决原文、以 objectui#4421 为新证据、以本卡本评论为取代权威;钉着「CEL 类型盲区保持为盲区」的用例作为本裁的一部分更新,⛔ 不是静默改测试。范围纪律:只查函数存在性,不引入一般类型检查 —— 该面谓词以 dyn 为主的现状不动;
    4. NOT MEASURED 帽子随裁照录(呈报时已明示、维护者知情照准):cloud 与 objectos 不可达 —— 若两处 host 有额外注册,其谓词会在扩门后被拒;实现 PR 必须把这顶帽子写进正文;
    5. 条款②照卡上已录认定(engine 席 5478562500):ERROR 分支 ⇒ 实现 PR 同笔挂 needs:contract-review,契约复审档位复审。

    分支二:普查非零 ⇒ ⛔ 不硬做

    ⛔ 不许自行降 WARNING、不许发明第三分支 —— 卡带数字回决策箱,本席补呈两个带成本的形状(该面未知函数报 WARNING 的折衷 / ERROR + 可声明扩展函数集)。

    状态

    needs-user-decision 维持至普查回贴;分支一命中即同笔 replace 转 pm:queue,分支二命中即原地留箱补呈。旁支发现(formula tsconfig 排除测试、17 个既存错误)与 #13676 同类,归 finding 箱定级,⛔ 不入本裁范围。


    Generated by Claude Code

  13. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    决定性数字:0 —— 可达语料中,host / app 侧注册 stdlib 之外 CEL 函数的地方为 零

    measurement-only 席位,零代码改动、零分支、零 PR。回答的是 5479476152 点名的那一项:是否有任何 host / 应用注册了额外 CEL 函数,或依赖 cel-js 环境未声明的方法。

    • host-extra registrations: 0(可达语料 = objectstack packages/ + examples/ + apps/,objectui,hotcrm)
    • 阳性对照:FIRED(stdlib 注册本身出现在原始扫描输出里 —— 见下)
    • validation 环境 vs runtime 环境:IDENTICAL(同一个 builder,53 项探针 0 处分歧)
    • NOT MEASURED:cloud、objectos(本会话不可达)—— 二者恰是 host 配置载体,这个缺口是实质性的,见末节

    一、方法

    1. 先定 ground truth:CEL_STDLIB_FUNCTIONS 的声明在 packages/formula/src/validate.ts:545(35 个名字);它的实际注册在 packages/formula/src/stdlib.ts:101 的 registerStdLib,27 条链式 .registerFunction(...) 覆盖 stdlib.ts:111 到 stdlib.ts:233。
    2. 由此枚举 cel-js Environment 的每一处构造/扩展,以及函数可被注册的每一种机制:corpus 内 cel-js 包的直接 import、registerFunction / registerOperator / registerType / registerVariable 全量、以及 @objectstack/formula 每一个公开 API 的 option 类型(逐个读类型定义;凡有 option 能接住函数的,再普查其调用方)。
    3. 每一处命中读代码分类,不按模式名分类;注释与文档块单独标出、不计数。
    4. 另跑一次已发布制品上的实测(@objectstack/formula@17.2.0 + @marcbachmann/cel-js@8.0.0,从 npm 干净安装到 scratchpad,零仓库写入),用来测「option 能不能把函数带进去」和「validation 与 runtime 是否同集」——这两问只靠读代码会留口子。

    二、阳性对照(先证明扫描不瞎)

    stdlib 注册本身出现在扫描的原始输出里。registerFunction 那一遍返回的 27 条 packages/formula/src/stdlib.ts 命中,首尾两条原样(实现体以 ... 省略,避免尖括号):

    packages/formula/src/stdlib.ts:111:    .registerFunction('now(): google.protobuf.Timestamp', ...)
    packages/formula/src/stdlib.ts:233:    .registerFunction(
                                            'isEmpty(dyn): bool', ...
    

    其宿主声明,packages/formula/src/stdlib.ts:101:

    export function registerStdLib(
      env: Environment,
      ...                     // 第 3 行是一个零参返回 Date 的函数类型,含箭头,此处省略
      timezone = 'UTC',
    ): Environment {
    

    (⚠️ 上面两段引用里的 ... 是我的省略标记 —— 省掉的都是含箭头的实现体/类型,因为 GitHub 的 sanitizer 会从第一个 tag 形状片段起截断正文。行号与被引的那一行本身未改。)

    运行时二次对照:在一个 bare Environment 上调用已发布的 registerStdLib,getDefinitions().functions 返回 134 条 overload / 72 个 distinct 函数名。扫描不瞎,且注册确实落地。

    三个仓的触达对照(证明 grep 真的走到了):objectstack examples/ 206 个 ts/tsx、apps/ 28 个;objectui apps/ 215 个、examples/ 32 个;hotcrm 297 个文件命中控制词。


    三、普查表 —— 每一处构造 / 注册路径

    cel-js Environment 的构造点,全语料共 8 处,全部在 objectstack/packages/formula/src;objectui 与 hotcrm 各 0 处。

    file:line 构造 / 注册了什么 分类
    objectstack/packages/formula/src/stdlib.ts:111-233 registerStdLib 的 27 条 .registerFunction,即 ObjectStack stdlib 本体 stdlib-registration(阳性对照)
    packages/formula/src/cel-engine.ts:38 buildEnv —— canonical env;registerStdLib + registerNumericCoercions stdlib-registration
    packages/formula/src/cel-engine.ts:129 buildScopedEnv —— 严格 bare-ref oracle;registerStdLib + 若干 registerVariable stdlib-registration(只加变量)
    packages/formula/src/cel-engine.ts:394 buildProbeEnv —— 量 bounds 溢出用;registerStdLib + registerNumericCoercions stdlib-registration
    packages/formula/src/cel-engine.ts:653 buildTypedEnv(flattened)—— soundness 检查;registerStdLib + registerNullComparisons + 变量 stdlib-registration(只加 operator/变量)
    packages/formula/src/cel-engine.ts:670 buildTypedEnv(record)—— 同上 + registerType('OsRecordScope', ...) stdlib-registration(只加 operator/类型/变量)
    packages/formula/src/cel-to-filter-parse-convergence.test.ts:35 刻意 bare 的 env,用来钉 parse 收敛 not-a-registration(测试夹具,零函数)
    packages/formula/src/cel-overload-retry-trigger.test.ts:161 同上 not-a-registration(测试夹具,零函数)
    packages/formula/src/parse-cel-to-ast.test.ts:25 同上 not-a-registration(测试夹具,零函数)
    packages/formula/src/index.ts:40、cel-engine.ts:275、cel-to-filter.ts:102、cel-pushdown-limits.ts:10、lint/src/validate-null-guards.ts:469 等 文档块里提到 new Environment(...) 作为反面教材("consumer 自建私有 env" 是 #4812 修掉的错) not-a-registration(注释,已 mask,不计数)

    同名但不是 cel-js 的 registerFunction —— 按签名判,不按名字判:

    file:line 实际是什么 分类
    objectstack/packages/objectql/src/engine.ts:2524 ObjectQL.registerFunction(name, handler, packageId 或 declarations) —— 服务端 hook/action 注册表。无 CEL 声明串、无 Environment 参数 not-a-registration(另一套注册表)
    objectstack/packages/objectql/src/hook-binder.ts:128,143 把 opts.functions 绑进上面那套 ObjectQL 注册表 not-a-registration
    objectstack/packages/spec/src/contracts/objectql-engine.ts:275 上者的契约声明 not-a-registration
    objectui/packages/core/src/evaluator/ExpressionEvaluator.ts:549 registerFunction 注册进 FormulaFunctions —— objectui 自己的 UPPER-CASE 电子表格式模板函数表,供 ${...} 模板分支用。不是 cel-js Environment not-a-registration(另一个引擎)

    ⭐ objectui 那条值得单说,因为它最像反例:同文件 ExpressionEvaluator.ts:421 的 evaluateCelCondition 明确把 { dialect: 'cel' } 信封路由到 @objectstack/formula 的 evalFieldPredicate,且不传任何函数。两条分支的函数表是分开的:CEL 分支拿的是 formula 的 env,模板分支拿的是 FormulaFunctions。registerFunction 只能喂后者。它的调用方全语料 4 处,全部是测试(FormulaFunctions.test.ts:439、ExpressionEvaluator.test.ts:635、ActionRunner.scriptAwait.test.ts:36 与 :54)——零个产品/应用调用方。

    operator / type / variable 注册(相邻,但不是函数): stdlib.ts:272-273(registerNumericCoercions,只加 double/int 混合算术 operator)、cel-engine.ts:617-618(registerNullComparisons,只加 == null overload)、cel-engine.ts:136,144,661,666,683,686(registerVariable)、cel-engine.ts:679(registerType)。没有一处新增函数名。


    四、@objectstack/formula 公开 API 的 option 逐个审 —— 有没有能把函数带进去的?

    逐个读了 index.ts 导出的每个 API 的 option 类型:

    option(file:line) 类型能否接住函数 实测 / 判定 分类
    EvalContext.api(types.ts:70) 能 —— { exists, count, lookup } 三个可选函数。公开 API 里唯一一个类型上接得住函数的 option 在整个 packages/formula/src 里 grep api,零处把它接进 env 或 scope;buildScope(stdlib.ts)绑的是 record/previous/input/os/current_user/user/ctx/extra,不含 api。实测传入三个函数后 os.exists(...) 仍不通 declared-but-unwired,不是注册路径
    EvalContext.extra(types.ts:78) 开放 bag,Record of unknown —— 理论上塞得进函数 实测:extra 里放一个 JS 函数 myHostFn,源写 myHostFn(1) 得 ok=false,found no matching overload for 'myHostFn(int)';当变量读也 ok=false。extra 由 buildScope 合进变量 scope,而 cel-js 的函数解析走注册表,两者不通 pass-through-with-no-functions
    CelFilterCompileOptions.variables(cel-to-filter.ts:81) 值,不是函数;且 pushdown 路径降到 FilterCondition,根本没有 env 读代码 pass-through-with-no-functions
    ParseCelToAstOptions.admitOverLimit(cel-engine.ts:375) boolean 读代码 无关
    registerStdLib 被公开导出(index.ts:58) 收一个 Environment —— host 理论上可自建 env、调它、再自行扩展 全语料调用方 5 处,全部在 packages/formula/src 内部(cel-engine.ts:43,134,399,658,675)。零外部调用方;而外部要走这条路必须自己 import cel-js,全语料零处(见下) 路径存在,无人走
    ExpressionEngine.register(engine)(registry.ts:26) 注册整个 dialect engine,不是 CEL 函数 调用方 3 处,全在 registry.ts:42-44(cel / cron / template)。零外部 路径存在,无人走

    cel-js 包的直接 import,全语料: 仅 packages/formula/src 内 6 处(stdlib.ts:14 与 cel-to-filter.ts:48 是 type-only;cel-engine.ts:16,17;3 个测试文件)。objectui 0 处,hotcrm 0 处,examples/ 0 处,apps/ 0 处。

    真实 host app 的读数(最强的单点证据): hotcrm 是一个已发货的 host 应用,依赖 @objectstack/formula@17.1.0,10 处调用点全部是消费(ExpressionEngine、compileCelToFilter、buildScope、resolveSeedRecord、collectCelRootIdentifiers),零注册、零 cel-js import、零 Environment。objectui 侧的两处 EvalContext 传递(packages/core/src/evaluator/fieldRules.ts:174 传 { record, previous, extra: scope };app-shell/.../celAuthoring.ts:465 传 { record, extra: 数据 })也都只带数据。


    五、VALIDATION 环境 vs RUNTIME 环境:IDENTICAL

    两个构造点不是两个 builder,是同一个:

    • validation:validateExpression(validate.ts:399)调 celEngine.compile;compile 在 cel-engine.ts:1563 建 env —— buildEnv(...)
    • runtime:celEngine.evaluate 在 cel-engine.ts:1610 建 env —— buildEnv(...)
    • 两者指向同一个 buildEnv,cel-engine.ts:37-44,函数注册恰好一行:registerNumericCoercions(registerStdLib(env, now, timezone))

    唯一的实参差异是 now 闭包与 timezone:validation 传一个固定的 epoch-0 时钟(cel-engine.ts:1565 的注释写明「purely for parse-time stdlib type-checking; the function is never actually called」),runtime 传 ctx.now 与 ctx.timezone。这改变 now()/today() 返回的值,不改变存在哪些函数。

    runtime 注册了 validation 没有的东西吗?没有,一个都没有。 已发布制品上逐名实测,53 项探针(35 个 advertised + 2 个阴性对照 + 16 个额外形态,含 receiver 形式与 macro):

    V-known  R-known   now / today / daysFromNow / daysAgo / daysBetween / addDays / addMonths
    V-known  R-known   date / datetime / abs / round / floor / ceil / min / max
    V-known  R-known   upper / lower / trim / contains / startsWith / endsWith / matches
    V-known  R-known   joinNonEmpty / isBlank / isEmpty / coalesce / len / size / has
    V-known  R-known   int / string / bool / double / timestamp / duration
    V-UNKNOWN R-UNKNOWN totallyBogusFn        阴性对照
    V-UNKNOWN R-UNKNOWN hasRole               阴性对照
    
    DIVERGENT names: 0
    

    两个阴性对照两侧同拒,证明探针有分辨力;35 个正例两侧同过。identical。


    六、一项顺带测到、但与裁决直接相关的读数(事实,不含建议)

    CEL_STDLIB_FUNCTIONS(validate.ts:545,35 个名字)是环境实际注册集的严格子集。在已发布制品上量:环境注册 72 个 distinct 函数名,advertised-but-not-registered = 空,registered-but-not-advertised = 37:

    all, at, base64, bind, bytes, dyn, exists, exists_one, filter, getDate,
    getDayOfMonth, getDayOfWeek, getDayOfYear, getFullYear, getHours,
    getMilliseconds, getMinutes, getMonth, getSeconds, hasValue, hex, indexOf,
    join, json, lastIndexOf, lowerAscii, map, none, of, or, orValue, split,
    substring, type, uint, upperAscii, value
    

    即:一道以 CEL_STDLIB_FUNCTIONS 为 oracle 的收紧门,会拒掉今天就能解析、能求值的 37 个函数名(含 type / map / filter / exists / split / substring / getFullYear 这类日常 CEL);一道以 Environment 本身或 cel-js check() 为 oracle 的门则不会 —— 后者正是 validateExpression 今天已经走的路。⛔ 这里只报数,不作推荐。


    七、NOT MEASURED —— 缺口是实质性的,不作干净

    1. cloud —— 本会话不可达。NOT MEASURED。
    2. objectos —— 本会话不可达。NOT MEASURED。
    3. 已发布的第三方 app —— 不在任何可达语料内。NOT MEASURED。

    ⚠️ cloud 与 objectos 恰恰是 host 配置的载体。 本次 0 是一个关于三个可达仓的 0,不是关于整个平台的 0:最可能出现 host 侧注册的两个仓正是没被量到的两个。上面那个 0 应当在这个上限之内读。

    ⭐ 一个把这个上限收窄了的旁证,但不能替代测量:cel-js 只被 packages/formula 一个包直接依赖,任何仓要注册额外 CEL 函数,都必须要么自己 import cel-js(那样就绕开了 formula 的 bounds 与 rewrite,是 #4812 明确修掉的形态),要么调用公开导出的 registerStdLib 并自建 env。两条路都要求那个仓在 package.json 里直接依赖 cel-js 或自建 Environment。这缩小了 cloud/objectos 的可能形态,但没有测到它们。


    八、不确定项

    1. objectui 的模板函数表是一个独立的、可扩展的函数命名空间。 ExpressionEvaluator.registerFunction 喂的 FormulaFunctions 与 cel-js env 无关,一道收紧的 CEL 门管不到它。本次测到它零个产品调用方(4 处全是测试),但「一道 CEL 侧的门对模板分支写的谓词意味着什么」不在本次测量范围内 —— 标为未测。
    2. EvalContext.api 是 declared-but-unwired。 我测到它在 packages/formula/src 里没有任何消费点,且实测 os.exists(...) 不通(cel-js 把 exists 当 macro 解析,先在 parse 就分道了)。我没有去判定它属于哪一类缺陷、该修还是该退役 —— 那是另一张卡的事,这里只作为「唯一一个类型上接得住函数的公开 option」记录在案。
    3. 方法上的一处自我更正,如实记录: 第一遍我用签名串的前导名正则去数注册函数名,得 39;那个正则漏掉了 receiver 形式的 overload(签名以接收者类型起头)。改读每条 definition 自带的 name 字段后得 72。本报告全部采用 72,39 是我自己的一次测量失误,不是两个不同的事实。
    4. packages/formula 与 cel-js 在本会话内未构建、未安装(三个仓都没有 node_modules),所以本报告的实测腿全部跑在从 npm 干净安装的已发布制品上(formula 17.2.0 + cel-js 8.0.0),而非工作树。读代码的腿跑在工作树 objectstack@90578117。两条腿结论一致。

    measurement dev dispatched by director seat session_01KGtaLpkW1mycWgkbSb3H6t (decision batch #21)


    Generated by Claude Code

  14. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    分支一执行:前提命中(普查 = 零)⇒ 同笔转 pm:queue

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 决裁批 #21 ① 裁决(5479573383)的机械执行

    前提裁定

    普查回贴(5479634264):host 注册 stdlib 之外 CEL 函数的路径数 = 0,阳性对照命中(stdlib 注册本身被扫描返回,扫描不盲);校验环境与运行环境的注册集一致。与 engine 席 A2.1(objectstack 腿:27 个 registerFunction 调用点全在 stdlib.ts)相互印证;交接单(5479554726)点名的差集已补测 —— hotcrm:10 个调用点全是消费、零注册、零 cel-js import;objectui 的 registerFunction 喂的是它自己的模板注册表(另一引擎),4 个调用者全是测试。结构性佐证照录:ERROR 在 validateExpression 侧自 #1877 已发货,任何注册额外函数的 host 今天就已在被拒而无人报障 —— 零读数与推理互证。

    ⇒ 分支一触发:needs-user-decision → pm:queue(同笔);priority:p1 / domain:engine 不动。

    实现精化(全部由裁决自身条款推出,⛔ 无新分支)

    1. oracle = 引擎实际注册集(cel-js check() 裁定),⛔ 不是 CEL_STDLIB_FUNCTIONS 常量。 测量席实测:该导出常量列 35 名,环境实际注册 72 名(type、map、filter、split、getFullYear、json 等 37 名未列)—— 按常量键控的门会把 37 个真函数拒成「未知」,直接违反裁决「全部 stdlib 对照不回归」条款。存在性由引擎定义(裁决范围纪律:「只查函数存在性」),check() 正是 validateExpression 已用的路径。35/72 漂移本身另立 finding 卡(见下一评论引用)。
    2. 不给 nearestName 建议。 A2.4 已被证伪到函数命名空间之内(nearestName('can', 函数集) 返回 'min' —— 误导)。裁决第 2 条自身原则「误导性建议宁可不给」⇒ 发货形态 = 引擎原话、零建议,与 validateExpression 现行为一致。
    3. 只拒未知函数裁定,⛔ 不搬运 check() 的其他抱怨。 函数存在性之外的类型盲区照旧保持盲区(type(record.x) == string 类合法 dyn 谓词不动)。这也是对撤回评论(5479554726)所指「dyn 误报腿」的结构性回答:本裁不是全量类型检查,误报通道被范围纪律关闭;且校验/运行两环境一致 ⇒ 校验不识的函数运行时必 fault,拒之非误报。
    4. 旧裁决取代机制照裁决第 3 条执行:模块注记改写(引旧裁原文 + objectui#4421 新证据 + 本裁权威),「盲区保持盲区」钉子作为本裁一部分收窄为「函数存在性除外」。engine dev 报告(5479498314)的选项 A(lint 门不动、改道 validateExpression)已阅、不改道 —— 维护者照准的分支一即 lint 门扩函数存在性,两者差异在检查所居之处而非发布期结果,机械执行不再重开选型。
    5. NOT MEASURED 帽子照裁决第 4 条随实现 PR:cloud、objectos(host 配置载体)、已发布第三方应用;另加测量席申报的一条 —— objectui 自有模板函数注册表(FormulaFunctions,另一引擎)不在本门辖内,帽子里写明免得被读成已测。
    6. 条款②照第 5 条:实现 PR 同笔挂 needs:contract-review,契约复审档位复审。

    卡面整理

    卡标题仍指控 validateExpression —— 已被两席独立双通道证伪(树 + 已发布制品)。标题同笔更新为实测事实与裁决方向;正文与评论链原样留作历史。旁支:#13821(误导性 hint 尾注)已在 finding 箱;formula tsconfig 测试排除并入 #12511/#4311 既有卡(dev 已申报,不另开)。


    Generated by Claude Code

  15. changed the title [-]validateExpression accepts a CEL call to a function that does not exist — publish-clean, then fail-closed and invisible at runtime[/-] [+]lint: `validate-visibility-predicates` passes unknown CEL functions clean (the `validateExpression` premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling[/+] on Aug 31, 2026
  16. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p1High: required for production / M2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions