Skip to content

docs(core): state the registerFunction case-fold on the method itself - #5578

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5363-registerfunction-casefold-doc
Aug 21, 2026
Merged

docs(core): state the registerFunction case-fold on the method itself#5578
os-sales merged 1 commit into
mainfrom
claude/issue-5363-registerfunction-casefold-doc

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5363

What this is — and what it deliberately is not

The card reads like a bug: ExpressionEvaluator.registerFunction('formatCurrency', fn)
registers FORMATCURRENCY, so ${formatCurrency(price)} renders its own ${...} source
on screen. This PR changes no behavior. It states the case-fold on the method that
performs it, so the declaration matches what the code enforces, and pins the half of that
contract nothing covered.

I re-derived the fork rather than taking it on trust, and agreed with triage: direction 1.
Reasoning is in the report comment on #5363; the short form is that the fold is the
registry's contract, not an accident of this method — FormulaFunctions.register folds,
and every built-in (SUM, IF, UPPER) is registered through the same call. Making only
registerFunction case-preserving would put two dialects in one registry, which is exactly
what commandment #0.1 exists to stop, and it is a breaking change rather than an
unobserved one: ActionRunner.scriptAwait.test.ts already registers 'doWrite' and calls
DOWRITE(). That is a Feature card through the decision inbox, not a drive-by, so I did not
write it.

Premise re-verified on today's main

The issue measured against bdf8cf76e; origin/main is now ac73c24b0. Re-measured against
a fresh packages/core/dist/ build — the premise holds, and one detail is sharper than the
card states:

proberesult
evaluate('${formatCurrency(price)}') after registering 'formatCurrency''${formatCurrency(price)}' — the source, verbatim
evaluate('${FORMATCURRENCY(price)}')'$1,234.50'
getFormulas().has('formatCurrency') / .get(...)true / functioncase-insensitive
getFormulas().toObject() keyFORMATCURRENCY
evaluateExpression('formatCurrency(price)')throws "formatCurrency" is not a function

The sharper detail: the registry API is case-insensitive (has/get answer to the
original spelling). Only expressions are case-sensitive, because the evaluation scope is
built from toObject() — a plain object whose identifiers match exactly. That is why the
fold is invisible right up until someone writes an expression, and it is the part the new
JSDoc leads with.

The pin can fail — measured, not asserted

The dispatch asked for a pin that proves something about the documented contract, or none
at all. Registering 'DOUBLE' and calling DOUBLE(...) proves nothing, so the three new
cases pin the uncovered half: that the given spelling does not resolve, that the failure
renders raw template source instead of raising, and that the registry API stays
case-insensitive underneath.

Reverse-verified by ablation — FormulaFunctions.register mutated to set(name, fn), i.e.
direction 2 simulated:

Tests 3 failed | 79 passed (82)

All three new cases go red; the 79 pre-existing ones stay green — so nothing previously
covered this fold at all, and the pin is a real tripwire on the behavior change this card
declined. Mutation was confirmed on disk by anchored grep -c on both the removed and the
injected text (1→0 and 0→1) plus git diff --stat, never an editor exit code; the script
carried a trap ... EXIT INT TERM restore, and the tree was verified byte-identical to HEAD
afterwards (git status --porcelain empty). The suite resolves its subject by relative
source path
, so no dist is in the loop and no rebuild sits between mutation and result.

Changeset: a real patch, not the empty-frontmatter exemption

.changeset/README.md says not to write changesets for documentation updates. That rule is
about documentation files; this is source in a released package, and the JSDoc is emitted
into the published artifact — measured:

packages/core/dist/evaluator/ExpressionEvaluator.d.ts:134: * **The name is case-folded: it is stored — and must be called — in UPPER

So it is what consumers see on hover, which is a user-visible change to what npm ships.
Cheap to overrule: swap the frontmatter for the empty form.

Verification

Exit codes captured before any pipe; each gate's own verdict line quoted.

gateresult
pnpm --filter @object-ui/core lintLINT_EXIT=0✖ 513 problems (0 errors, 513 warnings), all pre-existing no-explicit-any
pnpm --filter @object-ui/core type-checkTYPECHECK_EXIT=0 (tsc --noEmit && tsc -p tsconfig.test.json)
pnpm exec vitest run packages/core/src/evaluator/__tests__/ExpressionEvaluator.test.tsTest Files 1 passed (1) · Tests 82 passed (82)
pnpm exec vitest run packages/core/ (whole package)Test Files 93 passed (93) · Tests 1945 passed (1945)
node scripts/check-control-bytes.mjsEXIT=0✅ OK (scanned 4629 tracked text file(s))
node scripts/check-changeset-presence.mjsEXIT=0✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjsEXIT=0✅ No changeset declares a major bump
node scripts/check-changeset-fixed.mjsEXIT=0✅ All workspace packages are in the changeset fixed group

All run at 29d23acc0, which is the head of this branch.

Repo-wide pnpm lint / pnpm type-check are left to CI, and the narrowing is declared
rather than assumed.
Evidence, since a narrowing without it is just a skipped run:
eslint's own population for packages/core is 185 files (count read from --format json,
not guessed), both touched files are in it; and root eslint.config.js declares no
projectService / parserOptions.project, so linting is not type-aware and this diff
cannot move a verdict in any file it does not itself contain. For type-check the invariance
is stronger still: the ExpressionEvaluator.ts diff contains zero non-comment lines
(single hunk at line 349; lines 1–200 byte-identical to origin/main), so the emitted type
signature is unchanged and no consumer package can be affected.

One measurement worth flagging: eslint . --no-inline-config reports 5 errors in
packages/core, but that flag is not what this repo's gate runs (pnpm lint is
turbo run lint → per-package eslint .). All 5 sit at deliberate eslint-disable
comments in code this PR does not touch — including the ES2020 preserve-caught-error
waiver at ExpressionEvaluator.ts:176, proven pre-existing above.

Scope

Exactly the dispatched surface, no breach: ExpressionEvaluator.ts, one test file, one
changeset. FormulaFunctions.ts carries the same fold undocumented on its own register,
but it is outside the declared surface and its toUpperCase() is visible in its one-line
body — noted in the report rather than filed or fixed.


Generated by Claude Code

…#5363)
`ExpressionEvaluator.registerFunction` delegates to `FormulaFunctions.register`,
which stores under `name.toUpperCase()`, so `registerFunction('formatCurrency',
fn)` registers `FORMATCURRENCY` and only that spelling resolves inside `${...}`.
The fold is deliberate — the built-in formula vocabulary is spreadsheet-style
(`SUM`, `IF`, `UPPER`) and goes through the same call — but it was declared
nowhere on the public method, and two mechanisms hide it: the registry API stays
case-insensitive (`has`/`get` answer to the original spelling), and a wrong-case
call site soft-fails through `evaluate()`'s catch to `defaultValue ?? expression`,
rendering the template's own source as literal text instead of raising.
Documentation only; behavior is unchanged. Three test cases pin the previously
uncovered half so that making registration case-preserving fails a test rather
than silently invalidating the JSDoc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.8 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-BwZDAHOh.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.61KB59.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.96KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.48KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 14:51
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit f2158ecAug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5363-registerfunction-casefold-doc branch August 21, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(core): ExpressionEvaluator.registerFunction upper-cases the name, so a lower-case call in an expression renders its own ${...} source

2 participants

@os-sales@claude