Skip to content

validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821

Description

@claude

Summary

validateExpression refuses an unknown CEL function correctly, then hands the author a
prescription that cannot succeed: it tells them to write bare CEL, on a source that already
is bare CEL. This is the same defect #7073 / PR #7209 repaired for the bounds class,
still live for the type class.

Measured

packages/formula @ 936aa2d3a, @marcbachmann/cel-js@8.0.0:

validateExpression('predicate', "record.x.nosuchmethod('a')").errors[0].message

  invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'

  >    1 | record.x.nosuchmethod('a')
           ^ - predicates are bare CEL (e.g. `record.rating >= 4`).

The front half is right and is cel-js's own vocabulary, matching the runtime fault exactly.
The trailer after the dash is the generic dialect prescription. The same trailer is attached
to totallyBogusFn(1,2), current_user.can(object, verb), and every other unknown-function
refusal.

Why it is wrong

In validate.ts the hint is chosen as:

const hint = (compiled.error.kind === 'bounds' ? boundsHint(source) : null) ?? bracesHint(source);

so a type fault gets bracesHint, which returns null for a source with no {x} brace, and
falls through to the default dialect trailer. The dialect is not what is wrong: the source is
already a bare CEL predicate and parses fine. What is wrong is one name.

This is precisely the reasoning boundsHint's own doc-comment gives for the class it repaired:
"an author who obeys the last sentence they were given - an LLM author above all - rewrites the
dialect, learns nothing, and comes back with the same" broken expression. A type fault is in
the same position: the last sentence the author is handed points at the one thing that is
correct.

Why it matters here specifically

#13594 documents that this refusal is the publish-time guard against the failure mode an
AI-authored app is most likely to hit - a plausible-looking function name that does not exist
is exactly what a generator invents - and that the runtime consequence is fail-closed and
near-silent (the action is hidden for every user including ones holding the grant). The guard
fires correctly today; the self-correction message that ADR-0032 Decision 1d exists to provide
is the part that misdirects.

Suggested direction (not a decision)

Give the type class its own prescription, the way bounds got one: name the unknown
function, and point at the callable set rather than at the dialect. introspectScope already
publishes CEL_STDLIB_FUNCTIONS for exactly this audience.

A did-you-mean suggestion needs care and may be worth omitting. Measured: nearestName is
exported and reusable, but against the function set it answers
nearestName('can', CEL_STDLIB_FUNCTIONS) with 'min' - a suggestion across an unrelated
namespace, which is worse than silence. nearestName('isBlnk', ...) correctly gives
'isBlank', so a threshold that admits real typos and refuses distant matches is the shape to
aim for, if one is shipped at all.

Filed unassigned from the #13594 dev seat while measuring that card; no repair attempted here,
and out of that card's scope. Searched the 390 open issues before filing - no duplicate.


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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions