Skip to content

fix(plugin-grid,fields,app-shell): a promoted bulk action's field-backed param resolves through the shared action-param resolver (objectui#12105) - #12119

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12105-bulk-param-field-resolve
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12105-bulk-param-field-resolve

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12105
Clause-②: yes

A promoted bulk action's field-backed param ({ field, objectOverride }) now resolves through the same resolver as the single-record path, so the selection bar's dialog renders the record picker and the confirm step shows the record's label. One resolver, shared: it moved out of app-shell into @object-ui/fields. There is no second copy.

Implemented by os-dev for the domain:ui seat, session https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz, branch claude/issue-12105-bulk-param-field-resolve.

The defect, reproduced red on main a21ff9a

A throwaway probe against main's source, with HotCRM's shape (params: [{ field: 'crm_campaign', objectOverride: 'crm_campaign_member' }]):

  • resolveBulkActions produced {"field":"crm_campaign","objectOverride":"crm_campaign_member","required":true,"name":"crm_campaign","type":"text"}.
  • bulkParamToField turned that into a text widget.
  • BulkActionDialog rendered input#bulk-param-crm_campaign with type="text".

The card's premise holds. toBulkParam renamed field onto name, defaulted type to 'text', and read object only off an inline reference. It never looked at the field definition.

What changed, and why each package is touched

  • @object-ui/fields receives the resolver. resolveActionParams.ts moved here with git mv, unchanged in behaviour, together with the three-row param-type alias table and resolveParamWidgetType. The index exports them (list under Clause-② below). The one other edit to the moved file is a module-scoped type for the bundler-replaced process.env.NODE_ENV read. This package has no node typings. The runtime read is the same bytes it was in app-shell, and nothing ambient is declared, so it cannot shadow node's global anywhere.
  • @object-ui/app-shell now imports the resolver from @object-ui/fields. Its two callers changed (useConsoleActionRuntime, RecordDetailView), and paramToField / ActionPreview now read resolveParamWidgetType from fields. The resolver was never exported from app-shell, so its public face is unchanged. Most of the files counted under app-shell are tests whose import line moved. Two tests key on the file's path: the INHERITED_TARGET citation pin and core's column-identity ratchet. Both were repointed.
  • @object-ui/plugin-grid is the fix.
    • resolveBulkActions.ts: the fold no longer attaches promoted params. A new resolvePromotedBulkParams(def, ctx) runs resolveActionParams and renames the resolved param onto the bulk vocabulary (helpText → help, defaultValue → default, referenceTo → object, displayField → labelField, plus dependsOn, accept and maxSize). It contains no field lookup of its own.
    • ObjectGrid.tsx: dispatchBulkActionDef resolves when the dialog opens. It first awaits useMetadata().ensureType('object'), then unions the grid's own object schema with the session's objects (withKnownObjects, the grid's first). That is the record dialog's order, so an objectOverride target that has not loaded yet is waited for, not refused.
    • BulkActionDialog.tsx: a param the resolver could not resolve (unresolvedField) is refused before any widget is built, and Next is disabled. This matches ActionParamDialog and reuses its actionDialog.unresolvedParam copy. Without it, the resolver's own warning, "The action dialog refuses the param", would be false on the bulk path, and the missing-field case would still draw a raw text box. The key travels on a plugin-grid-local reader (unresolvedFieldOf), not as a declared BulkActionParam member, because that interface is also the authoring face (AGENTS.md #0.1).
    • The stale "left open there" sentence in the dialog's dependsOn note now says the promoted route exists.
  • @object-ui/core: comments only. Four comments named the resolver's old home (expand-fields.ts, predicate-fields.ts, reference-keys.ts, one test). The column-identity ratchet total goes from 12 to 11, because the hand-mapper's name || field read is gone.
  • Docs: the fields README has a new "Resolving declared action params" section. plugin-grid.mdx and enhanced-actions.mdx each gain a short paragraph. Changeset: .changeset/12105-bulk-param-field-resolve.md.

Why the resolver moved instead of being injected

I measured what the resolver imports before choosing. It reads @object-ui/core (EXPANDABLE_FIELD_TYPES and the ActionParamDef / ActionParamOption types), the fields widget fold (resolveFormWidgetType, through resolveParamWidgetType) and @objectstack/spec/ui (resolveI18nLabel). Its only app-shell-local import was paramToField.ts's resolveParamWidgetType, a thin wrapper over the fields fold plus a three-row alias table. plugin-grid already depends on all three of those packages. @object-ui/fields is the one package both sides depend on that holds all of them. @object-ui/core cannot host the resolver, because it cannot import the fields widget map.

  • Move (chosen). It adds 5 values and 3 types to @object-ui/fields, and nothing to plugin-grid's or app-shell's published face. Every host gets the fix, including a grid rendered without the console.
  • Inject (rejected). It would add a context, a provider and a value type to plugin-grid, plus a provider mount in app-shell. That is fewer exports, but the fix would depend on the host. A grid with no provider would still need some answer for a field-backed param, and the old toBulkParam mapping is that answer. That leaves two resolution behaviours, and the one a host gets by forgetting the provider is the defect this card reports.
  • The cost of the move. fields/src/resolveActionParams.ts imports resolveFormWidgetType from the package barrel that re-exports it. That is a cycle in the source graph only, and only at call time: nothing at the module's top level reads it, and the published bundle is one file. The fields build printed no circular-dependency warning, and check:node-esm-load loads the built entry.

Spec guidance sentence for the objectstack follow-up

Per the card, objectstack is not edited here. Quoted verbatim from objectstack origin/main 7098acaef, packages/spec/src/ui/bulk-action.zod.ts, BulkActionParamSchema's guidance.field:

field declares a FIELD-BACKED param, and the bulk surface has no such route: resolveActionParams consults the object's field definitions for the single-record dialog, resolveBulkActions's toBulkParam never does. Declare the param inline instead — name + type, plus object (and optionally labelField) for a picker.

After this PR the clause "toBulkParam never does" is false for a promoted action. An action declared on the object and named in a view's bulkActions now resolves its field-backed params. An authored bulkActionDefs[].params entry still has no field-backed route, so the refusal stays right and only its reason and remedy change. The remedy could be: "or declare the action on the object with a field-backed param and name it in bulkActions". Three more sentences in the same file make the same claim:

  • the BULK_PARAM_WIDGET_CONFIG_KEYS prescription: "⛔ Declaring the key on the object's FIELD does not reach this dialog either: the bulk surface has no field-backed param route."
  • the JSDoc above that constant: "There is no field-backed param route on the bulk surface — toBulkParam never consults the object's field definitions"
  • the dependsOn JSDoc: "the FIELD-BACKED route (resolveActionParams resolves the object's field definitions), which is the very route the bulk surface does not have."

For the widget-config one, note that a promoted lookup param receives only object, labelField and dependsOn from its field so far (see Acceptance notes).

Tests

New file: packages/plugin-grid/src/__tests__/bulkFieldBackedParam-12105.test.tsx, 8 cases.

  • Pin: a field-backed lookup param on the bulk path. Unit level: it resolves to type: 'lookup', object: 'crm_campaign', labelField: 'name', and bulkParamToField builds the picker. A parity case shows the same answer as resolveActionParam for the same declaration.
  • Pin, through the real ObjectGrid + BulkActionDialog: lookup-trigger-crm_campaign renders and input#bulk-param-crm_campaign does not. The confirm step shows "Spring Launch" (via findOne('crm_campaign', 'c1')). Run sends crm_campaign: 'c1' once per selected record. The metadata store loads late, so the case also pins the ensureType('object') wait.
  • CONTROL: a literal-typed param is unchanged at unit level (same widget, label, help, default, placeholder; an inline reference: 'sys_user' still promotes to the person picker) and at grid level (input#bulk-param-note with type="text").
  • Refusal: a field-backed param whose field is missing shows bulk-param-unresolved-crm_campaign with the OBJECT.FIELD locator, and Next is disabled.
  • resolveBulkActions.test.ts's key-mapping case now asserts through resolvePromotedBulkParams.

Results:

  • Affected suites at ef07e4e: pnpm exec vitest run over 47 files (plugin-grid bulk suites, app-shell resolveActionParams* / paramToField* / ActionParamDialog* / ActionPreview* / the identity and twin pins, core's ratchet and ActionParamDef.options, and one-authority-per-exported-name-6273): Test Files 47 passed (47), Tests 510 passed (510). The commits after it change typing, comments, docs and the changeset. The rerun at the final head is in the os-dev-report on the card.
  • Ablation A2, resolver starved of metadata (resolveActionParams(raw, { ...ctx, objects: [] })): the three field-backed pins went red, and both CONTROLs, the refusal case and the authored-def case stayed green. The mutation was proven on disk (anchor 1 → 0, blob e2e6b01a → e770d6fb), and so was the restore (blob equal to HEAD, git diff HEAD empty).
  • Ablation A1, the ensureType('object') wait removed from ObjectGrid: only the grid-level picker pin went red (it got the refusal instead), 7 of 8 green. Restore proven the same way.
  • Reverse type check: with an unknown key added to the context ObjectGrid hands the resolver, pnpm --filter @object-ui/plugin-grid type-check reported TS2353 … does not exist in type 'ResolveActionParamsContext'. That proves plugin-grid reads the rebuilt fields .d.ts. Restored, git diff HEAD empty.
  • Type-check (every output echoes type-check): @object-ui/fields, @object-ui/plugin-grid, @object-ui/app-shell and @object-ui/core, all exit 0, after a turbo build of each closure. The plugin-grid and fields run was at d5e2f59; the app-shell and core run was on the same code.

Gates, all exit 0 at 5c915e9

  • Spec, deps and imports: check:spec-symbols, check:handler-key-reads, check:action-forward-parity, check:phantom-deps, check:unused-deps, check:self-import, check:esm-specifiers (specifiers-only, as the root script runs it), check:unreferenced-sources.
  • Line citations and bytes: check:new-line-citations (VERDICT new-cross-file-line-citations: 0 new citation(s)), check-control-bytes.
  • Changesets: check-changeset-presence ("30 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"), check-changeset-no-major, check-changeset-fixed, check:changeset-claims, check:pending-changeset-literals.
  • Tests and mocks: check:test-path-roots, the three check:vi-mock-*, check-type-check-coverage.
  • i18n: check:i18n-keys. It confirms that the reused actionDialog.unresolvedParam default matches the en pack. No locale copy changed, so the other i18n checks were not run.
  • Docs: check:readme-exports, check-doc-snippet-types, check-doc-example-types, check-doc-component-types, check:doc-example-readers, check:doc-fences, check-doc-example-ids, check-doc-links.
  • Dist and budget:
    • check:published-dist.
    • check:node-esm-load --force-build at 5c915e9: "37 of 37 gradable entries were built by this tree". A first run without --force-build was refused on provenance alone, because 6 untouched packages were replayed from a sibling worktree's shared turbo cache.
    • check:eager-closure at head: "Console eager closure is 3170.6 KB gzipped across 290 of 2474 chunks (budget: 3204.6 KB, headroom: 34.0 KB)". The base-to-head delta is in the os-dev-report.
  • Lint, narrowed:
    • Population: eslint.config.js applies its rules to **/*.{ts,tsx}. All 30 changed code files are in that population, and none came back as ignored.
    • Count: eslint --format json over those 30 files reported 30 results, 0 errors.
    • Invariance: the config enables no type-aware linting (no parserOptions.project or projectService), and no rule under eslint-rules/ reads the disk. So this diff cannot change a verdict on a file it did not touch.
    • With --no-inline-config the same run shows 4 errors. They sit exactly on the 4 pre-existing eslint-disable-next-line react-hooks/static-components comments in these files. Repo-wide pnpm lint is CI's.

Clause-② surface added

New exports from @object-ui/fields:

  • values: resolveActionParam, resolveActionParams, resolveParamWidgetType, withKnownObjects, RESOLVED_ONLY_PARAM_KEYS
  • types: RawActionParam, RawActionParamOption, ResolveActionParamsContext

Before this PR they were internal to app-shell (not exported from it). They are now exported from fields' package entry.

Nothing is added to plugin-grid's package entry. resolvePromotedBulkParams and unresolvedFieldOf are module exports of resolveBulkActions.ts, which the index does not re-export. No prop, context member or locale key is added. The bulk dialog reuses the existing actionDialog.unresolvedParam key.

Acceptance notes (noted, not filed)

  • The bulk adapter forwards only part of a lookup's picker config. A resolved lookup param reaches the bulk picker with only object, labelField and dependsOn. lookupFilters, lookupColumns, lookupPageSize, descriptionField, idField and titleFormat are resolved but not forwarded. bulkParamToField has no rename for them, while app-shell's paramToField does. Converging the two param adapters is its own change. The changeset and the docs say so. Carrier: whoever next converges bulkParamToField with paramToField.
  • Bulk param labels skip _actions.ACTION.params.PARAM localization. The record dialog runs that pass after resolution (actionParamText), and the bulk path never had it. This is a gap from before this PR, not something it introduces. Carrier: none.
  • The bulk dialog's dependsOn note is still stale elsewhere. It says BulkActionParamSchema "is not strict", but the spec has since closed that shape and declares dependsOn. Only the sentence this PR made false was edited. Carrier: none.
  • BULK_PARAM_TYPE_ALIASES in bulkParamToField.ts duplicates the param alias table. The table now lives in fields beside resolveParamWidgetType. Not touched, because it is a different defect class. Carrier: the same as the first note.

Generated by Claude Code

…ked param resolves through the shared action-param resolver (objectui#12105)

The selection bar's bulk dialog mapped a promoted action's params by hand
(`toBulkParam`): `field` became `name`, `type` defaulted to `text`, and the
picker target was read only off an inline `reference`. A field-backed param
(`{ field, objectOverride }`) therefore drew a raw text box and the confirm
step listed raw ids, while the record page resolved the same declaration
into the record picker through app-shell's `resolveActionParams`, which
plugin-grid cannot import.

The resolver moves, unchanged in behaviour, from app-shell to
`@object-ui/fields` (the one package both sides depend on that holds every
input it reads), together with its three-row param-type alias table.
app-shell imports it from there. plugin-grid resolves a promoted def's
params when its dialog opens (`resolvePromotedBulkParams`), after awaiting
the session's object metadata, exactly as the record dialog does, and maps
the resolved param onto the bulk vocabulary. A param whose backing field is
missing is refused in the bulk dialog as it is in the record dialog.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…solver, module-scoped (objectui#12105)

`@object-ui/fields` carries no node typings, so the two dev-only warnings
the resolver brought from app-shell failed `TS2591`. The read stays the
bundler-replaced `process.env.NODE_ENV` it was in app-shell; only its type
is declared, inside the module, so no ambient global can shadow node's in
a program that loads `@types/node`.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…nd the bulk path's field-backed params; changeset (objectui#12105)

The fields README gains a section on the resolver exports, the plugin-grid
and enhanced-actions guides say a field-backed param resolves on the
selection bar as on the record page, and four core comments that named
the resolver's old home now name `@object-ui/fields`.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
… the caller, as real use does (objectui#12105)

The snippet's inline field def taught `type: 'lookup'`, which the doc
component-type gate reads as a node type. The objects now come from the
caller and the session store, which is where they come from in use.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…resolve — picks up objectui#12117's ObjectGrid change (objectui#12105)

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3171.5 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-GIWo0qPP.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
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) 586.66KB 141.23KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 274.56KB 69.81KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.33KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 251.36KB 69.68KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ed8ea958e5300705289c8bc1961d6e247488ff30
Local-runs: none

Inputs read: card objectui#12105 (body, the Claim: and the os-dev-report comments), PR objectui#12119 (body, 34-file list, net diff against main at the head, the one thread comment), the 43 check-runs on the head, and file contents at the head, the base 206505c3 and objectstack main where a judgment needed them. Check-runs: 40 success, 3 skipped (dependabot, the two coverage-matrix rows that skip by design), 0 failure or pending; Governed Surface Queue Guard, Lint, Type Check, Test and all eight shards, Test (dist pins), Build & E2E, Spec Main Shape Gate and all five changeset gates green. The PR touches no governed surface, is not a fork PR, and changes 914 lines.

① Derived judgments

  • @object-ui/fields package entry widens by exactly eight names — right. Base-to-head diff of packages/fields/src/index.tsx adds one value block (resolveActionParam, resolveActionParams, resolveParamWidgetType, withKnownObjects, RESOLVED_ONLY_PARAM_KEYS) and one type block (RawActionParam, RawActionParamOption, ResolveActionParamsContext), nothing else. The PR body, the changeset and the fields README list the same eight. ResolveActionParamsContext is now a public contract (a later required member would be breaking); acceptable and now declared.
  • @object-ui/app-shell public face unchanged — right. Its src/index.ts at the base exports none of the moved names; package.json exports is . and ./styles.css only. Dropping resolveParamWidgetType and the three-row alias table from utils/paramToField.ts is module-internal; paramToField now reads the fold from fields, so the alias table has one home on the single-record side.
  • @object-ui/plugin-grid public face unchanged — right. src/index.tsx at the head re-exports none of resolveBulkActions, resolvePromotedBulkParams, unresolvedFieldOf, BulkActionDialog or bulkParamToField. The behavioural change to resolveBulkActions (a promoted def leaves the fold without params) is therefore internal. I read all 34 non-test plugin-grid sources: the only consumer of a promoted def's params is ObjectGrid, through dispatchBulkActionDef, which awaits resolveDialogParams before setActiveBulkDef; the dialog's step choice reads params.length on the def it receives, so the params step still opens. plugin-list's ListView hands schema.bulkActions to ObjectGrid as batchActions and never opens the dialog itself. No other consumer loses params.
  • Accept-set of a promoted action's params — right, with the exact bound. Field-backed { field, objectOverride } now resolves against the field definition (type, label, options, default, picker target, display field, dependsOn), which is the card. For inline params the new toBulkParam forwards a closed list: name, label, type, required, help, default, options, placeholder, multiple, object, labelField, dependsOn, accept, maxSize. Every key BulkActionDialog and bulkParamToField read is in that list. The old hand-mapper spread the raw param, and bulkParamToField spreads its ...extra onto the widget, so the keys that no longer reach a widget are visible, defaultFromRow, carryOver, field, objectOverride (and reference, which was and is renamed to object). The spec's ActionParamSchema is a strictObject of exactly name field objectOverride label type required options placeholder helpText defaultValue multiple accept maxSize reference defaultFromRow carryOver visible, so no other inline key can arrive; of the five dropped, the bulk dialog evaluates only the def-level visible (never a param-level one), and the other four are declaration or single-record keys with no bulk reading. The CONTROL pins (unit and through the real grid) hold for the keys that have a bulk meaning.
  • The unresolved-field refusal — right. A field-backed param whose field is not in ctx.objects comes back with unresolvedField set; BulkActionDialog renders a role="alert" refusal ahead of bulkParamToField (so the hand-off key never reaches the widget spread) and paramsValid returns false, disabling Next. That is Prime Directive [WIP] Enhance every detail of the designer #10 (absence must be loud) and parity with ActionParamDialog: same actionDialog.unresolvedParam key, present in the en pack under the top-level actionDialog block that useObjectTranslation serves; check:i18n-keys green. The key rides BulkActionParam's already-declared catch-all with a plugin-grid-local const and one reader (unresolvedFieldOf) in the module that writes it. objectui AGENTS.md #0.1 forbids putting a host-written key on the authoring face and prefers a package-local hand-off type; the reader-plus-const is the functional equivalent, no cast or added index signature. Accepted, noted.
  • Resolution moved from render to dispatch, and the metadata wait — right. useMetadata() from @object-ui/react returns the frozen NO_METADATA_PROVIDER fallback outside a provider (ensureType resolves to an empty list, objects empty), so a grid rendered without the console does not throw and a cross-object param is refused loudly rather than drawn as text. app-shell's real MetadataProvider builds its context value with get objects() reading the live cache (readType over getEntry), so the context value the dispatch closure holds sees the cache after await metadata.ensureType('object'); the dev's "getter, not snapshot" claim is verified in source, and the grid-level pin models the same late load. The .catch on ensureType swallows a metadata fetch failure, but the downstream outcome is the refusal, not a text box.
  • The fields source cycle — right, acceptable. resolveActionParams.ts imports resolveFormWidgetType from the barrel that re-exports it; the read is inside resolveParamWidgetType at call time and the target is a hoisted function declaration, so instantiation order cannot bite; the published entry is one bundle; Test (dist pins) and Build & E2E green.
  • Module-scoped declare const process in fields — right. Scoped to the module, shadows no global elsewhere; the runtime read is unchanged.
  • @object-ui/core — right. Three comment-only source edits renaming the resolver's home, one test comment, and the column-identity ratchet 12 to 11: the retired toBulkParam read p.name || p.field, the new one reads resolved.name only, so the dropped row is real.
  • Docs and spec quote — right. enhanced-actions.mdx, plugin-grid.mdx and the fields README keep the claim as narrow as the enforcement (the picker-config keys not yet forwarded are stated). The quoted guidance.field sentence matches objectstack main packages/spec/src/ui/bulk-action.zod.ts byte for byte, and the three sibling sentences named in the PR body are present there. Two further comments in that file still say toBulkParam maps the same three keys when promoting; that remains true of the rename adapter.

② Semver level

PR body: Clause-②: yes, no arm. The diff publishes a pure widening (eight new names on the @object-ui/fields entry) and no narrowing: nothing leaves any package entry, and the resolveBulkActions output change is internal to plugin-grid. .changeset/12105-bulk-param-field-resolve.md declares @object-ui/fields: minor, @object-ui/plugin-grid: patch, @object-ui/app-shell: patch; minor on the widened package satisfies the "yes takes at least minor" rule, and the fixed group in .changeset/config.json lifts every member to minor at version time; no major (Changeset Bump Policy green). @object-ui/core is touched by comments and tests only and is not named in the frontmatter; it publishes no behaviour, Changeset Declaration is green, and the fixed group bumps it regardless. The changeset body states the eight exports and the not-yet-forwarded picker settings, so the CHANGELOG text matches the diff. No migration section is owed. Level: right. Clause-②: yes — right, no arm owed.

③ Boundary flags

open_questions: none declared.

Deviations, each answered:

  1. File surface widened beyond the claim's named files — accepted. A git mv across packages repoints every importer: fourteen test import lines, two path-keyed pins (ActionPreview citation, core ratchet), four core comments, the fields README and two docs pages. Each is in the diff and does what the report says.
  2. Unresolved-param refusal added beyond the PM's route — accepted and required: once bulk calls the shared resolver, the missing-field case would otherwise fall back to the placeholder type and draw a text box, which is the defect under review.
  3. Module-scoped process type in fields — accepted (judged above).
  4. Resolution at dispatch time instead of in the fold — accepted (single consumer, fallback and live getter verified above).
  5. Model-free commit trailers instead of the harness reminder's model-named trailer — accepted: both repos' AGENTS.md require the model-free pair and the pre-push hook refuses a model identifier; the PR footer is the session-URL form prescribed for bodies.
  6. One mis-split test invocation, one lock timeout — process notes; the readings of record are the re-runs at ed8ea95 and the green check-runs on this head.
  7. origin/main merged mid-run (objectui#12117) — accepted; the head is that merge commit and every check-run on it is green.

Out-of-scope findings, each answered or escalated:

  1. A resolved lookup param forwards only object, labelField and dependsOn to the bulk picker; lookupFilters, lookupColumns, lookupPageSize, descriptionField, idField, titleFormat are resolved and dropped. Not a contract violation today: the spec's own BULK_PARAM_WIDGET_CONFIG_KEYS prescription says a key declared on the FIELD does not reach this dialog, and the PR narrows docs and changeset to say the same. It is, however, a reproducible divergence between the two dialogs for one declaration, and it becomes a contract question the moment the objectstack follow-up rewrites the four guidance sentences. Escalated to the seat: file it as a card, or fold it into the objectstack spec follow-up, rather than leaving the carrier as "whoever next converges the two adapters".
  2. BULK_PARAM_TYPE_ALIASES in bulkParamToField.ts twins the alias table now in fields — pre-existing, accepted as noted; same carrier as finding 1.
  3. Bulk param labels skip the _actions.ACTION.params.PARAM localization pass the record dialog applies — pre-existing and outside the card, but a declared translation that one dialog ignores is a reproducible gap. Escalated to the seat to decide whether to file (domain:ui, i18n); this record writes no card.
  4. Stale BulkActionParamSchema "is not strict" docblock sentence — accepted as noted; only the sentence this PR falsified was edited.

Card-level follow-up carried forward: the four bulk-action.zod.ts sentences in objectstack are quoted in the PR body as the card asked; the seat's next objectstack PR touching that file owes the correction.

Implemented-by: claude/issue-12105-bulk-param-field-resolve
Reviewed-by: session_01AswpQDLCKiZos2jCXknwKz

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 06:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 06:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 29b9949 Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12105-bulk-param-field-resolve branch October 11, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants