fix(formula): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk
, '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): prescribe the bare call shape for a stdlib function written as a method - #14447

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription
Sep 2, 2026
Merged

fix(formula): prescribe the bare call shape for a stdlib function written as a method#14447
os-musk merged 1 commit into
mainfrom
claude/issue-14203-receiver-call-prescription

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14203

The defect

validateExpression refused record.name.upper() correctly and then handed the author the generic dialect trailer — "predicates are bare CEL (e.g. record.rating >= 4)" — advice that cannot succeed, because the source already IS bare CEL and parses fine. The third instance of the family repaired for the bounds class (#7073) and the unknown-name class (#13821), and the one neither could cover: #13821's arm fires only when the name is ABSENT from CEL_STDLIB_FUNCTIONS, and upper is present, so this class had no prescription at all. The name is right; the call SHAPE is wrong.

What changed

The type class now carries two disjoint arms. The new one prescribes the bare call, assembled from the SOURCE:

invalid CEL predicate: found no matching overload for 'dyn.upper()'
> 1 | record.name.upper()
^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
a dialect mistake, so re-spelling the expression will not fix it. Write
`upper(record.name)` instead. The callable names this platform advertises for
authoring (the `functions` list `introspectScope` returns,
`CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
receiver methods (`record.name.split(',')`) are written after a dot.

The spelling has to come from the source: cel-js's message names the receiver's TYPE (dyn.upper(), string.upper()), never the author's expression, so NO_OVERLOAD_RE discarding the receiver prefix loses nothing that could have been printed. When the receiver is not a plain dotted identifier chain — record.tags[0].upper(), (record.a + record.b).upper(), 'literal'.upper(), and the measured near-miss record.x[0].name.upper() — the message names the call SHAPE (upper(…) with the receiver as its first argument) rather than inventing a spelling it cannot derive. Inventing one would repeat, one level up, the defect this card is about.

The two keys, and why one of them is not enough

Per the triage fence, the arm is keyed on membership of the bare-callable catalog, never on call shape alone — plus a second key the measurement forced.

Measured on this branch through buildEnv().getDefinitions(), the same seam celEngine.compile and celEngine.evaluate build with (cel-js registers 72 distinct names over 134 definitions):

classcountexamplebehaviour
advertised, bare-only28upper, lower, isBlank, daysFromNowthe new arm — receiver call gets the bare-call prescription
advertised, registered BOTH ways7contains, endsWith, matches, size, startsWith, string, trimunchanged — keeps today's trailer
receiver-only, never advertised33split, map, getFullYearunchanged — valid as receiver calls, never reach this arm

The both-forms row is the one catalog membership alone would have got wrong: record.name.contains() is a real receiver call faulting on ARITY, and "write contains(record.name)" would fault just as hard. That class keeps the existing trailer, because its fault is the arguments — the blind spot #13594 deliberately keeps blind. The arm therefore also asks isReceiverRegistered(name), read from the environment rather than from a hand-copied list, so it cannot drift.

Files

  • packages/formula/src/validate.tsreceiverCallHint and receiverChainInSource, and the kind === 'type' routing. The two arms are disjoint by construction rather than by ordering: the new one fires only when the name IS advertised, validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's only when it is not.
  • packages/formula/src/unknown-function.ts — the declared extractor growth: receiverCallNameFromNoOverload (a sibling regex with the receiver prefix REQUIRED, so the message-shape knowledge keeps one home) and isReceiverRegistered. callNameFromNoOverload and firstUnknownFunctionCall are unchanged in behaviour; the memo now carries both name sets off one getDefinitions() read.
  • packages/formula/src/validate.test.ts — 26 cases in a new #14203 block.
  • packages/formula/src/unknown-function.test.ts — 26 cases pinning the two new exports.
  • .changeset/formula-receiver-call-prescription.md@objectstack/formula patch.

Negatives — one per class the shape alone would have swallowed

  • receiver-only names stay VALID as receiver calls: record.name.split(','), record.dates.map(d, d), record.created.getFullYear();
  • both-forms names stay valid when called correctly (record.name.contains('x'), record.name.trim()) and keep the dialect trailer when they fault on arity (record.name.contains(), record.name.startsWith(), record.n.matches());
  • a bare-callable name called BARE with wrong arguments (upper(1, 2)) and an operator mismatch (1 + 'a') keep the trailer;
  • validateExpression's unknown-function refusal hands the author the dialect prescription - advice that cannot succeed on a source that is already bare CEL #13821's arm answers exactly what it answered before, in both call forms (record.name.nosuchmethod(), split(record.name, ','), nosuchfn(record.name)), and the bounds prescription is untouched;
  • and the control that makes the pair a pair: upper(record.name) — the prescription this arm hands out — type-checks. A repair that prescribed something unusable would be another sentence that cannot succeed.

The both-forms set is pinned as a measured list, not remembered: if cel-js moves a name across that line, the pin reddens before the arm silently changes class.

Ablation

Predicted before the run: reverting the routing to unknownFunctionHint alone sends the class back through bracesHint (null) to the dialect trailer, so the ten prescription cases go RED and every negative stays GREEN.

Measured, exactly that: 10 failed, 150 passed over the two test files. The mutation was confirmed on disk before the run — the removed text went 1 occurrence to 0 while the injected text stayed at 1, with git diff HEAD --stat showing 1 insertion / 1 deletion — and the restore leg was proven by git diff HEAD being empty AND git hash-object on the file equalling its HEAD blob e2b884a8 (a non-empty hash, compared rather than assumed). The restoring trap used absolute paths resolved from git rev-parse --show-toplevel.

No rebuild leg applies.validate.test.ts imports ./validate relatively, so vitest resolves the package's own source; nothing in this ablation reads dist/. A first attempt of the same ablation is recorded as VOID rather than as a reading: it passed the test paths package-prefixed, vitest matched no files and exited 1 — an exit 1 that is not a red gate.

Verification

All of the following ran on this branch at 8909ee86e, exit codes captured after redirect, never through a pipe.

  • pnpm --filter @objectstack/formula test — 27 files, 764 passed
  • pnpm --filter @objectstack/formula typecheck — clean
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, no narrowing claimed
  • the 34-family gate union re-derived from the real changeset by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no hand-built path list) — 32 green, plus pnpm check:error-status-conformance and pnpm check:nul-bytes green
  • pnpm check:type-check-debt --re-measure green: 27 ledger entries re-measured, "none above its recorded number, surplus: none". This is the reading that covers the two edited TEST files — packages/formula/tsconfig.json excludes **/*.test.ts, so the package's own typecheck script says nothing about them (confirmed with tsc --listFiles: 0 hits). The TEST_DEBT ratchet is where they are measured, and it did not move.
  • two of the 34 returned exit 3 = NOT MEASURED, recorded as such and not as passes, in each gate's own words: scripts/check-test-completeness.mjs ("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") and scripts/pm/check-half-states.mjs ("the trigger-file index gathered nothing, so this result says NOTHING about whether the board carries half-states").

Consumers

validateExpression is ADR-0032's shared validator (~10 expression slots including @objectstack/lint's gate and the validate_expression MCP tool). Grepped origin/main for a test outside packages/formula pinning the dialect trailer for a receiver-call input: none — the other "are bare CEL" strings belong to packages/lint and service-automation's own messages, different producers. Spot-checked the largest consumer anyway: @objectstack/lint's three expression validators, 422 passed.

Scope

No change to CEL_STDLIB_FUNCTIONS membership, to stdlib.ts, or to cel-js's environment, and no change to what validateExpression ACCEPTS: the receiver call was refused before and is refused after. Only the sentence the author is told to act on changes. Card labels and assignee untouched.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…tten as a method
`validateExpression` refused `record.name.upper()` correctly and then handed the
author the generic dialect trailer ("`predicate`s are bare CEL"), advice that
cannot succeed on a source that already IS bare CEL and parses fine. #13821's
unknown-name arm stays silent here by design -- `upper` IS advertised, so calling
it "not a callable name" would replace a useless sentence with a false one -- so
the class had no prescription at all. The name is right; the call SHAPE is wrong.
The `type` class now carries two disjoint arms. The new one fires when the name
cel-js reports IS in the bare-callable catalog `CEL_STDLIB_FUNCTIONS` AND the
message shows a receiver form AND the environment does not register the name as
a receiver method; it prescribes the bare call assembled from the SOURCE
(`upper(record.name)`), because cel-js's message names the receiver's TYPE
(`dyn.upper()`) and never the author's expression. A receiver that is not a
plain dotted chain gets the call shape instead of an invented spelling.
Keyed on catalog membership plus the environment's own record of the receiver
form, never on call shape alone: the 33 receiver-only names (`split`, `map`,
`getFullYear`) stay valid as receiver calls, and the seven both-forms names
(`contains`, `endsWith`, `matches`, `size`, `startsWith`, `string`, `trim`)
keep today's trailer when a receiver call of them faults on arguments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula, touching 10 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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 d16df741808edbe39e0b6e09c993e440bad78444packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71dc706aa2abe51edb33a8e31571acf5305746df — the merge of head 8909ee86eaa3505991b9993d3f378ff55bafe476 into base d16df741808edbe39e0b6e09c993e440bad78444, 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 71dc706aa2abe51edb33a8e31571acf5305746df && git checkout 71dc706aa2abe51edb33a8e31571acf5305746df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d16df741808edbe39e0b6e09c993e440bad78444 8909ee86eaa3505991b9993d3f378ff55bafe476 && git checkout -B drift-repro d16df741808edbe39e0b6e09c993e440bad78444 && git merge --no-ff 8909ee86eaa3505991b9993d3f378ff55bafe476
node scripts/docs-audit/affected-docs.mjs --json d16df741808edbe39e0b6e09c993e440bad78444

⚠️ 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 d16df741808edbe39e0b6e09c993e440bad78444 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

ACCEPT on the card: comment 5505968164 (#14203). Flip pre-checks on head 8909ee86e: every check run completed with conclusion success or skipped (33 runs; Lint & Repo Gates completed 07:39:02Z, TypeScript Type Check 07:27:47Z); governed-surface test on the PR's 5 paths: NOT governed — ordinary queue landing applies; closing-keyword two-read done at ACCEPT. Clause-② self-reading was no (formula diagnostics text, no contract member moves), so no contract-review carrier applied.

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 1d8ad0fSep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-14203-receiver-call-prescription branch September 2, 2026 08:03
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

Development

Successfully merging this pull request may close these issues.

validateExpression hands the dialect prescription to a stdlib function called as a receiver method (record.name.upper()) - advice that cannot succeed

1 participant

@os-musk