Skip to content

fix(view): the spec→FilterBuilder map follows the four operators #2942 added - #3022

Merged
os-zhuang merged 1 commit into
mainfrom
claude/canonical-to-builder-follows-2942
Jul 30, 2026
Merged

fix(view): the spec→FilterBuilder map follows the four operators #2942 added#3022
os-zhuang merged 1 commit into
mainfrom
claude/canonical-to-builder-follows-2942

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Found while implementing objectstack-ai/objectstack#4135 — a concurrent change made a table I added in #2989 quietly wrong.

The drift

CANONICAL_TO_BUILDER (view-config-utils.ts) mapped four canonical VIEW_FILTER_OPERATORS members to null, with a comment asserting the reason:

starts_with/ends_with have no FilterBuilder operator at all, and is_null/is_not_null carry a NULL/empty distinction that isEmpty/isNotEmpty erase

The first half stopped being true. #2942 added startsWith, endsWith, isNull and isNotNull to the FilterBuilder — its own comment says they "validated at the data layer but were unreachable from this dropdown". This table did not follow, so a stored view carrying any of the four still reached the builder as a raw spelling it could by then have rendered, and the comment explaining why was simply false.

All four now map. is_null/is_not_nullisNull/isNotNull, not isEmpty/isNotEmpty: the builder draws both pairs now, so folding the NULL predicate onto the empty-string one would silently rewrite the author's operator the next time the view was saved. The unmapped set is empty — all 19 canonical members translate.

The guard could not have caught this. Now it can.

The parity test asserted that the unmapped set equalled a hand-kept list of gaps. That comparison stays true when the builder gains an operator, because neither side of it moves — the exact drift that happened.

The new assertion is derived rather than hand-kept:

it('leaves nothing unmapped that the builder can already draw', () => {
  const byFolded = new Map(FILTER_BUILDER_OPERATORS.map(id => [fold(id), id]));
  const missed = Object.entries(__CANONICAL_TO_BUILDER)
    .filter(([, id]) => id === null)
    .filter(([op]) => byFolded.has(fold(op)))
    

starts_with and startsWith fold to the same key, so an unmapped canonical operator whose folded name matches a folded builder id is an omission by definition — no list to remember.

Confirmed it has teeth rather than trusting the green. Reverting the four mappings to null reproduces the original drift as four named failures:

+ "starts_with -> startsWith exists but is unmapped",
+ "ends_with -> endsWith exists but is unmapped",
+ "is_null -> isNull exists but is unmapped",
+ "is_not_null -> isNotNull exists but is unmapped",

Also added an explicit assertion that the NULL and empty-string pairs stay distinct, since that is the one mapping a future edit might be tempted to collapse.

Verification

  • plugin-view: 72 tests across 4 files, green (12 in the parity guard, up from 9).
  • The guard was confirmed failing before the fix, as quoted above.

Note on scope

This is the same public-API surface as #2989toFilterGroup has no in-repo caller — so no in-repo behaviour changes. It is worth fixing because the table is the third of the three the #2901 audit identified, and because a guard that cannot detect a whole class of drift is worse than one that says so.

Refs #2945, #2942, #2989

🤖 Generated with Claude Code

…added

CANONICAL_TO_BUILDER mapped starts_with, ends_with, is_null and is_not_null
to null, with a comment asserting the FilterBuilder had no such operator.
#2942 gave it startsWith/endsWith/isNull/isNotNull and this table did not
follow — so a stored view carrying any of the four still reached the builder
as a raw spelling it could by then have rendered, and the comment was false.

is_null/is_not_null map to isNull/isNotNull, NOT isEmpty/isNotEmpty: the
builder draws both pairs now, and folding the NULL predicate onto the
empty-string one would rewrite the author's operator on the next save.

The old guard could not catch this — it compared the unmapped set against a
hand-kept gap list, and neither side moves when the BUILDER gains an
operator. The new assertion is derived: starts_with and startsWith fold to
the same key, so an unmapped canonical operator whose folded name matches a
folded builder id is an omission by definition. Confirmed by reverting the
four mappings, which reproduces the drift as four named failures.

The unmapped set is now empty — all 19 canonical members translate.

Refs #2945, #2942, #2989

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 30, 2026 10:58am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-QekY3Rio.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 457.75KB 99.96KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 134.67KB 34.24KB
fields (index.js) 222.07KB 54.35KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.59KB 28.74KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.52KB 53.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 104.83KB 25.30KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 180.86KB 47.45KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 102.39KB 24.18KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.91KB 20.99KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 2.00KB 0.96KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 1.08KB 0.64KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Sign up for free to 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.

1 participant