Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) by os-sam · Pull Request #7065 · objectstack-ai/objectui · GitHub
Skip to content

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934) - #7065

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback
Aug 31, 2026
Merged

feat(core,types,components)!: retire ActionRunner's legacy onSuccess chained-callback channel (#5934)#7065
os-sam merged 2 commits into
mainfrom
claude/issue-5934-retire-onsuccess-callback

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5934

Implemented in session claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB (dev seat). Executes the verbatim maintainer ruling on the card (hotlong, 2026-08-31, director batch #5, item #1): 「退役 ActionRunner 的传统 onSuccess 回调通道(ActionDef | ActionDef[] 形),onSuccess 收敛为 spec 声明的唯一语义 { navigate, openIn }」. All three must-answers were re-verified independently before any edit; both hold, so the BREAKING route applies (the ruling's downgrade clause was not triggered).

Must-answer 1 — the member IS on the exported ActionDef (verified, holds)

Chain, measured on base 85b495795:

  • packages/core/src/actions/ActionRunner.tsexport interface ActionDef spans lines 113 to 441 and contains line 324 onSuccess?: ActionDef | ActionDef[] (the card said line 323; one line of drift on current main, same member).
  • src/actions/index.ts line 9 export * from './ActionRunner.js'src/index.ts line 45 export * from './actions/index.js' → package exports map .dist/index.d.ts line 29 → dist/actions/index.d.ts line 8 → dist/actions/ActionRunner.d.ts line 104 export interface ActionDef, line 295 onSuccess?: ActionDef | ActionDef[].

So the removal is a breaking change to the published surface of @object-ui/core. Per this repo's release model (scripts/check-changeset-no-major.mjs: objectui's major is pinned to the @objectstack family major, and the repo's own breaking changes ship as minor with the break spelled out in the changeset body), the changeset is minor and opens with the BREAKING declaration.

Must-answer 2 — zero-producer census, re-run with a positive control

Procedure: repo-wide grep of every onSuccess occurrence outside dist (286 hits, 70 files), filtered to writer-shaped hits (onSuccess: with a value, excluding type declarations and property reads), then each writer classified by the ruling's discriminator — is the value an ActionDef-shaped object written onto an action object, as opposed to a React form callback on a different surface, a spec-shaped { navigate, openIn } block, or a forward of a read value.

Producers of the retired callback shape found: exactly three, all test fixtures of the channel itself or of its discrimination —

  • packages/core/src/actions/__tests__/ActionRunner.test.ts lines 1028 and 1056 — the channel's own pins (rewritten by this PR to pin the absence);
  • packages/app-shell/src/utils/__tests__/consoleServerAction.test.tsx line 415 — a negative fixture proving the shape is NOT mistaken for the declared hop (still passes unchanged, because that behavior survives retirement).

Zero producers in app code, metadata, or examples. The many form-component onSuccess: writers (handleFormSuccess, arrow functions) are React callbacks on ObjectFormSchema and friends — a different surface, excluded exactly as triage instructed.

Positive control: the identical procedure run against chain (adjacent member, certainly produced) reports non-zero producers — ActionRunner.test.ts lines 970, 988, 1007 write ActionDef arrays onto action objects, among 50 writer-shaped hits. The instrument also distinguishes shapes on the same key: it found the spec-shaped onSuccess: { navigate: ... } producers (e.g. consoleServerAction.test.tsx line 390). The census is a live measurement, not a broken instrument reading zero everywhere.

Also re-measured on the installed @objectstack/spec 17.2.0 pin, with a valid-fixture control: a bare valid action parses; onSuccess: { navigate: '/x', openIn: 'newTab' } parses; onSuccess: { navigate: '/x', bogus: 1 } is refused with unrecognized_keys; onSuccess: { type: 'notify' } and onSuccess: [{ type: 'cb1' }] are refused. The channel was unreachable from validated metadata, exactly as the card claimed.

Published-surface delta, by specifier

  • @object-ui/coreBREAKING (minor bump by the pinned-major release model; the break is declared in the changeset body). import type { ActionDef } from '@object-ui/core': ActionDef['onSuccess'] was ActionDef | ActionDef[] (chained callbacks the runner dispatched through executeChain on success). After: derived from the spec — SpecActionInput['onSuccess'], the closed strict block { navigate: string, openIn?: 'self' | 'newTab' }, following the interface's own derivation doctrine (never hand-copied). Runtime: a callback-shaped value now gets NO reading — no handler dispatch, no navigation, the action's own result untouched. Before, it was executed as a chain. Consumers who drove the channel programmatically migrate to chain (unchanged, declared) or author the spec block. ActionDef['onFailure'] is NOT changed — the spec declares no such key, so it has only ever had its one runner-native meaning; the ruling covers onSuccess alone.
  • @object-ui/typesadditive (minor). import type { UIActionSchema } from '@object-ui/types': the renderer view now declares onSuccess, derived from the spec's ActionSchema.onSuccess. Before, the key reached renderers only through casts; nothing an existing consumer imported changes shape.
  • @object-ui/componentspatch, no API change. The four action renderers forward onSuccess without the as any casts the ruling ordered removed: action-button.tsx, action-group.tsx, action-icon.tsx, action-menu.tsx. Same key, same value, now type-checked. (Measured why the casts existed: UIActionSchema is a closed interface — the index signature at ui-action.ts line 625 belongs to ActionContext, not to it — so the read side needed the key declared; the types change above is the minimal enabler, and the write side is covered by the core retype.)

Behaviour unchanged for every consumer that did not use the removed member

  • The spec-path pins were NOT touched by this PR outside the retired-channel section and stay green: ActionRunner.onSuccessNavigation.test.ts (SPA hop, interpolation depth, openIn spelling isolation), action-onSuccess-forward.test.tsx, action-forward-precedence.test.tsx, consoleServerAction.test.tsx (declared-hop precedence, redirectUrl convention, legacy-shape non-confusion).
  • Full targeted union at head 365f96c29; the only later commit, 309ca3ec8, changes a single .changeset/*.md file which none of those runs read (declared narrowing), and both changeset gates — node scripts/check-changeset-no-major.mjs and pnpm changeset:check — were re-run green at 309ca3ec8: core actions suite + components action renderer suite + the whole @object-ui/types test dir + the app-shell server-action test — 117 files, 1677 tests, all passing. Type-checks green for @object-ui/types, @object-ui/core, @object-ui/components, @object-ui/app-shell (after building the dependency closure). Gates: check:action-forward-parity, check:spec-symbols, check:control-bytes all green. pnpm exec eslint on every touched file: 0 errors (135 pre-existing warnings, all no-explicit-any/style classes already present in those files).
  • check:readme-exports is NOT MEASURED locally: it exits 1 in this worktree with 69 findings, every one of them "type entry dist/index.d.ts is not on disk — run pnpm build first" for packages outside this diff (app-shell, cli, plugin-ai, plugin-gantt, ...), zero findings for core/types/components READMEs. It needs the full workspace built, which CI does before running it.

Reverse verification (both directions, from the committed state)

  • Runtime leg: restored the BASE ActionRunner.ts (channel alive) while keeping the new pins — mutation proven on disk by marker count (the executeChain fall-through line: 0 before, 1 after mutation). Result: exactly the 3 retirement pins go red (3 failed, 91 passed), every spec-path pin stays green. Restoration proven three ways: empty git diff HEAD for the path, marker count back to 0, and git hash-object of the working file equal to the HEAD blob hash. The pinned tests import the runner by relative path, so this leg is source-compiled — no stale-dist hazard.
  • Type leg, consumer eye: a probe file inside @object-ui/components importing ActionDef from @object-ui/coreonSuccess: { type: 'notify' } is refused (TS2353, and the error text quotes the new derived shape { navigate: string; openIn?: "self" | "newTab" }, proving the REBUILT d.ts is what consumers resolve, not a cache); the array form is refused (TS2741, navigate missing); the spec block compiles. Zero non-probe errors; probe removed; tree clean.

Retirement conventions

ADR-0087 registries / retirementTombstone() were judged NOT applicable and here is why: that machinery retires authored spec metadata keys (parse-level tombstones so authoring a retired key is a loud rejection). This card retires a runtime channel on a published TypeScript type — the authored key onSuccess is not retired, it converges on its declared spec meaning, which the spec already enforces at parse. The compile error for the callback shape (measured in the type leg above) is the analogous authoring-time loud rejection. The in-code documentation records the retirement at the declaration, the post-execution site, and the shape guard.

Follow-on note: this is the sequential follow-on to #5493, which remains closed (landed via #6304) — its four forward sites are exactly the cast sites this PR un-casts. #5493 is not re-opened or otherwise affected here.


Generated by Claude Code

…chained-callback channel (objectui#5934)
The maintainer ruled (2026-08-31, director batch #5 item #1) to retire the
runner's pre-spec onSuccess meaning: ActionDef | ActionDef[], dispatched
through executeChain. The spec strict-refuses a callback shape inside
onSuccess at parse, so no validated metadata could ever reach the channel,
and a producer census with a positive control found zero producers outside
the channel's own pins.
- core: ActionDef.onSuccess now derives the spec's { navigate, openIn }
block (SpecActionInput['onSuccess']); handlePostExecution no longer falls
through to executeChain; readOnSuccessNavigation is a shape guard on
unparsed rows, not a discriminator. onFailure is untouched.
- types: UIActionSchema declares onSuccess, derived from the spec, so the
renderer view spells the key the four action surfaces forward.
- components: the four renderers forward onSuccess uncast.
- tests: the channel's pins now pin its absence; the spec-path pins are
untouched and stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…release model
objectui's major is pinned to the @objectstack family major; the repo's own
breaking changes ship as minor with the break spelled out in the changeset
body (scripts/check-changeset-no-major.mjs). The BREAKING declaration in the
body is unchanged and now names the model explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-sam
os-sam marked this pull request as ready for review August 31, 2026 16:13
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3149.2 KB3191.4 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CAGVW1R_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)14.51KB5.35KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correcting a hold I placed on this PR in error. Auto-merge (squash) is now armed at 309ca3ec8.

I had marked this PR ready but deliberately withheld auto-merge, on the stated reason that it is a published-surface narrowing and therefore had to go through the contract-review chain. That reason does not apply to this card. I carried it over from #6881's ruling, which is a different card.

The two rulings differ exactly here:

So this PR was waiting on a gate its ruling never prescribed. Holding it did not make anything safer; it just stalled a PR whose floor had already been cleared by the maintainer, in the very comment that authorised the work.

For the record, the reason I noticed: #7066 carried needs:contract-review and this PR did not — the implementing agent applied the label to #7066 per that ruling's clause and correctly did not apply it here. That asymmetry was the tell, and it was right while my hold was wrong.

State at arming: mergeable_state: clean, not draft, CI complete on head 309ca3ec8 with no failing or running third-party suite, both changeset gates green at that head (check-changeset-no-major.mjs exit 0, changeset:check exit 0), no OBJECTUI_ALLOW_MAJOR, gate untouched. Both must-answers were independently verified before any edit and both held, so the BREAKING route applied and the ruling's downgrade clause was not triggered.

#7066 remains held and unarmed — its ruling does prescribe the review chain, and that hold stands.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued at 20:12:13Z with CI_FAILURE, re-queued once. Recording what I established and — importantly — what I could not.

What I established

⛔ What I could NOT establish, stated as a gap rather than papered over

I do not have the failing check's name or log. The merge_group workflow-run listing returns only the batch re-formed at 20:12:16–20:12:20Z — 60 runs across two pages, all success — so the run that actually failed is not reachable through that channel at the page depth available to me. I am therefore not claiming to know which check failed, nor asserting this was another PR's fault as a measured fact. It is an inference from the five points above, and it is labelled as one.

⛔ I am also not calling it a flake. "Flake" is not a root cause, and I have not earned that word here.

What I did

Re-enabled auto-merge (squash) on all three — one attempt, on the strength of the evidence above and because re-queueing is the remedy this repo's own dequeue notice prescribes for a grouped-queue failure. ⛔ Not a fix, because there is nothing here to fix that I can see; and ⛔ not an empty commit or a close/reopen to kick CI.

If it dequeues a second time, that is real and it is this PR's to root-cause — not another re-queue. The next reader should pull the failing job's log directly from the merge-queue run rather than the workflow listing, which is where I ran out of reach.

Context: the domain:ui dispatch seat's shift has ended; handover is #7089. This PR was armed as part of that shift's close-out, and the ruling it implements (#5934, hotlong, director batch #5 item #1) discharges its own human floor — see the earlier comment on this PR for why it is correctly not held for the contract-review chain that its sibling #7066 is.


Generated by Claude Code

Merged via the queue into main with commit 2c3cd1bAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5934-retire-onsuccess-callback branch August 31, 2026 20:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionRunner's legacy ActionDef.onSuccess chained-callback channel is unreachable from validated metadata and has zero producers

2 participants

@os-sam@claude