Skip to content

fix(app-shell+kanban+list): row-predicate CEL authoring advertises runtime-bound roots; kanban binds host scope (#2571 follow-up) - #2603

Merged
os-zhuang merged 1 commit into
mainfrom
fix/cel-row-predicate-roots
Jul 16, 2026
Merged

fix(app-shell+kanban+list): row-predicate CEL authoring advertises runtime-bound roots; kanban binds host scope (#2571 follow-up)#2603
os-zhuang merged 1 commit into
mainfrom
fix/cel-row-predicate-roots

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

#2571 added scope/roots to CelPredicateField and wired the field conditional rules editors to scope="record". The remaining CEL call site — ConditionalFormattingEditor (hosted by ViewVariantInspector) — still authored row-predicate conditions with the engine-default advertisement, which has two traps:

  1. Autocomplete suggests roots the runtime never binds. The engine's default catalog advertises previous / input / os / vars, but the ADR-0058 row-predicate evaluator (evalRowPredicate — list rows, grid rows, kanban cards) binds none of them. Accepting one of those suggestions authors a condition that lints clean and silently never matches.
  2. The actually-bound global roots were missing from the catalog (features / app / data / ctx).

Runtime audit (why NOT scope="record")

The task hypothesis was that these predicates evaluate with the record bound only under the record namespace. Auditing @object-ui/core's evalRowPredicate shows otherwise: it binds the row bare (status), under record.*, and under data.* (documented as the three authoring conventions), plus the host's global predicate scope (current_user / user / ctx / app / featuresExpressionProvider, #1583/ADR-0068), and @objectstack/formula's buildScope merges the extra scope top-level. So a bare status == 'overdue'does match at runtime, and scope="record" (bare ref ⇒ hard lint error) would false-flag valid, documented predicates. Lint therefore stays 'flattened'; only the advertised roots are pinned to the runtime-bound set (ROW_PREDICATE_ROOTS).

Runtime gap closed alongside

Grid rows already bound the ambient predicate scope (ObjectGridusePredicateScope), but kanban cards did not — a features.* / current_user.* formatting condition worked on the grid and silently never matched on kanban, contradicting the "identical verdict" comments in both boards. Both KanbanEnhanced and KanbanImpl now bind usePredicateScope(); ListView's exported evaluateConditionalFormatting accepts the optional host scope for parity. Without this, advertising the global roots in the editor would have re-created the same authoring trap on kanban.

Changes

  • ConditionalFormattingEditor: pass scope="flattened" + roots={ROW_PREDICATE_ROOTS} (record, current_user, user, features, app, data, ctx) to CelPredicateField.
  • KanbanEnhanced / KanbanImpl: bind the ambient predicate scope in card conditional formatting.
  • ListView.evaluateConditionalFormatting: optional scope param.

Tests

  • Bare-field condition lints clean against the real engine (guards against flipping this surface to scope="record").
  • Unknown record.<field> still errors with did-you-mean.
  • Roots override: advertises features, withholds vars even when the engine advertises it.
  • Contract test: every ROW_PREDICATE_ROOTS entry resolves through evalRowPredicate with an ExpressionProvider-shaped scope; previous/input/os/vars provably don't (and must stay unadvertised).
  • Kanban: both boards style a features-gated card inside PredicateScopeProvider, fail soft outside; bare-field conditions keep working.

vitest: 225 tests green across the touched files; tsc --noEmit clean for app-shell / plugin-kanban / plugin-list (after workspace build). Pure bug fix — no changeset per AGENTS.md §9.

🤖 Generated with Claude Code

…ntime-bound roots; kanban binds host scope (#2571 follow-up)
The view designer's conditional-formatting editor (ConditionalFormattingEditor,
hosted by ViewVariantInspector) linted/completed with the engine-default
'flattened' advertisement, whose autocomplete offers roots the ADR-0058
row-predicate runtime never binds (previous / input / os / vars) — accepting
one authors a condition that silently never matches — while omitting the
roots that ARE bound (features / app / data / ctx).
Auditing the runtime first (as #2571 did for field rules): evalRowPredicate
binds the row's fields BARE, under record.*, and under data.*, plus the host
shell's global predicate scope (current_user / user / ctx / app / features —
ExpressionProvider, #1583/ADR-0068). Therefore:
- lint stays scope='flattened' — a bare `status == 'overdue'` DOES match at
runtime for row predicates, so scope='record' would false-flag valid
conditions; only the advertised roots change (new ROW_PREDICATE_ROOTS);
- kanban cards now bind the ambient predicate scope (usePredicateScope) in
both boards, closing the grid/kanban verdict gap for features.* /
current_user.* conditions (ObjectGrid rows already bound it);
- ListView's exported evaluateConditionalFormatting accepts the optional
host scope for parity.
Tests: bare-field condition lints clean (real engine); the roots override
advertises `features` and withholds `vars`; a ROW_PREDICATE_ROOTS ↔
evalRowPredicate contract test keeps the advertised catalog honest against
the evaluator; kanban boards style a features-gated card only inside
PredicateScopeProvider and fail soft outside it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJul 16, 2026 1:13pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)27.6 KB350 KB
Entry fileindex-BlZlaXcD.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.17KB2.95KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)21.70KB4.21KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.12KB3.41KB
auth (LoginForm.js)17.12KB5.00KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.63KB2.15KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)30.17KB7.28KB
auth (createAuthenticatedFetch.js)3.93KB1.55KB
auth (index.js)1.83KB0.79KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.86KB0.85KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.42KB0.96KB
collaboration (index.js)1.25KB0.53KB
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)446.26KB96.59KB
core (index.js)1.82KB0.62KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)115.42KB28.53KB
fields (index.js)207.90KB50.76KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)21.22KB4.69KB
i18n (useSafeTranslation.js)2.68KB0.98KB
layout (index.js)38.45KB10.67KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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)4.42KB1.27KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)1.77KB0.77KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)5.90KB2.15KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.46KB1.03KB
permissions (evaluator.js)4.00KB1.23KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.50KB0.70KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.12KB12.34KB
plugin-charts (index.js)46.31KB13.06KB
plugin-chatbot (index.js)176.76KB42.28KB
plugin-dashboard (index.js)117.25KB29.14KB
plugin-designer (index.js)213.89KB43.03KB
plugin-detail (index.js)211.10KB51.37KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)104.96KB25.43KB
plugin-gantt (index.js)149.18KB37.17KB
plugin-grid (index.js)172.00KB45.43KB
plugin-kanban (index.js)47.23KB12.88KB
plugin-list (index.js)97.88KB23.12KB
plugin-map (index.js)16.80KB5.24KB
plugin-markdown (index.js)13.65KB4.67KB
plugin-report (index.js)37.63KB9.92KB
plugin-timeline (index.js)25.37KB7.20KB
plugin-tree (index.js)8.36KB2.81KB
plugin-view (index.js)85.72KB20.86KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.55KB0.67KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)3.19KB1.38KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)18.70KB6.09KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)0.76KB0.42KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)2.16KB0.94KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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 (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)1.67KB0.75KB
types (layout.js)0.20KB0.18KB
types (mobile.js)0.20KB0.18KB
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.26KB1.96KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)0.75KB0.46KB
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-zhuang
os-zhuang merged commit 92a3440 into mainJul 16, 2026
10 checks passed
@os-zhuang
os-zhuang deleted the fix/cel-row-predicate-roots branch July 16, 2026 13:22
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.

1 participant

@os-zhuang