Skip to content

fix(plugin-grid): harvest row-action predicate fields from the object's userActions only - #5426

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5240-grid-userActions-collision
Aug 20, 2026
Merged

fix(plugin-grid): harvest row-action predicate fields from the object's userActions only#5426
os-support-ai merged 1 commit into
mainfrom
claude/issue-5240-grid-userActions-collision

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5240

Implements the maintainer ruling of 2026-08-20 (「4 张 同意」): Q1=A · Q2=B · Q3=B.

What was wrong

userActions names two different blocks, and the grid's $select harvest read them as one.

  • View-leveluserActions is toolbar policy — the spec's UserActionsConfigSchema: sort, search, filter, refresh, rowHeight, addRecordForm, editInline, buttons. It rejects editby name.
  • Object-leveluserActions is the CRUD-predicate blockedit / delete / create carrying visibleWhen / disabledWhen (objectui#2614). It is the only shape listViewPredicates can read: its loop skips every non-object value.

The harvest that decides which fields the server is asked for read the key view-first:

userActions: (schemaasany).userActions??(resolvedSchemaasany)?.userActions,

So a perfectly legal toolbar block shadowed the object's CRUD predicates. Every value in it is a boolean, the harvest collected zero predicates, the predicate's operand left the projection, CEL faulted on the absent key, failed closed, and the row Edit/Delete button disappeared for everyone with nothing pointing at the projection — objectui#3501's failure shape, reached with a success receipt at every step.

The view-level block is not hypothetical. Re-measured on this branch, @objectstack/spec@17.0.0:

proberesult
UserActionsConfigSchema.shape keyssort, search, filter, refresh, rowHeight, addRecordForm, editInline, buttons
UserActionsConfigSchema.safeParse({edit:{visibleWhen}})success: false, unrecognized_keys: ["edit"]
ListViewSchema.safeParse(… userActions:{search:true,sort:true})success: true
ListViewSchema.safeParse(… userActions:{edit:{visibleWhen}})success: false, unrecognized_keys: ["edit"] at path userActions
ComponentPropsMap['object-grid'].safeParse({objectName, userActions})success: false, unrecognized_keys: ["userActions"]

And two live producers write the toolbar block onto the view: packages/react/src/spec-bridge/bridges/list-view.ts (inside bridgeListView, which emits a type: 'object-grid' node — i.e. straight into the read this PR fixes; pinned by P1SpecBridge.test.ts) and packages/app-shell/src/views/ObjectView.tsx (built unconditionally as an object literal).

What changed

  1. packages/plugin-grid/src/ObjectGrid.tsx — the $select harvest reads resolvedSchema.userActionsonly. The view-schema-first operand is gone.
  2. Both userActions read sites carry a comment naming the collision — the surviving object-block read at resolveRowCrudAffordances too (Q3=B).
  3. packages/plugin-grid/src/__tests__/gridNonAuthorKeys.test.tsx — a new section pinning each clause of that comment. 11 new assertions.
  4. A patch changeset on @object-ui/plugin-grid.

Why the wording is load-bearing

An earlier dispatch of this card stopped without a PR: the original ruling's premises — "zero producers", "an undeclared view-level key", "an ADR-0049 dead override" — were falsified by measurement, one of them in the very file the card cited as proof. The action survived; the stated reasons did not. Q1=A exists to keep the falsified claim out of the census artifact, so the comment and the pin state the measured reason instead.

Concretely, the pin does not assert "no producer writes this" and does not call the key non-author surface. It asserts:

  • the view-level schema refuses edit / delete / create by name, and accepts the toolbar vocabulary (the control that makes those refusals mean something);
  • a view document carrying the toolbar block is spec-legal, while the CRUD block is refused on a view;
  • SpecBridge.transformListView really copies it onto the object-grid node — the producer is the evidence, exactly as in the NON_AUTHOR_KEYS table above it, except here it proves the opposite;
  • listViewPredicates harvests zero fields from the toolbar block and the operand from the CRUD block;
  • and the renderer behaviour: the object's predicate operand reaches $select, with a toolbar block present on the view.

The render channel of the surviving object-block read is already pinned four times over by rowCrudEffectiveOps.test.tsx's userActions opt-out control group, so it is cited in the docblock rather than duplicated.

Verification — all at efc95b643

No build artifact sits between any edit and any measurement here: vitest.config.mts aliases every @object-ui/* specifier to that package's src/, and this worktree had no dist/ at all while the suites ran (packages/core/dist, packages/react/dist, packages/plugin-grid/dist all absent), so source is provably what executed. @objectstack/spec is the published 17.0.0 from node_modules; nothing local builds it. The dependency closure was built afterwards, only because tsc --noEmit needs the .d.ts files.

Run from the repository root, per AGENTS.md:

pnpm exec vitest run packages/plugin-grid/ -> 81 files, 737 tests passed
pnpm exec vitest run packages/plugin-list/ -> 42 files, 632 tests passed (downstream consumer)
pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
-> 65 tests passed
pnpm --filter @object-ui/plugin-grid type-check -> exit 0 (after building the dep closure)
pnpm --filter @object-ui/plugin-grid lint -> 0 errors, 641 warnings (pre-existing)
pnpm run check:control-bytes / check:spec-symbols / check:phantom-deps / check:self-import -> all OK
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs -> OK

Reverse-verification (two legs, predicted before running, fix committed first)

Leg 1 — restore the view-first ??. Predicted: exactly one red, "…and a spec-legal toolbar block on the view does not knock it out"; the sibling case stays green because its fixture carries no view-level key. Observed: 1 failed | 30 passed, that assertion, expected [ 'id', 'name' ] to include 'status'. Matches.

Leg 2 — delete the userActions line from the harvest entirely (the "tidy finish" mutation). Predicted: two red — both projection cases lose status. Observed: 2 failed | 29 passed. Matches.

Leg 2 is why the first projection case is counted at all: it is green on both legs of leg 1, so on its own it pins nothing about this fix — it pins that the read still exists, and leg 2 is what shows it can go red.

Scope

plugin-grid only, per Q2=B. The identical (and worse — its left operand is always truthy on the app-shell path) instance at packages/plugin-list/src/ListView.tsx is #5398, deliberately serial behind this PR so both read sites end up with one shape; out of scope here, and #5398 remains open. The producer-side question — the bridge writing a key ComponentPropsMap['object-grid'] refuses by name — is routed through triage as its own spec-coordination card and is not addressed here either. Independently re-confirmed while checking downstream impact: ListView reads schema.userActions as toolbar policy and does not forward it into its child grid node, so the app-shell path is unaffected by this change.

Toolbar policy itself is untouched — it was never read through this path.


Generated by Claude Code

…'s userActions only
`userActions` names two different blocks. On a view it is toolbar policy
(`UserActionsConfigSchema`: sort/search/filter/refresh/rowHeight/addRecordForm/
editInline/buttons, which rejects `edit` by name); on an object it is the
CRUD-predicate block (edit/delete/create with `visibleWhen`/`disabledWhen`) —
the only shape `listViewPredicates` can read, since its loop skips every
non-object value.
The `$select` harvest read the key view-first, so a spec-legal toolbar block —
written by `SpecBridge.transformListView` onto the very `object-grid` node the
renderer receives, and by app-shell's `ObjectView` unconditionally — shadowed
the object's CRUD predicates. The harvest found none, the predicate's operand
left the projection, CEL faulted on the absent key and the row Edit/Delete
button failed closed for everyone (objectui#3501), with a success receipt at
every step.
The harvest now reads the resolved object block only. Both read sites carry a
comment naming the collision, and `gridNonAuthorKeys.test.tsx` pins each clause
of it.
Maintainer ruling 2026-08-20 on objectui#5240 (Q1=A, Q3=B).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-AYirSaOC.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)7.42KB2.32KB
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)1.61KB0.85KB
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.61KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
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.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.07KB32.77KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.19KB30.20KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.28KB53.06KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)20.08KB6.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.52KB20.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)36.10KB12.26KB
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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

schema.userActions is a sixth grid key read but undeclared — #5091's evidence claimed it was already in GRID_QUERY_INPUTS; it is not

2 participants

@os-support-ai@claude