Skip to content

fix(studio): object writers seeded from a served read drop a picklist-bound field's served options (objectui#11692) - #11706

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11692-writers-drop-served-options
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11692-writers-drop-served-options

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11692

Clause-②: yes

Object writers outside the two objectui#10202 designers seed their PUT from a served object read. The runtime serves a picklist-bound field with picklist and the options it resolved from the list, and the authoring door refuses that pair for the whole object (422 INVALID_METADATA at fields.FIELD.options). Each writer the card lists now either drops those served options before its PUT, or is shown here not to seed from a served read. A fifth writer of the same class, EmbeddedItemEditor, was found during the work. The seat added it to the claim's file surface (comment 6012529702), and it is fixed the same way.

Dispatched work: claim 6011332561 on objectui#11692, PM session https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju.

What changes

The helper moves to @object-ui/data-objectstack (new export, the reason for Clause-②: yes).

  • dropServedPicklistOptions moves from app-shell/src/views/metadata-admin/picklist-binding.ts to data-objectstack/src/picklist-binding.ts (a git mv, with its test) and is exported from the package barrel.
  • Why it moves: @object-ui/plugin-designer does not depend on @object-ui/app-shell, and app-shell lists plugin-designer as a peer, so the two Setup pages could not import it where it lived. Both packages already depend on data-objectstack. That package also owns the client that serves the read and carries the write, the same reason extractDraftBody lives there.
  • The two existing callers, builtinComponents.tsx (the metadata-admin object resource's fromDraft) and StudioDesignSurface.tsx, now import it from @object-ui/data-objectstack. No copy is left in app-shell, which never exported it.
  • Built declaration, read from packages/data-objectstack/dist/index.d.ts after turbo run build: declare function dropServedPicklistOptions with type parameter T extends Record of string to unknown, taking body: T and returning T. It appears in the barrel's export list beside extractDraftBody. The type parameter is spelled out in words here because GitHub strips angle brackets from PR bodies.
  • ⛔ It is not applied at a door. MetadataClient.save is unchanged, which leaves option B ruled out as triage directed. The barrel comment and the module docblock both say the export is for WRITERS. A door cannot tell a served copy from an authored pair, and the authored pair must stay the server's loud refusal.

Per writer (card "Done when"):

Writer Seeds from a served read? Now
PackageOwdOverviewPanel doSave yes: getDraft body, else layered.effective dropServedPicklistOptions(next) before client.save
MetadataFieldsPage handleFieldsChange yes: client.get('object'), and carryOver / carryPreservedField carry every per-field key back dropServedPicklistOptions(mergedObject). The page has no options editor, so a bound field's options can only be the served copy
MetadataObjectsPage handleObjectsChange yes: client.list('object') spread as the merge base dropServedPicklistOptions(merged)
EmbeddedItemEditor doSave (opened from an object's fields, indexes or validations) yes: it re-reads client.layered(parentType, parentName).effective, splices the edited item in, and PUTs the whole parent an object parent goes through dropServedPicklistOptions(updated). Other parent types are sent exactly as before
MetadataService.saveFields yes: client.meta.getItem('object') and the per-field carry-over dropServedPicklistOptions(updatedObject). DesignerFieldDefinition declares no picklist, so this method can neither author nor remove a binding
MetadataService.saveObject no: it reads nothing, and builds from the caller's obj and existingFields unchanged, with the reason in its docblock
MetadataService.saveMetadataItem no: it reads nothing, and data is the caller's unchanged, with the reason in its docblock

The list read resolving the binding is a source reading of objectstack, not a live measurement. objectstack's metadata-protocol foldObjectExtendersFromRegistry docblock says the list read "reads registry.listItems, whose object branch resolves through the same fold", and SchemaRegistry.foldExtendersOntoDefinition ends in resolvePicklistFields.

Triage's correction stands: runtime-metadata-persistence.ts saves no object, so it is untouched.

Pins (a 422-refusing transport, as objectui#10202's pins are)

Each new test drives a real MetadataClient, or for MetadataService a real ObjectStackAdapter and SDK. The transport double answers a PUT whose fields carry picklist beside options with the door's 422 INVALID_METADATA envelope, so every claim is read off the wire:

  • PackageOwdOverviewPanel.picklistServedOptions-11692.test.tsx: an OWD change on an object with a bound field, seeded from layered.effective and from a served draft. The door accepts the body, the bound field goes out as picklist only, the inline select and the text field go out byte-identical, and the OWD edit lands.
  • MetadataFieldsPage.picklistServedOptions-11692.test.tsx: relabel an unrelated field. Both the designable bound select and a carried-through bound multiselect leave without options, and the inline select keeps its options.
  • MetadataObjectsPage.picklistServedOptions-11692.test.tsx: relabel the object. Same assertions.
  • EmbeddedItemEditor.picklistServedOptions-11692.test.tsx: the editor is mounted over a served object with a bound tier. Two cases: saving another item (amount), and an untouched save of the bound field itself. In both, the door accepts the parent, tier goes out as picklist only, and the inline select goes out byte-identical.
  • MetadataService.picklistServedOptions-11692.test.ts:
    • saveFields with a field list built from the served read is accepted, and the bound field has no options.
    • saveObject and saveMetadataItem issue no read, and an authored pair still reaches the door and is refused, asserted on the SDK error's code: 'INVALID_METADATA' and httpStatus: 422.
  • The moved picklist-binding.test.ts is unchanged apart from its header. It still reads the contract off the installed @objectstack/spec.

Ablation. Each fix was committed first (51bb8bb for the four writers, 32b8c2e for EmbeddedItemEditor). Each writer's helper call was then removed through objectstack's scripts/ablation-replace.mjs, which wraps the run, verifies that the mutation landed on disk, and restores from HEAD. Every pin went red for the right reason, and every restore was proven by blob hash equal to HEAD and an empty git diff HEAD.

Mutation Result Failure message
OWD dropServedPicklistOptions(next) to next 2 failed of 2 refused at fields.tier.options: expected 422 to be 200
fields page dropServedPicklistOptions(mergedObject) to mergedObject 1 failed of 1 refused at fields.tier.options, fields.regions.options
objects page dropServedPicklistOptions(merged) to merged 1 failed of 1 refused at fields.tier.options
saveFields dropServedPicklistOptions(updatedObject) to updatedObject 1 failed, 2 passed of 3 the SDK's failed spec validation throw
EmbeddedItemEditor dropServedPicklistOptions(updated) to updated (at 32b8c2e) 2 failed of 2 refused at fields.tier.options: expected 422 to be 200

The saveObject and saveMetadataItem pins stay green under that last mutation, as they should. The first attempt at the last three ablations was REFUSED by the tool and ran no test, because the replacement was a substring of the anchor. They were re-anchored on the whole save(...) line and re-run. The table shows the re-run.

No dist build sits in the ablation path. The writers are imported from src by relative path, and the root vitest config aliases @object-ui/data-objectstack to its src.

Gates

Patch round (fifth writer), at branch head 32b8c2e. This head includes the merge of main at 3409fe8, which is a merge commit, not a rebase.

Gate Exit Verdict line
turbo run build --filter=@object-ui/app-shell^... --concurrency=2 0 Tasks: 28 successful, 28 total
pnpm --filter @object-ui/app-shell run type-check 0
pnpm --filter @object-ui/app-shell run lint 0 0 errors. The warning count is unchanged from bc90d2f, and the new test has no hit
vitest run of the 6 pin files plus the 3 existing EmbeddedItemEditor suites 0 Test Files 9 passed (9), Tests 39 passed (39)
check:new-line-citations / check:control-bytes / check:changeset-claims / check:pending-changeset-literals / check:metadata-write-doors all 0 0 new citation(s) and 3 can carry an object document, 3 reach assertObjectMetadataWritable
check-changeset-no-major / check-changeset-presence / check:self-import / check:phantom-deps / check-vi-mock-{specifiers,inherit,override-shape} / check:test-path-roots all 0 15 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)

Round 1, at branch head bc90d2f. These readings were not re-taken in the patch round. All at bc90d2f unless stated. Heavy runs went through os-verify-lock.sh. Every exit code was captured before any pipe.

Gate Exit Verdict line
turbo run build --filter=@object-ui/app-shell^... --filter=@object-ui/plugin-designer^... --concurrency=2 (at 51bb8bb, whose src is identical to bc90d2f) 0 Tasks: 28 successful, 28 total
pnpm --filter @object-ui/data-objectstack run type-check 0
pnpm --filter @object-ui/plugin-designer run type-check (tsc --noEmit && tsc -p tsconfig.test.json) 0 the new tests are program inputs (--listFiles)
pnpm --filter @object-ui/app-shell run type-check (same two legs) 0 the new tests are program inputs (--listFiles)
pnpm --filter @object-ui/{data-objectstack,plugin-designer,app-shell} run lint 0 / 0 / 0 0 errors. No warning on a line this PR adds
vitest run of the 5 new or moved test files 0 Test Files 5 passed (5), Tests 13 passed (13)
vitest run packages/data-objectstack/ packages/plugin-designer/ plus 126 app-shell files (see below) 0 Test Files 247 passed (247), Tests 2342 passed (2342)
check:self-import / check:phantom-deps / check:unused-deps 0 / 0 / 0
check:readme-exports 1, a precondition (see below) the new import is judged real (--list)
README snippet, compiled --strict against the built dist 0 negative control (a misspelt name): TS2724, exit 2
check:new-line-citations 0 0 new citation(s)
check:control-bytes 0 OK
check:metadata-write-doors 0 3 can carry an object document, 3 reach assertObjectMetadataWritable
check:changeset-claims / check:pending-changeset-literals 0 / 0 No pending changeset names a file this change touches.
check-changeset-no-major / check-changeset-fixed / check-changeset-presence 0 / 0 / 0 13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)
check:unreferenced-sources / check:test-path-roots / check-vi-mock-{specifiers,inherit,override-shape} / check:esm-specifiers / check:doc-fences / check:designer-field-key-parity / check-type-check-coverage / check-lint-coverage / check:spec-symbols / check:component-surface-parity (report-only) all 0

Notes on the table:

  • Narrowed test run, declared. app-shell has 1025 test files. A full run of all three packages was started and held the shared lock for 29 minutes without finishing, with other seats queued behind it, so I stopped it (my own PID). I re-ran a narrowed set instead: all of data-objectstack and plugin-designer, and in app-shell every test under src/services/ and src/views/studio-design/, every test that names StudioDesignSurface, builtinComponents, PackageOwdOverviewPanel, MetadataService, picklist-binding, dropServedPicklistOptions or register-builtins, and the objectui#10202 suites. That set is derived from direct importers. It is blind to a test that reaches a touched module through a third module, so the full app-shell suite is left to CI. app-shell's barrel surface is byte-unchanged. data-objectstack gains one export and loses none.
  • check:readme-exports exit 1 is not a measurement of this change. It is the gate's "type entry not on disk" refusal for 8 packages outside this build's closure (plugin-gantt, plugin-map, plugin-markdown, plugin-timeline and others). With --list it exits 2 (PRECONDITION NOT MET) and judges packages/data-objectstack/README.md's dropServedPicklistOptions import real.
  • NOT MEASURED: check:doc-snippets, reason: its build closure is every package. The one new README block was compiled --strict against the built data-objectstack dist instead, with the negative control shown in the table.
  • NOT MEASURED: check:eager-closure, reason: it needs a full console build. See Acceptance notes.

Acceptance notes

  • The fifth writer, EmbeddedItemEditor, is fixed here after the claim amendment (comment 6012529702). Its wrap covers the edited item too, the same rule the four writers and the Studio data page apply: every field that names a picklist leaves without options. If the drawer's generic field form offers options on a bound field, an edit made there is dropped while the binding stays, because the editor cannot tell the served copy from an edit. Unbinding a field stays the job of the select-field editor's "Options from" picker (objectui#10202).
  • MetadataService has no in-repo caller of saveObject, saveFields or saveMetadataItem. It is public API through useMetadataService. The saveFields fix and the two pins of "no read, the pair still refused" are therefore measured on the method, not on a screen.
  • check:eager-closure was not run locally (it needs a full console build). The helper moved into a package that apps/console/src/dataSource.ts already imports statically, so no new module joins the eager graph. That claim is read from source, not measured.

Generated by Claude Code

claude added 2 commits October 6, 2026 07:25
…-bound field's served options

Move `dropServedPicklistOptions` from app-shell to `@object-ui/data-objectstack`
(a new export) so the plugin-designer writers can share it, re-point app-shell's
two callers, and apply it at the writers that seed an object PUT from a served
read: the OWD overview, the Setup fields and objects pages, and
`MetadataService.saveFields`. `saveObject` and `saveMetadataItem` read nothing
and are left as they are, so an authored picklist + options pair stays the
server's loud 422.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
… for objectui#11692

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3322.4 KB 3330.4 KB
Main entry chunk (gzip) 152.7 KB 350 KB
Entry file index-BAjpzmik.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
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) 40.70KB 10.94KB
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) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
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) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 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) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.83KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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.08KB 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.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

claude added 2 commits October 6, 2026 08:45
…'s served options before the parent PUT

`EmbeddedItemEditor.doSave` re-reads the parent object's served
`layered.effective`, splices the edited item in and PUTs the whole parent,
so an object with a picklist-bound field was refused (422 at
`fields.FIELD.options`) whatever item was saved. The object parent now goes
through `dropServedPicklistOptions` before the PUT; other parent types are
sent as before. Pinned with a 422-refusing transport; changeset prose names
the fifth writer.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3322.4 KB 3330.4 KB
Main entry chunk (gzip) 152.7 KB 350 KB
Entry file index-2XokqdHp.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
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) 40.70KB 10.94KB
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) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
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) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 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) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.83KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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.08KB 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.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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: 32b8c2ee4a3471f58c9b2313415c8a1827e5b7d8
Local-runs: none

Inputs: card objectui#11692 (body and all six comments, 6011093735 through 6012902044), PR objectui#11706 (body, 17-file list, the API net diff against main 3409fe8), the check-runs on the head, and git-object reads of the head and base shas in the objectui clone (no checkout, no build, no test). ⚠ FETCH_HEAD in that clone was moved to main (daa7caf) by another process mid-review; every reading below was re-taken pinned to the sha.

① Derived judgments

Public surface.

  • @object-ui/data-objectstack gains one named export, dropServedPicklistOptions, with a type parameter T extending Record of string to unknown, taking body: T and returning T. src/index.ts :7145 is the tsup entry (entry: ['src/index.ts'], exports["."].types = dist/index.d.ts), so the name reaches the built declaration and reaches no new type. The function body is byte-identical to the base's app-shell copy: both hunks of the renamed file are docblock-only. The colocated picklist-binding.test.ts follows the package's own precedent (78 src/*.test.ts files) and is outside the tsup entry. Right: a widening.
  • @object-ui/app-shell loses views/metadata-admin/picklist-binding.ts and its test. At the base the module had no barrel line (src/index.ts, and the package exports only . and ./styles.css); its only importers were builtinComponents.tsx :28 and StudioDesignSurface.tsx :99, both re-pointed, and the old path is absent at the head sha. Not a surface change. Right.
  • MetadataClient.save and every door: metadata-client.ts is byte-identical base to head; MetadataService.saveObject and saveMetadataItem change only in docblocks, and their pins show no read issued and an authored picklist + options pair reaching the SDK door and refused INVALID_METADATA / 422. Triage's ⛔ holds: no door strips.

Published write behaviour (each writer, read at the head sha).

  • PackageOwdOverviewPanel.doSave :250-271: next is the pending draft body or layered.effective, and only sharingModel / externalSharingModel are touched before dropServedPicklistOptions(next). The pair in the body can only be the served copy. Right.
  • MetadataObjectsPage.handleObjectsChange :227-262 (exported from plugin-designer src/index.tsx :77): merged spreads the served client.list('object') row and rewrites only object-level keys; fields is never edited. Served copy only. Right.
  • MetadataFieldsPage.handleFieldsChange :943-961 (exported, index.tsx :79): fromDesignerField :450-468 writes no options key at all, the served entry rides in only through carryOver(prev) :329 (and carryPreservedField :373 for the carried half), and FieldDesigner's own docblock :131-134 says "this drawer has no options editor". The PR comment "this page has no options editor" is a true reading. Served copy only. Right.
  • MetadataService.saveFields :931-980: MetadataService is reachable from app-shell's built entry through useMetadataService(): MetadataService | null (src/index.ts :232), so this PUT is published behaviour. DesignerFieldDefinition (packages/types/src/designer.ts :1176) declares options but no picklist, and toFieldPayload :611 spreads carryOver(prev) first, so a binding on the wire can only be the served document's. No previously accepted PUT changes: a bound field with options set was always refused, and with options unset the wire body is identical (JSON drops undefined). A caller's options beside a served binding now leaves as the binding alone, which is the door's own prescription ("Keep picklist and delete options") for a pair the caller has no way to unbind. Right; a fix, not a narrowing.
  • EmbeddedItemEditor.doSave :112-132 (mounted by MetadataDetailDrawer :123 from the object anchors fields / indexes / validations, anchors.ts :42-80): the strip is gated on parentType === 'object', right, because the served-picklist shape is an object read, and every other parent type goes out byte-identical. The strip runs after spliceEmbedded, so it covers the edited item. This is the ONE writer where an author's own keys sit inside the stripped body: the drawer renders the generic SchemaForm from the server-published field schema and does not reach ObjectFieldInspector.tsx, the only host of objectui#10202's "Options from" picker (:2236-2241). So an options value typed in that drawer on a field carrying picklist, or a picklist typed onto a field holding inline options, now leaves as picklist alone instead of the loud 422. Whether the server's field schema renders those controls is not readable from this repo. The dev named this (patch-round deviation 3, PR Acceptance notes), the seat accepted it on 6012902044 as the objectui#10202 rule, and builtinComponents.tsx :171 fromDraft: dropServedPicklistOptions already applies that rule on main to the whole metadata-admin object draft. Right as the card's rule; the residual is writer-side and stated, and the server's refusal is intact for every body that reaches it with the pair.
  • StudioDesignSurface.tsx :26 / :3729 / :3797 and builtinComponents.tsx :31 / :171: import re-point only, call sites unchanged. Right.
  • Triage's correction honoured: runtime-metadata-persistence.ts :37 RuntimeArtifactType = 'view' | 'report' | 'dashboard' | 'page', untouched. Right.

Pins against triage's pin sentence ("each listed writer saves a picklist-bound object it read and did not touch, and the server receives no options on the bound field"): every new file drives a real MetadataClient (or the real adapter and SDK) over a transport that answers 422 to a body carrying both keys, and asserts the accepted status, the bound field as picklist only, the inline select byte-identical, and the edit landing. OWD (2), fields page (1, a bound select and a carried bound multiselect), objects page (1), embedded editor (2, another item and the untouched bound field itself), saveFields (1) plus the two no-read refusal pins. The moved helper test still reads the contract off the installed @objectstack/spec. Matches.

README (packages/data-objectstack/README.md :873-902): the snippet's client.get with a Record type argument and 3-arg client.save match get :965 (returns a Promise of T or null) and save :1048; CI README Export Check and Doc Snippet Type Check concluded success.

② Semver level

.changeset/11692-writers-drop-served-options.md: @object-ui/data-objectstack: minor, @object-ui/app-shell: patch, @object-ui/plugin-designer: patch; no major. All three sit in the one fixed group of .changeset/config.json, so the release lifts together to minor, which is what the new export earns. Prose checked against the diff sentence by sentence: the five writers named are the five call sites; "a field without picklist keeps its inline options unchanged, and no other key is touched" is the helper's by-reference pins; "it was not exported there" is the base grep; "MetadataClient.save does not apply it, and neither do saveObject and saveMetadataItem, which read nothing" is the unchanged client and the two no-read pins. CI: Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read all success.

Clause-②: yes

One widening (the new data-objectstack export), no narrowing: no public name removed, no previously accepted write refused, no type narrowed. The claim's Clause-②: yes (6011332561) and the PR's line agree.

③ Boundary flags

  • open_questions: [] in both dev reports (6012485118, 6012839770). None to answer.
  • Round-1 deviation, narrowed local test run (247 files, blind to transitive importers): the measurement is CI's eight test shards; see the unconcluded runs below.
  • Round-1 deviation, packages/data-objectstack/README.md outside the claim's literal surface: it is the package doc of the moved export; the seat's ACCEPT counted all 17 files inside the amended surface, and README Export Check concluded success. Answered.
  • Round-1 deviation, three ablations first refused as no-ops then re-anchored: reported, only the re-run counted, and the five red-then-restored legs match the five call sites. Answered.
  • Round-1 out-of-scope finding, EmbeddedItemEditor: absorbed by claim amendment 6012529702 and fixed in the patch round. Closed. The second (no in-repo caller of MetadataService's three saves) is recorded in the PR's Acceptance notes, not a card. Answered.
  • Patch-round deviation, the merge commit amended once before its first push: parents unchanged, never on the remote, fast-forward. No action.
  • Patch-round deviation, the object-only gate: right, judged in ①.
  • Patch-round deviation, the drawer trade-off: judged in ① and accepted by the seat on 6012902044. Escalated as a note, not a blocker: if the server-published field schema renders an options control in the drawer, an author's options edit on a bound field vanishes without a message; the remedy is drawer-side (a guard, or a read-only options control while picklist is set), a follow-up card, not this PR.
  • Patch-round deviation, pr_body_patch: applied by the seat (ACCEPT reads back 14468 bytes). Answered.
  • Attribution deviations in both rounds: not a contract matter.
  • NOT MEASURED locally, check:doc-snippets: CI Doc Snippet Type Check success. check:eager-closure: the dev's source reading holds (apps/console/src/dataSource.ts :14 / :22 / :28 import @object-ui/data-objectstack statically at the head sha), and CI Pre-Install Import Graph Check, Bundle Analysis, Build & E2E success. check:readme-exports precondition: CI README Export Check success.
  • Check-runs on the head at this write: 42 total, 37 success, 3 skipped (dependabot, Test coverage, Test coverage shard, none a failure), 2 not yet concluded: Test (shard 1/8) and Test (shard 4/8). Type Check, Lint, Spec Main Shape Gate and the other six shards concluded success. Their convergence is the seat's landing check.

Implemented-by: claude/issue-11692-writers-drop-served-options
Reviewed-by: session_01FngvPpdrnhHMdHHq6vwwju

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 09:12
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 09:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit da35453 Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11692-writers-drop-served-options branch October 6, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants