Uh oh!
There was an error while loading. Please reload this page.
fix(formula): unknown-function refusal names the function and points at the callable set - #14204
Conversation
…at the callable set An unknown-function refusal is graded `type` by the engine's own check(), so it fell through bracesHint to the generic dialect trailer -- "predicates are bare CEL" handed to an author whose source already is bare CEL and parses fine. Second leg of the repair #7073 made for the bounds class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 41adf369c52c58415f6b05c9245c82ccd5741582 && git checkout 41adf369c52c58415f6b05c9245c82ccd5741582
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c41b42e8db6efdc7091e9c320c0598cd30c7777d dabda374fae7901355f53c55ea9c8c205bfe976c && git checkout -B drift-repro c41b42e8db6efdc7091e9c320c0598cd30c7777d && git merge --no-ff dabda374fae7901355f53c55ea9c8c205bfe976c
node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777d
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13821
An unknown-function refusal is graded
typeby the engine's owncheck(), so it fell throughbracesHint(null, no brace) and out to the generic dialect trailer. The author was told"predicates are bare CEL (e.g.
record.rating >= 4)" about a source that already is bare CEL andparses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the
boundsclass, for the reasonboundsHint's own doc-comment gives: an author whoobeys the last sentence they were given, an LLM author above all, rewrites the dialect, learns
nothing, and comes back with the same unresolvable name.
What changed
validate.tsroutes thetypeclass to its own prescription, one class per arm, alongside theexisting
boundsarm. The new hint names the function that did not resolve and points at thecallable set
introspectScopepublishes:The front half is untouched: it is cel-js's own vocabulary and matches the runtime fault exactly.
Only the trailer after the dash is new.
Message only. The refusal fires on exactly the same inputs as before — no rule id, no severity,
no match set, no gate behaviour.
CEL_STDLIB_FUNCTIONSis neither reshaped nor renamed; it ispointed at. The message states no member count, deliberately: what the catalog contains is
being adjudicated on #13933, and a sentence asserting a size would be falsified by that ruling
without failing any test here. A pin asserts the absence of a count.
did-you-mean ships WITH a threshold, and the threshold is the whole point
Against this catalog the shared
nearestNamebudget is measurably unsafe:nearestName('can', CEL_STDLIB_FUNCTIONS)answers'min'— two edits on a three-character name,a jump from a permission verb to a numeric function. That is worse than silence: an author who
takes it writes
min(object, verb)and is further from working than before it asked.This class therefore narrows locally to at most one edit per three characters of the longer
name, so at least two thirds of a suggestion must already be typed. The shared
nearestNamebudgetis untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:
isBlnk(record.name)isBlnk, suggests`isBlank`current_user.can(object, verb)can, no suggestionA third pin asserts
nearestName('can', ...)still answers'min'— if that ever stops beingtrue, the local threshold is no longer what protects the message and the silence pin has quietly
become vacuous.
What deliberately keeps the old trailer
cel-js emits one message shape for two different faults, so the arm is gated on the name being
absent from the advertised catalog. Faults that name no unresolvable call fall through untouched,
each with a pin:
1 + 'a') — there is no name to hand back;upper(1, 2)produces the identicalfound no matching overload for 'upper(int, int)'shape) — callingupper"not a callable name"would replace a useless sentence with a false one.
Tests
packages/formula/src/validate.test.tsgains 13 pins asserting the specific prescription text,not merely that an error fires (an error already fired before this change). They mirror the
boundssuite's structure, including its flipped controls.
Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the
boundscontrol ("leaves theboundsprescription untouched") stays green — thatcontrast is what proves a new class was routed rather than the shared tail replaced for everyone.
The mutation was confirmed on disk by blob hash before the run, and the restore proven after it by
an empty
git diff HEADplus a blob hash equal to HEAD's.Union re-run at
dabda374, after the final commit:pnpm --filter @objectstack/formula test— 26 files, 679 tests, all pass. Includes the drifttests
cel-stdlib-drift.test.tsandskill-catalog-sync.test.ts, confirmed by name in a verboserun; both read
CEL_STDLIB_FUNCTIONS, which this PR does not modify.pnpm --filter @objectstack/formula typecheck— clean.--listFilesconfirms it readsvalidate.ts. It does not readvalidate.test.ts(this package's tsconfig excludes**/*.test.ts), so the test file was type-checked separately through a throwawaytests-inclusive config: 0 errors in
validate.test.ts.dispatch-gates.mjs --repo objectstack-ai/objectstack:33 commands, 30 pass, 0 red, 3 NOT MEASURED. The three are
check-test-completeness,check:dual-build-cjs-loadsandcheck:type-check-debt, each exiting 3 and each printing its ownprerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
before any pipe.
pnpm check:nul-bytesclean, plus a control-byte self-scan over the three changed files.validate-expressions.test.tsandvalidate-null-guards.test.tsinpackages/lintpass (256 tests).validate-visibility-predicates.test.tsis NOT MEASURED — it fails to load on an unbuilt@objectstack/sdui-parser, an import chain unrelated to this diff.Repo-wide
pnpm lintis left to CI. The local run is a declared narrowing: eslint linted allthree changed files (count read from
--format json, not assumed) with 0 errors and 0 warnings, and--print-configshowsparserOptions.projectis null — type-aware linting is not enabled, so thisdiff cannot move the verdict on any file it does not touch.
A repo-wide grep confirms nothing outside
packages/formulaasserts the trailer this PR changes.Out of scope, filed separately
#14203 — the same family's remaining arm: a bare-callable stdlib function invoked as a receiver
method (
record.name.upper()) still gets the dialect trailer. The name is advertised, so this PR'sarm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.
Generated by Claude Code