Skip to content

fix(app-shell): a position decision output lists the position registry and commits position names (objectui#7611) - #12152

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-7611-decision-output-position
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-7611-decision-output-position

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #7611
Clause-②: no

Item 3a of the C9 remainder census on #7611 (comment 6111307114), dispatched by the seat's answer 6111342697 as PR-2a. Item 3b, the spec-derived approver readers, waits on objectstack-ai/objectstack#22865 and is not in this PR, so #7611 remains open.

What changes

  • The typed table entry is rewritten. In decisionOutputParams, position leaves OUTPUT_LOOKUP_OBJECTS, which keeps department and team, and becomes the one entry of a new OUTPUT_REGISTRY_TYPES table: a catalog kind. Nothing else in the diff names position.
  • A catalog-kind output renders a select over the registry items the caller passes as registry. Each option's value is the item's machine name and its label is the item's label, else its name. Items without a name are dropped, and the list is sorted by name. This is the same reading RecipientPickerField's registry kinds make of the same registry (ADR-0131 D3/D7, objectui#12089).
  • readDecisionOutputRegistry reads those items through the console's metadata store, ensureType('position'), once per type. A failed read maps to an empty list. decisionOutputsReadRegistry says whether any output is a catalog kind.
  • DeclaredActionsBar is the one decision surface. The Approval Center and the record page both mount it. It reads the store raw from MetadataCtx. Only a decision with a catalog output awaits the registry read; every other decision still dispatches in the click's own tick.
  • Without a metadata store (a host outside a MetadataProvider), a position output degrades to the free-text box an untyped output gets. The recipient picker takes the same fallback. An empty registry is a different case: it renders the select with no options.
  • No new param key and no new widget. select is a declared param type, and resolveActionParams() copies its options through key by key.
  • A comment made false is fixed. The decision_output_defs docblock in apps/console's approvalsApi.ts said position values are record ids. It now says position commits machine names.

Measured in a browser

Setup:

  • The showcase was booted from objectstack origin/main 8532783f65 with objectstack dev --seed-admin --fresh. It ran in a detached worktree; the closure build was 60 of 60 tasks cached.
  • Two consoles ran under vite with DEV_PROXY_TARGET pointed at it: one from this branch, and one from the base a7d492011 for the before leg.
  • On the fresh database, GET /api/v1/data/sys_position answers "records":[] with "total":0, and GET /api/v1/meta/position serves 16 positions.

The pending approval:

  • A flow, pr2a_position_output, was saved at runtime with PUT /api/v1/meta/flow/pr2a_position_output. It has two approval steps.
  • Step one, lead_review, declares decisionOutputs: [{ key: 'parallel_positions', type: 'position', multiple: true, required: true }].
  • Step two, co_sign, has the approver { type: 'expression', value: 'vars.lead_review.parallel_positions', resolveAs: 'position' }.
  • Editing an announcement's body opened a pending request whose decision_output_defs carries the position output.

Before (base a7d492011). Checked on the Approval Center and on the record page:

  • The Approve dialog's Parallel Positions control is the lookup.
  • Opening it sends GET /api/v1/data/sys_position?top=50 and shows "No options found".
  • The output is required, so the approval cannot complete.

After (this branch). Checked on the Approval Center and on the record page:

  • Clicking Approve sends GET /api/v1/meta/position and sends nothing to sys_position.
  • The control offers all 16 positions by label.
  • Confirm with nothing picked is refused: "Parallel Positions is required".
  • Picking Auditor and Project Manager and confirming sends POST /api/v1/approvals/requests/ID/approve with the body {"outputs":{"parallel_positions":["auditor","manager"]}}.

The engine routes by those names:

  • The approved request's node_config_json stores __decisionOutputs: {"parallel_positions":["auditor","manager"]}.
  • The co_sign request opened with pending_approvers set to the users who hold auditor and manager in sys_user_position: the demo auditor and the admin.

The after leg ran at 87aa90858, then again at 4223b14c0 on a second fresh database. Both runs sent the same wire body and opened the same downstream slate. 6eb495c67 changes only when the bar awaits the read, and a position output still awaits it. The pins and the ablation table below cover that commit.

Not on the wire: the replaced lookup committed sys_position row ids (LookupField's default id field). That is read from code. A fresh database has no rows, so there was nothing to commit.

Tests (head 6eb495c67)

  • Related suites: 29 files, 308 tests passed. They are every test file under packages/app-shell and apps/console that names DeclaredActionsBar, a decision output, or ApprovalDecisionPanel. The list was enumerated by git grep -l and covers the Approval Center page, the record page's approval actions and the override-affordance suite.
  • pnpm --filter @object-ui/app-shell type-check: exit 0. That is tsc --noEmit plus tsc -p tsconfig.test.json. Run with --listFilesOnly, the test program lists both edited test files.
  • Gates, all exit 0:
    • check-changeset-presence (5 source files, 2 released packages, 1 changeset) and check-changeset-no-major;
    • check:changeset-claims, check:pending-changeset-literals;
    • check:new-line-citations (0 new) and check:control-bytes;
    • check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape and check:test-path-roots;
    • check:i18n-keys, check:shell-escape-residue and check:spec-symbols.
  • eslint, narrowed to the 5 changed source files: 0 errors.
    • Against the base, the only new warnings are 2 no-explicit-any, on the as any action fixtures the test file's other cases also use.
    • The narrowing proves itself in three parts. The 5 files are in the population: eslint returned a result for each, and none was reported as ignored. The count of 5 is read from --format json. The root eslint.config.js enables no type-aware linting (no parserOptions.project and no projectService), so this diff cannot move the verdict on any file it does not touch.

One-time ablations

Every leg ran through objectstack's scripts/ablation-replace.mjs in wrap mode. The anchor had to hit, the blob had to change, and the restore was proven: blob equal to HEAD, and an empty git diff HEAD. No dist was involved, because vitest aliases every @object-ui/* package to its src.

Mutation Run at Pins that went red
Drop position from OUTPUT_REGISTRY_TYPES 87aa90858, 6eb495c67 9, then 10: every registry pin, the predicate, the read-once and failed-read pins, the bar's registry pin, and the label, multiplicity and never-degrades pins
The bar passes no registry 87aa90858, 6eb495c67 the bar's "lists the position registry" pin
A failed read leaves the type absent 87aa90858 "answers {} without a reader, and [] for a read that fails"
No sort 87aa90858 the offers pin, the widget-values pin and the bar's registry pin
Label is always the name 87aa90858 the offers pin and the bar's registry pin
An absent registry reads as [] 87aa90858 "no registry reader degrades to the free-text box"
A second read per type 87aa90858 the read-once pin and the bar's registry pin
Read every typed output, not only catalog kinds 87aa90858 the read-once pin and "reads nothing when no output is a catalog kind"
Keep empty-name items 87aa90858, 4686fb7dd none at first, then 2 (see below)
Fold joins arrays 87aa90858 "folds the picked names into the decide body unchanged" and the existing fold pin
The bar awaits whenever a store is mounted 6eb495c67 "dispatches in the click's own tick when no output lists the registry"
The predicate answers true for any output 6eb495c67 the predicate pin and the click's-own-tick pin

Two legs did not measure on the first try:

  • The empty-name mutation stayed green. The fixture had no item whose name is an empty string. 4686fb7dd adds one, and the rerun went red on 2 pins.
  • The tool refused the first await-guard leg before any test ran. Its replacement text was already a prefix of the anchor, so the replacement count could not rise. The rerun used a distinct replacement and went red as listed.

Not measured here, declared for CI

  • The full app-shell package suite. A run at 4223b14c0 was stopped after about 11 minutes: it held the shared verification lock while another agent waited behind it. Before it stopped, it had reported one red file, DeclaredActionsBar.overrideAffordance (3 failed). That suite reads the dispatch synchronously after its click, so awaiting on every decision broke it. 6eb495c67 keeps the dispatch synchronous unless a catalog output exists, and the file is green in the 29-file run.
  • Full pnpm test, pnpm lint, and the console's type-check. The console change is a comment.

Acceptance notes

  • Audience positions in the list (read from code, not measured). The list includes the registry's built-in audience positions everyone and guest. The engine's expandPositionUsers reads sys_user_position rows plus the membership tier, so a position held only implicitly probably resolves to nobody, and a run that picked one would route into onEmptyApprovers. The sharing-rule recipient picker offers the same list. Successor: none.
  • Option order. Options are sorted by machine name, so the labels are not alphabetical: "Project Manager" sits between "Legal" and "Operations". This follows RecipientPickerField. Polish, noted only.
  • Chip group. A multiple output renders as a chip group (MultiSelectField), which was 16 chips on the showcase. Polish, noted only.

Changeset: .changeset/7611-decision-output-position-registry.md (@object-ui/app-shell and @object-ui/console, minor). Draft per AGENTS.md section 9: this PR comes from a dispatched claim.

Developer session session_01Rerax7QTjKMPCUZxQUtPFR, dispatched by the epic PM of objectstack-ai/objectstack#15194.


Generated by Claude Code

…y and commits position names (objectui#7611)

The Approve/Reject dialog rendered a position decision output as a
sys_position lookup. A fresh database holds no sys_position rows, so the
picker offered nothing, and a row it did offer committed a row id where the
spec (objectstack#22710) and the engine's resolveAs position read a machine
name.

decisionOutputParams moves position from the record-lookup table to a
catalog table, and renders it as a select over the registry items the
caller read through the console's metadata store (ensureType), the reader
RecipientPickerField lists the same registry through. DeclaredActionsBar
reads them at click time. No reader degrades to free text; an empty
registry is still a picker.

Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
Co-authored-by: Claude <noreply@anthropic.com>
…the name guard is pinned (objectui#7611)

The first ablation of the empty-name clause in registryOptions stayed
green: the fixture held no item whose name is an empty string.

Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
Co-authored-by: Claude <noreply@anthropic.com>
…t, not during render (objectui#7611)

Writing the ref during render raised react-hooks/refs on DeclaredActionsBar.
A click always lands after a commit, so an effect with no dependency list
keeps the ref current without keying on the store's context identity.

Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
Co-authored-by: Claude <noreply@anthropic.com>
…stry read (objectui#7611)

Awaiting the registry read on every approve/reject moved the dispatch out of
the click's tick, and DeclaredActionsBar.overrideAffordance's poll, which
reads the dispatch synchronously after its click, timed out three times. A
decision with no catalog output now dispatches synchronously, as before;
decisionOutputsReadRegistry is the one predicate the bar asks, and a store
with no catalog output to serve is never read.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 291 chunks) 3177.4 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-_2uJJmTq.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) 20.05KB 7.41KB
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) 587.77KB 141.53KB
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) 275.30KB 70.07KB
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.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.37KB 66.07KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.19KB 70.94KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.82KB 31.24KB
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
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 19:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 19:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 8cc7158 Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-7611-decision-output-position branch October 11, 2026 19:41
zhuangjianguo pushed a commit that referenced this pull request Oct 11, 2026
Brings main to faf4ce9 (objectui#12151, #12152, #12153, #12154, #12148,
#12149, #12147, #12158, #12159); none of them touches this branch's files.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants