fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(formula): unknown-function refusal names the function and points at the callable set - #14204

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription
Sep 1, 2026
Merged

fix(formula): unknown-function refusal names the function and points at the callable set#14204
os-support-ai merged 2 commits into
mainfrom
claude/issue-13821-unknown-function-prescription

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13821

An unknown-function refusal is graded type by the engine's own check(), so it fell through
bracesHint (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 and
parses fine — advice that cannot succeed. This is the second leg of the repair #7073 / PR #7209
made for the bounds class, for the reason boundsHint's own doc-comment gives: 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 unresolvable name.

What changed

validate.ts routes the type class to its own prescription, one class per arm, alongside the
existing bounds arm. The new hint names the function that did not resolve and points at the
callable set introspectScope publishes:

invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
> 1 | record.x.nosuchmethod('a')
^ — `nosuchmethod` is not a callable name here — a NAME fault, not a dialect mistake, so
re-spelling the expression will not fix it. The callable names this platform advertises for
authoring are the `functions` list `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of
those, or precompute the value in a stored field and reference that field instead.

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_FUNCTIONS is neither reshaped nor renamed; it is
pointed 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 nearestName budget 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 nearestName budget
is untouched, so field-name suggestions are unaffected. Both measured cases are pinned, in both
directions:

inputbeforeafter
isBlnk(record.name)dialect trailernames isBlnk, suggests `isBlank`
current_user.can(object, verb)dialect trailernames can, no suggestion

A third pin asserts nearestName('can', ...) still answers 'min' — if that ever stops being
true, 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:

  • an operator or ternary type mismatch (1 + 'a') — there is no name to hand back;
  • a real function given arguments no overload accepts (upper(1, 2) produces the identical
    found no matching overload for 'upper(int, int)' shape) — calling upper "not a callable name"
    would replace a useless sentence with a false one.

Tests

packages/formula/src/validate.test.ts gains 13 pins asserting the specific prescription text,
not merely that an error fires (an error already fired before this change). They mirror the bounds
suite's structure, including its flipped controls.

Ablation on the committed tree, reverting only the routing block: 7 type-class assertions go
red while the bounds control ("leaves the bounds prescription untouched") stays green — that
contrast 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 HEAD plus 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 drift
    tests cel-stdlib-drift.test.ts and skill-catalog-sync.test.ts, confirmed by name in a verbose
    run; both read CEL_STDLIB_FUNCTIONS, which this PR does not modify.
  • pnpm --filter @objectstack/formula typecheck — clean. --listFiles confirms it reads
    validate.ts. It does not read validate.test.ts (this package's tsconfig excludes
    **/*.test.ts), so the test file was type-checked separately through a throwaway
    tests-inclusive config: 0 errors in validate.test.ts.
  • Gate family derived from the actual diff via 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-loads and check:type-check-debt, each exiting 3 and each printing its own
    prerequisite banner (a full-repo build, or a saved turbo log). Exit codes captured by redirect
    before any pipe.
  • pnpm check:nul-bytes clean, plus a control-byte self-scan over the three changed files.
  • Downstream consumers that forward this message: validate-expressions.test.ts and
    validate-null-guards.test.ts in packages/lint pass (256 tests).
    validate-visibility-predicates.test.ts is NOT MEASURED — it fails to load on an unbuilt
    @objectstack/sdui-parser, an import chain unrelated to this diff.

Repo-wide pnpm lint is left to CI. The local run is a declared narrowing: eslint linted all
three changed files (count read from --format json, not assumed) with 0 errors and 0 warnings, and
--print-config shows parserOptions.project is null — type-aware linting is not enabled, so this
diff cannot move the verdict on any file it does not touch.

A repo-wide grep confirms nothing outside packages/formula asserts 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's
arm stays silent on it by design; the mistake is the call shape, not the name. Filed unassigned.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 4 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx(via validateExpression (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c41b42e8db6efdc7091e9c320c0598cd30c7777dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 41adf369c52c58415f6b05c9245c82ccd5741582 — the merge of head dabda374fae7901355f53c55ea9c8c205bfe976c into base c41b42e8db6efdc7091e9c320c0598cd30c7777d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c41b42e8db6efdc7091e9c320c0598cd30c7777d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude