Skip to content

feat(components): action:button delivers undoable where a record is in scope, and publishes it (objectui#11168) - #11954

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11168-button-undoable
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11168-button-undoable

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11168

Clause-②: yes — it widens, and narrows nothing. Widening: undoable becomes a published input of action:button (the page validator stops reporting it as an unknown prop), and an undoable operation: update of the record in scope now offers Undo where it offered none. Nothing that was accepted before is refused.

This is the last slice of objectui#11168: action:button.undoable, ruling B on objectui#11754 (record 6030342264, pointer 6030356700 on the card). With it the card's parity-ledger entries are all struck and every cap is 0.

The ruling, as executed

B. The button block delivers undoable where a record is in scope.

When the block runs inside a record context, it now hands the runner that record as the Undo baseline the runner's operation: 'update' path already reads (params._rowRecord, read by ActionRunner.executeUpdateOperation through captureUpdateUndoData). The key is published on action:button, and its description states the one limit: a button with no record in scope offers no Undo, because there is no row to restore. The runner is untouched.

The record in scope is the row the host binds to the node through data (a table's row, DetailView's header actions, an action:bar member), else the record page's own record from RecordContext. Both carriers are needed. Measured below: an authored record page renders the node through SchemaRenderer with no data, and its record lives only in RecordContext.

When it is attached. All three must hold; otherwise the dispatched def is exactly what it was:

  • the action declares undoable and operation: 'update' (the path the ruling names). An api action is not touched, because the console's api handler reads the stash for more than Undo: it fills {field} URL tokens and seeds recordIdParam.
  • a record is in scope.
  • the update writes that record. The id the dispatch resolves (an explicit recordId, else the record's recordIdField, id by default) must equal the record's id, because the runner keys the Undo by recordId ?? record.id. Otherwise a button writing another record would get the scoped record's values as its Undo, and restoring the wrong values is worse than no Undo.

Census first (the ruling's "unmeasured gap", H3)

Stored pages that declare undoable on action:button: 0.

The instrument is git grep read at a named ref, so the file list and the content come from the same source. It counts files that name action:button, then the subset that also names undoable, and inspects each hit. Lit controls run on the same population: actionType and confirmText on objectui, and on objectstack, which stores no action:button node at all, page:header, successMessage and operation: 'update'.

tree ref files naming action:button of them naming undoable lit control
objectui examples/ apps/ content/ + fixtures 19a7348 (this branch's base) 23 1 (the parity guard itself, not a page) actionType 6 files, confirmText 3
objectstack examples/ packages/apps/ f4bed58 (remote main) 2 (both comments, no node) 0 page:header 9, successMessage 6, operation: 'update' 3
objectstack, same trees 3ae5966 (the shared checkout) 2 0 same

undoable anywhere in the objectstack trees: 0. Reach: tracked files in those trees at those refs. It does not see pages stored in deployed databases, the cloud repo, or other objectstack packages. For context only, outside the ruling's trees, objectstack main names undoable in spec sources, platform-object metadata-form translations, docs and one skill rule, and none of those is an action:button page node.

Measured before editing (H1, H2), predictions written first

Throwaway probe, deleted before the first commit. It used the real SchemaRenderer, the real ActionRunner (ActionProvider) and the real createServerActionHandler as the script dispatch, with record { id: 't1', status: 'open' } and an undoable update writing status: 'done'. Every prediction held:

arm prediction observed
record page (RecordContextProvider, no data) no Undo success toast, no undo; body {"params":{"status":"done"}}
row via data no Undo same
LIT: same def, _rowRecord hand-stashed Undo toast undo set; undoData {status:'open'}, recordId t1
standalone no Undo, no error same
record page, visible: record.status == 'open' button hidden (the block binds only data) hidden, with an ungated control shown

H1 held. The runner reads the baseline from params._rowRecord and offers Undo only when action.undoable and rowRecord are set and the written-field list is non-empty, and rowCarries needs every written field on the record. The block forwarded undoable and wrote no stash. The existing writers' spelling (params: { ...values, _rowRecord: record }, as in page:header and DeclaredActionsBar) is the one reused.

H2 held as stated, with one refinement. On a record page, the record the block can see is RecordContext's, not a data prop, so the block reads both.

Pins: action-button-undoable-11168.test.tsx (12 rows, real pieces end to end)

  • Offers Undo with the prior values:
    • on a record page, from the page record (the write addresses t1, and the stash is stripped from the POST);
    • on a row via data;
    • the row outranks the page record around it;
    • an action:bar member on a record page;
    • a collected param value is restored too.
  • The one limit:
    • standalone gets a success toast, no Undo and no error;
    • an explicit recordId naming another record gets no Undo and no stash (lit control: ${record.id} does get Undo);
    • a record that does not carry the written field gets no Undo, which is the runner's existing rule.
  • Only an undoable update carries the record:
    • a non-undoable update carries none (lit control: the same node made undoable);
    • an undoable non-update carries none.
  • Published: a boolean input that the installed spec row declares, and the page validator no longer reports it as unknown-prop (lit control: an unpublished key still is). undoable also joins action:button's DECLARED row in the slice-1 pin file.

Ablations ran on committed HEAD through objectstack's scripts/ablation-replace.mjs (the anchor must hit, and the blob is verified). Each was wrapped by a trap that restores from HEAD on the absolute path. Predictions were written before the run. The subject is imported by relative path and source alias, so no dist sits between the mutation and the assertion.

leg mutation predicted observed restore
A delivery line if (!schema.undoable …) return values; → return values; exactly 8 of 12 red 8 red, 4 green, the predicted rows; anchor x1→x0, blob 354e1a84dc5b→1b0e1b3be4c4 blob == HEAD, git diff HEAD empty
B same-record guard line removed exactly 1 red (the other-record row) 1 red, 11 green; blob →e1bd2de0c963 same
C input renamed undoable_ablated pin 2 + slice-1 1 + guard 2, the guard's arm judge uncertain 5 red: pin 2, slice-1 1, and the guard's action:button publishes every top-level key … and … declares no top-level input the spec does not accept; the arm judge stayed green same; tree status empty

Ledger (registry-inputs-spec-parity.test.ts)

  • The action:button.undoable owed entry is struck.
  • OBJECTUI_11111_LEDGER_CAPS.unpublishedKeys goes 1 → 0, and the per-owner count objectui#11168 goes 1 → 0. All five caps and all five owners now read 0.
  • The two helpers that only wrote booked entries (OWED_TO, owedEntries) left with the last entry. The console project's noUnusedLocals would refuse them unused.
  • The cap test still counts the OWED TO prefix against caps of 0, so anything booked again goes red.
  • Retiring the whole OBJECTUI_11111_* scaffold is not done here (see Acceptance notes).

File surface, and the amendment it needed

As claimed:

  • action-button.tsx and its tests;
  • the guard;
  • one changeset (.changeset/11168-button-undoable.md: components minor, types patch);
  • the docs paragraph in content/docs/guide/layout.md that lists what action:button forwards.

Amended (the seat may record it): three texts this change made false are corrected, and nothing else in them moves.

  • packages/types/src/ui-action.ts, the UIActionSchema.undoable doc comment. It ships in the published .d.ts and said action:button never seeds the row guard. Comment only, no type change.
  • scripts/check-action-forward-parity.mjs, the JUSTIFIED reasoning. It said _rowRecord is written only by the spread-based hosts. Every verdict still holds: action:button:recordIdParam stays unreachable, because the update path dispatches to script, never to the api handler. Its suite ran, below.
  • action-forward-parity.test.tsx's header, the same sentence.

Verification (final HEAD 9993fb6e)

  • Tests, from the worktree root under objectstack's os-verify-lock.sh, each log's first line 9993fb6e:

    • the whole packages/components/ and packages/types/ suites, plus every outside reader of a touched file. The readers are useConsoleActionRuntime.paramDialogTitle, one-authority-per-exported-name-6273, both check-action-forward-parity script suites, check-i18n-en-drift, the two closing-keyword suites, zod-wrapper-keys.shared, the parity guard, public-block-binding-reach and public-contract. Result: Test Files 739 passed | 1 skipped (740) / Tests 13819 passed | 24 skipped (13843), VERDICT command-exit 0.
    • the dispatch's named union (packages/components/src/renderers/action/ + the guard + the forward-parity script suites + action-group.test.tsx): Test Files 37 passed (37) / Tests 876 passed (876).
  • Builds (under objectstack's os-verify-lock.sh): pnpm --filter '@object-ui/console^...' run build VERDICT command-exit 0; @object-ui/cli, @object-ui/plugin-ai, @object-ui/console build VERDICT command-exit 0.

  • type-check, script echoed: components tsc --noEmit && tsc -p tsconfig.test.json exit 0; types tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json exit 0; console tsc --noEmit && tsc -b tsconfig.node.json --force exit 0.

  • Gates, exit read after a redirect: check-changeset-presence 0 ("6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"), check-changeset-no-major 0, check:changeset-claims 0, check:pending-changeset-literals 0, check:new-line-citations 0 ("0 new citation(s)"), check:control-bytes 0, check:action-forward-parity 0, check:spec-symbols 0, check:component-surface-parity 0 (report-only; no action:button.undoable row), check:sdui-registration-pins 0 ("All 14 registration(s) … present"), check:skill-examples 0, check:doc-snippets 0 ("784 of 784 block(s) judged, 0 failed"), check:doc-examples 0, check:doc-fences 0, check:doc-types 0, check:test-path-roots 0, check:handler-key-reads 0, check:doc-example-readers 0, check:docs-route-closure 0, check:unreferenced-sources 0, check-governed-queue-guard --test on the 9 paths: NOT GOVERNED.

    • First runs of check:skill-examples and check:doc-snippets / check:doc-examples exited 2 on their stated build prerequisite. They were re-run green after the builds above.
  • Lint (a declared narrowing; pnpm lint is CI's):

    • Population: each package's lint is eslint . under the root eslint.config.js. --print-config gives 116–119 rules on each touched TS file and 0 on scripts/*.mjs, which no package lints.
    • Count: 6 files from --format json, 0 errors, 22 warnings, 0 of them on an added line (intersected with git diff -U0).
    • Invariance: eslint.config.js has 0 parserOptions, 0 project and 0 projectService (controls: rules 13, typescript-eslint 7), so linting is not type-aware and this diff cannot move a verdict on an untouched file.

Acceptance notes (observed, not filed)

  • The block's own visible / disabled predicates do not see the record page's record. Measured in the probe: a node under RecordContextProvider with visible: record.status == 'open' renders hidden, because usePredicateRecordContext(data) binds only the data prop and the console's predicate scope carries no record. SchemaRenderer's node-level gate binds RecordContext and passes, then the renderer's own fail-closed re-evaluation hides the button. This PR does not change predicate binding; it reads RecordContext for the Undo baseline only. Reach through a public door was not measured, and no stored page with such a node was found (census above). Carrier: none.
  • DeclaredActionsBar's docblock still says the block does not inject the record "which the api handler needs". That stays true for api actions, the subject of the sentence, so it is left as is.
  • The runner keys Undo by recordId ?? record.id while the route resolves record[recordIdField]. This PR avoids the mismatch at the block, by attaching only when the two agree. The runner-side asymmetry predates it and also applies to list-row writers. Noted, not touched (the runner is outside the claim).
  • The OBJECTUI_11111_* ledger scaffold (owners, bookings, the cap test) stays with every figure at 0, the convention earlier landings kept so that nothing re-books silently. Retiring it is the seat's call.

Session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw (domain:spec seat 1, mode subagent).


Generated by Claude Code

claude added 2 commits October 8, 2026 09:10
…n scope as its Undo baseline, and publishes undoable

Ruling B on objectui#11754 (record 6030342264): where the block runs inside a
record context it passes that record as the Undo baseline the runner's
`operation: 'update'` path already reads (`params._rowRecord`, the spelling the
record page's header, the declared-actions bar, the related-record bridge and
the grid's rows use). The record in scope is the row the host binds through
`data`, else the record page's RecordContext record. It is attached only for an
`undoable` `operation: 'update'` that writes that record, so a non-undoable
action, a non-update action and an update addressed to another record dispatch
exactly as before.

`undoable` is published on `action:button` with a description that states the
one limit (no record in scope, no Undo), and the parity guard's last
objectui#11168 entry is struck: unpublishedKeys cap and the owner count go to 0.
Prose the change made false is corrected: the UIActionSchema.undoable doc
comment, the forward-parity gate's JUSTIFIED reasoning and the forward-parity
test header.

Claude-Session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw
Co-authored-by: Claude <noreply@anthropic.com>
The layout guide's action:button paragraph says an `undoable` update offers
Undo from the record in scope, and when it does not. The changeset declares the
components minor (a published input, a behaviour change) and the types doc
comment.

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3307.8 KB 3307.0 KB
Main entry chunk (gzip) 71.3 KB 350 KB
Entry file index-DzWeUyFa.js —
Status FAIL —

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.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ✅ pass
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
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) 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) 587.16KB 141.51KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 266.92KB 67.47KB
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) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
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.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.06KB 68.77KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.45KB 29.33KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
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

Bundle Analysis is red on this head (9993fb6e), and it is red on main too. From the domain:spec @ objectui seat (objectui#10217), session session_01DBZ9bntPZ7VKyQNtJeNsgw, 2026-10-08T09:59Z. ⛔ Not a review verdict.

  • Not this PR's red: the same check fails on main's own pushes, at 19a7348 (job 113226826039) and f3a0488 (job 113232900960). Its anchor card is objectui#11937 (p0, domain:ui, claimed). The fixes in flight are PR objectui#11956 (a revert) and PR objectui#11949 (a ceiling raise), and decision card objectui#11942 is open. The anchor rules out raising the ceiling to get a green check. Nothing is ported here: the fix belongs to that card, and the two candidates are still being decided. A re-run would fail the same way while main is red, so none is spent.
  • This PR's own share, to re-read once main is fixed:
    • This merge ref reads 3307.8 KB against the 3307.0 KB ceiling (bot comment 6057351189).
    • PR objectui#11949, which changes no app code, read 3307.4 KB on its merge ref at 2026-10-08T09:38Z. So this PR adds roughly 0.4 KB gzip of eager bytes: the undoable registration description and withUndoBaseline are in @object-ui/components' eager index.
    • PR objectui#11956's merge ref reads 3306.6 KB. Once it lands, this PR would sit at about 3307.0 KB, within a few hundred bytes of the ceiling either way.
    • The seat re-reads this check after main's fix lands. If this PR alone still crosses the ceiling, trimming its eager bytes is this PR's work, and nothing will be asked of the ceiling.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9993fb6e55461ba303fdb94f7f81320bbbcb4ba7
Local-runs: none

Reviewed by an isolated subagent of the domain:spec @ objectui seat, read-only, at this record's write (2026-10-08T10:06Z). Inputs: card objectui#11168 (body and all 39 comments, the slice-1 report 5907196304 and ACCEPT 5907833984, the hold 5944262698 and its correction 5982135645, the triage pointer 6029656672, the ruling pointer 6030356700, the claim 6056149442, the dev report 6057304230); ruling record 6030342264 on objectui#11754 (letter B); PR #11954's body, its 9-file list and its net diff against main (base f3a0488, merge-base 19a7348, +461/-58); the check-runs on the head, read three times. Context read through git show at the head, not run: ActionRunner.ts (1340-1400, 1738-1830), serverActionHandler.ts (140-250), actionKeys.ts (495-520), RecordContext.tsx, SchemaRenderer.tsx (660-680, 1140-1235), static-params.ts, containers.tsx (1950-1990), useConsoleActionRuntime.tsx (381-432, 503-560, 600-615), RecordDetailView.tsx (1030-1075, 1188-1222), and the installed @objectstack/spec 17.7.0 dist/ui/index.d.ts row for action:button.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. undoable becomes a published input of action:button (boolean) — a widening of the authoring accept-set. Right. The installed spec row declares it (ComponentPropsMap['action:button'].undoable: z.ZodOptional(z.ZodBoolean)), the page validator stops reporting unknown-prop (pinned, with a lit control on an unpublished key), and nothing accepted before is refused. Decision 3 = B's condition ("declare what the renderer honours, never a key it does not deliver") is met because the renderer now delivers it (item 2), which is exactly what separates this from option D the ruling excluded. The description names the operation: update path and the one limit the ruling required stated.

  2. An undoable operation: 'update' in record scope now hands the runner the record under params._rowRecord — a behaviour widening. Right. The runner's update path is untouched and reads exactly this: collected._rowRecord (ActionRunner.ts:1766) under action.undoable && rowRecord with a non-empty written-field list (:1788), the line the ruling cites. The spelling is the one page:header (containers.tsx:1983-1985), DeclaredActionsBar, the related-record bridge and the grid rows already use. The shared dispatch deletes params._rowRecord before the POST (serverActionHandler.ts:220) and isAuthoredParamKey excludes every _-prefixed key, so the server contract is unchanged (pinned: the POST body is { recordId, params } with no stash). The claim's one ⛔ (the runner's Undo path) is honoured: packages/core is not in the diff.

  3. The record in scope is the data prop, else RecordContext.data, row outranking page. Right — and required by the ruling, not wider than it. The ruling names "the record page's current record, a table's current row". SchemaRenderer binds record for ${record.*} from useRecordContext() (SchemaRenderer.tsx:1146, configEvaluationScope) and hands the node no data, so a block reading only data would deliver nothing on an authored record page. useRecordContext() returns null outside a provider (RecordContext.tsx:243-246), so a standalone button is safe (pinned: success toast, no Undo, no error).

  4. The same-record guard (the explicit recordId, else record[recordIdField || 'id'], must equal record.id). Right — a safety refinement inside the ruling, not a narrowing of it. The runner keys the Undo by collected.recordId ?? rowRecord.id (ActionRunner.ts:1790), the shared dispatch resolves params.recordId ?? rowRecord[recordIdField] (serverActionHandler.ts:167-168), and the console runtime's own handlers resolve rowRecord.id. Attaching only where all of these agree is the only way a stash cannot address one record and restore another's values. Pinned (the other-record row with its ${record.id} lit control; ablation B reddened exactly that row).

  5. Side effect on record addressing, judged a widening. Right, with one observation carried to ③. With the stash attached, the shared dispatch resolves recordId from the record where none was explicit (serverActionHandler.ts:168; the console runtime's script handler reads rowRecord.id). So an undoable update in row scope with no explicit recordId now writes the row it sits in, where before it fell through to the grid selection or the record-scoped refusal. Nothing accepted before is refused, and the changeset states it in words. The console record page is unaffected: RecordDetailView's dispatch already resolves action.recordId ?? pureRecordId (:1211), so there the only change is the Undo. Observation: the non-undoable sibling keeps the old addressing. That asymmetry is the ruling's scope (undoable alone), not a defect of this slice.

  6. api actions untouched. Right. The ruling's mechanism is the runner's update path (ActionRunner.ts:1788). The console api handler reads the stash for more than Undo: {field} URL tokens (useConsoleActionRuntime.tsx:393-395) and recordIdParam seeding (:430-432) beside its own Undo guard (:549), so stashing on api would change request shape the ruling does not cover. On a record page the page's own api handler already honours undoable from pageRecord (RecordDetailView.tsx:1064), which is the very reason the ruling refused C. The published description scopes the affordance to operation: update, so nothing is declared that is not delivered.

  7. Parity-guard ledger: unpublishedKeys 1 → 0, owner objectui#11168 1 → 0, the owed entry and the two helpers that wrote only booked entries removed; the cap test still counts the OWED TO prefix against caps of 0. Right. This is the card's own term ("the last owner card to land restores the empty ledger and cap 0"); it narrows what the guard tolerates, in the direction the ruling booked. With undoable published and endpoint off the row since 17.6.0, action:button satisfies "publishes every top-level key its spec row declares" with no exemption, and ablation C (input renamed) reddened the guard on both arms.

  8. scripts/check-action-forward-parity.mjs JUSTIFIED prose, the forward-parity test header and the UIActionSchema.undoable JSDoc in the published .d.ts. Right. Comments and reason strings only, each made false by this change. The action:button:recordIdParam JUSTIFIED verdict still holds: the update path dispatches to the script route and never reaches the api handler that seeds recordIdParam. No type changes.

  9. Docs paragraph in content/docs/guide/layout.md. Right. It states the record-page record or the data row as the source, and the standalone and other-record limits, which is what the code does.

  10. Census (the ruling's "unmeasured gap"). Done as required. 0 stored pages declare undoable on action:button across the named trees at named refs, with lit controls on the same population; nothing needed correcting.

② Semver level

  • .changeset/11168-button-undoable.md: @object-ui/components minor, @object-ui/types patch. Right. The components change publishes a new input and a new behaviour, a widening, so at least minor (Post-Task Add public roadmap, VitePress documentation site, and GitHub Pages deployment #3); nothing an author can write is removed or renamed, so no major, no migration mapping and no ADR-0087 marker is owed. The types change is a doc comment that ships in the published .d.ts; patch is sufficient and consistent with the changeset's own text. The console test and the root script publish nothing.
  • The PR body's Clause-②: yes, no arm. Right. yes is the widening declaration and takes at least minor, which it has; the closed pair allows at most one arm and the body says in prose that it narrows nothing. A (narrowing) arm would be wrong here, since nothing accepted before is refused.
  • Gate verdicts on the head: Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report — all success.

③ Boundary flags

From the dev report 6057304230, each answered or escalated:

  • deviations[0], file-surface amendment (packages/types/src/ui-action.ts JSDoc; scripts/check-action-forward-parity.mjs JUSTIFIED prose; action-forward-parity.test.tsx header): answered — accepted. Each sentence was made false by the change; comment and prose only; none is the ⛔'d runner. The seat records the amendment on the card.
  • deviations[1], narrower than the suggested route (update only; same record only): answered — right, ① items 4 and 6.
  • deviations[2], wider than H1 (RecordContext read): answered — required by the ruling, ① item 3.
  • deviations[3], guard helpers OWED_TO / owedEntries removed: answered — right. They wrote only booked entries; with the last struck they were unused, and the console project's noUnusedLocals would refuse them. The cap test and its prefix read remain.
  • deviations[4], model-free trailer pair and footer: answered — right. objectui AGENTS.md (the 「提交信息以 model-free 的 trailer pair 收尾」 rule) is the rule, and it names the harness precedence that makes it bind.
  • deviations[5], Clause-②: yes with no arm: answered, ② above.
  • deviations[6], first runs of three gates exited 2 on a build prerequisite: answered — not measurements. Skill Example Check, Doc Snippet Type Check and Doc Example Id Check are success on the head.
  • open_questions[0], the zeroed OBJECTUI_11111_* scaffold — keep at 0 (A) or retire (B): answered — A. The card's own term is "the empty ledger and cap 0", which is what landed; a zero cap keeps any re-booking loud; B is not owed by objectui#11168 and, if wanted, is a new domain:spec card. Not a condition of this record.
  • out_of_scope_findings[0], the block's visible / disabled predicates do not see the record page's record: escalated to the seat as a filing candidate, carrier none today. After this slice the one renderer reads RecordContext for its Undo baseline but not for its predicates; the gap predates the slice, no stored producer was found, and the ruling did not cover predicate binding, so it is not a FAIL item here.
  • out_of_scope_findings[1], the runner's recordId ?? rowRecord.id beside the dispatch's rowRecord[recordIdField]: agreed, pre-existing, sidestepped by ① item 4's guard. Carrier none.
  • out_of_scope_findings[2], DeclaredActionsBar's docblock: agreed, still true for api actions, its subject.
  • This review's own observation (① item 5): a non-undoable operation: update in row scope keeps the old record addressing. Acceptance note, carrier none; the ruling's scope is undoable.

Check-runs on 9993fb6e (42; the fourth read, at this record's write): 33 success (Lint, Type Check, Spec Main Shape Gate, Build & E2E, Test (dist pins), Governed Surface Queue Guard, the doc and skill gates, the five changeset gates among them), 3 skipped by design (dependabot, the two coverage jobs), 5 in_progress (Test shards 1, 3, 4, 5 and 8; shards 2, 6 and 7 are success), and 1 failure: Bundle Analysis, on its "Check console performance budget" step — eager closure 3307.8 KB against the 3307.0 KB aggregate ceiling, every other half passing (the check's own comment 6057351189 on the PR). The same check is failure on main's own pushes at the base f3a0488 and the merge-base 19a7348 (their check-runs read directly), so the red predates this diff; this PR's own share of the closure is not separable from the check-runs alone. It is not a contract defect and does not move this verdict, but the landing condition "every check green" is unmet until main's fix lands and the head re-reads green; if the head then still crosses the ceiling on its own bytes, the trim is this PR's work, and a new head owes a new record. The in-progress shards are reported as read; a red among them would likewise need a new reading, not a new judgment of the contract.

Implemented-by: claude/issue-11168-button-undoable
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3307.0 KB 3307.0 KB
Main entry chunk (gzip) 71.3 KB 350 KB
Entry file index-kMADOrWS.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.82KB 6.58KB
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) 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) 587.16KB 141.51KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 266.92KB 67.47KB
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) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
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.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.06KB 68.77KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.45KB 29.33KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
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: 944e5affe6ea8db2aa801dc04a0c5a40e9f10e07
Local-runs: none

Reviewed by an isolated subagent of the domain:spec @ objectui seat, read-only, at this record's write (2026-10-08T10:54Z). A NARROW record on a base-merge head: 944e5aff merges objectui main (3c888c6e) into the branch on top of 9993fb6e, which the at-tier record 6057523337 on this PR judged PASS in full. This record judges whether anything that record judged has moved, and answers the gates on this head; it re-judges nothing that did not move. Inputs: card objectui#11168 (body; the claim 6056149442, the dev report 6057304230 and the seat's ACCEPT 6057547846); the earlier record 6057523337; PR #11954's body, its 9-file list, its 3 commits and its net diff against main at both heads; the check-runs on 944e5aff, read ten times. Every comparison below is a git fetch, git diff, git patch-id --stable, git merge-tree --write-tree or git show in the existing checkout — reads, nothing built, run or re-run.

① Derived judgments

  1. The PR's net diff against main is the same change at 944e5aff as at 9993fb6e. Nothing the earlier record judged under its ① items 1–10 has moved, so those judgments carry. Merge bases: 19a7348 for 9993fb6e, 3c888c6e for 944e5aff. git diff 19a7348 9993fb6e and git diff 3c888c6e 944e5aff are byte-identical (cmp silent; 687 lines each; 9 files, +461/−58 in both stats). Whole-diff git patch-id --stable is 6c9e4f6aa8230051b8dcb259885f5c2f433a9116 on both. Per-file patch-ids are equal pairwise for all nine: .changeset/11168-button-undoable.md 281328b7, apps/console/src/__tests__/registry-inputs-spec-parity.test.ts f6e7edcf, content/docs/guide/layout.md 8707320b, action-button-icon-inputs-11168.test.tsx bc76b10a, action-button-undoable-11168.test.tsx a92c87c5, action-forward-parity.test.tsx 022b8f84, packages/components/src/renderers/action/action-button.tsx 309159b9, packages/types/src/ui-action.ts f6494b06, scripts/check-action-forward-parity.mjs 3af29ac3. The diff was also read in full on this head: withUndoBaseline (the three conditions, the same-record guard on String(writtenId) !== String(record.id)), recordInScope = asRecord(data) ?? asRecord(recordContext?.data), the published undoable input and its description, the 12 pins, the struck ledger entry and the 1 → 0 caps, the three prose corrections and the changeset are exactly the hunks the earlier record names.

  2. The merge commit is a clean merge with no hand edits. Right. 944e5aff has parents 9993fb6e and 3c888c6e (the latter is origin/main at this read), author the maintainer's account, committer GitHub (the web merge button), committed 2026-10-08T10:35:16Z. git merge-tree --write-tree 9993fb6e 3c888c6e exits 0 (no conflict) and writes tree 28773a393939c9b82a79a3b6447c76a0f7c99e66, which is byte-for-byte 944e5aff's own tree. The branch-side delta of the merge (git diff 9993fb6e 944e5aff) has the same patch-id as main's delta (git diff 19a7348 3c888c6e): 47037426…. The set of files main changed and the set the PR changed have an empty intersection.

  3. What main brought in, and whether it moved anything the earlier record relied on. Nothing did. Two commits lie between 19a7348 and 3c888c6e: f3a0488c (feat(app-shell): Studio Interfaces pillar creates a page and opens it on source plus live preview (objectui#11823 step 3) #11932, the Studio Interfaces pillar creates a page, objectui#11823 step 3, merged 2026-10-08T08:43Z) and 3c888c6e (revert(app-shell): roll back objectui#11931 and objectui#11932 so the console's eager closure is under its ceiling again (objectui#11937) #11956, the revert of objectui#11931 and objectui#11932 so the console's eager closure is under its ceiling again, objectui#11937, merged 2026-10-08T10:15Z). Net over the pair: 10 files, +22/−699, all of them packages/app-shell/src/views/studio-design/* (ObjectValidationsPanel.tsx and its four test files, validationPresets.ts deleted), packages/app-shell/src/views/metadata-admin/i18n.ts, one line of content/docs/guide/console.md, .changeset/11861-validation-presets.md deleted and .changeset/11937-eager-budget-rollback.md added. Checked file by file with git diff 19a7348 3c888c6e -- FILE, each empty: packages/core/src/actions/ActionRunner.ts, packages/core/src/actions/serverActionHandler.ts, packages/core/src/actions/actionKeys.ts, packages/components/src/renderers/action/action-button.tsx, packages/components/src/renderers/action/static-params.ts, apps/console/src/__tests__/registry-inputs-spec-parity.test.ts (the console parity guard), packages/types/src/ui-action.ts, scripts/check-action-forward-parity.mjs, action-forward-parity.test.tsx, action-button-icon-inputs-11168.test.tsx, packages/react/src/context/RecordContext.tsx, packages/react/src/SchemaRenderer.tsx, packages/components/src/renderers/layout/containers.tsx, packages/app-shell/src/hooks/useConsoleActionRuntime.tsx, packages/app-shell/src/views/RecordDetailView.tsx, packages/app-shell/src/views/DeclaredActionsBar.tsx. Also unchanged: pnpm-lock.yaml, the root package.json and the package.json of apps/console, packages/components and packages/types, so the installed @objectstack/spec 17.7.0 row for action:button the earlier record read is the same row. main touched no path under packages/components, packages/core, packages/react, packages/types, apps/console, scripts or content/docs/guide/layout.md.

  4. The PR's body, file list and commits. Body: unchanged in substance, first line Fixes #11168, Clause-②: yes with no arm, base main, still draft. File list: the same 9 files with the same per-file counts. Commits: bfa93ec2 and 9993fb6e (the two the earlier record reviewed) plus the merge 944e5aff, whose only content is main's.

② Semver level

  • Unchanged, and still right. .changeset/11168-button-undoable.md is byte-identical to the one judged (@object-ui/components minor, @object-ui/types patch; patch-id 281328b7). main's own changeset churn (one file removed, one added, both app-shell) neither names nor overlaps it. Clause-②: yes, no arm, still matches a diff that widens and narrows nothing.
  • Gate verdicts on this head: Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report — all success.

③ Boundary flags

  • No new flag from the diff: it is the same bytes. The earlier record's answered items (the file-surface amendment the seat recorded in ACCEPT 6057547846, the narrower-than-suggested route, the RecordContext read, the removed guard helpers, the trailer pair, the zeroed OBJECTUI_11111_* scaffold kept at 0 = option A) and its carrier-none observations (the block's predicates not seeing the record page's record; the runner's recordId ?? rowRecord.id beside the dispatch's rowRecord[recordIdField]; DeclaredActionsBar's docblock; the non-undoable update's old addressing) stand as written there.
  • The one landing condition the earlier record left open is closed on this head. It read Bundle Analysis failure on 9993fb6e (eager closure 3307.8 KB against the 3307.0 KB ceiling) and on main's own pushes at f3a0488 and 19a7348, and said the head must re-read green after main's fix. main's fix is 3c888c6e (revert(app-shell): roll back objectui#11931 and objectui#11932 so the console's eager closure is under its ceiling again (objectui#11937) #11956, objectui#11937), it is in this head, and Bundle Analysis is success on 944e5aff (check-run 113268560698, started 2026-10-08T10:35:26Z, completed 2026-10-08T10:37:41Z; the check's own report on the PR is 6058023265). So the PR's own bytes do not cross the ceiling and no trim is owed by this PR.

Check-runs on 944e5aff (polled ten times, from 2026-10-08T10:39Z until none was in progress, 14 minutes, under the 25-minute cap; the final reading at 2026-10-08T10:53Z): 43 check-runs — 40 success, 3 skipped by design (dependabot, the two coverage jobs), 0 in progress, 0 failures. The first reading had 42 with 13 in progress; the 43rd is the aggregate Test job, which appeared once its shards completed (success at 2026-10-08T10:52:17Z). Green among them: Lint, Type Check, Spec Main Shape Gate, Build & E2E, Test (dist pins), all eight Test shards, Bundle Analysis, Governed Surface Queue Guard, the five changeset gates, the doc and skill gates, Live E2E (informational). No failure, so there is no failing step to name. Every check is green on this head, so the landing condition the earlier record left unmet is met on 944e5aff.

Implemented-by: claude/issue-11168-button-undoable
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 10:59
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 10:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit f0496bd Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11168-button-undoable branch October 8, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants