Skip to content

refactor(core): retire ActionEngine's zero-caller event-mapping API (#3368) - #4411

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3368-retire-event-mapping
Aug 12, 2026
Merged

refactor(core): retire ActionEngine's zero-caller event-mapping API (#3368)#4411
yinlianghui merged 1 commit into
mainfrom
claude/issue-3368-retire-event-mapping

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3368

The ruling

Maintainer ruling of 2026-08-11, recorded on #3368 by the triage seat and quoted verbatim:

Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

Ruling: retire addMapping — the enforce-or-remove default for a zero-caller public export; no usage plan exists. Remove the export and its dead registration path. State: findingpm:queue.

Zero-caller evidence, re-measured at this branch point (f762f5bdf)

The premise holds, unchanged from the four HOLD re-verifications on the card.

Symbol Where it occurred, before deletion Production callers
addMapping packages/core/src/actions/ActionEngine.ts:181 (definition) + __tests__/ActionEngine.test.ts:77, :113, :128, :211 0
ActionEngine.dispatch ActionEngine.ts:324 (definition) + __tests__/ActionEngine.test.ts:115, :121, :134 0
ActionMapping ActionEngine.ts:48 (interface), :96 (the private mappings field), :181 (the addMapping parameter type) 0

Commands, word-boundary aware, whole tree with node_modules excluded:

$ git grep -nw 'addMapping' -- . ':!**/node_modules/**'
packages/core/src/actions/ActionEngine.ts:181:  addMapping(mapping: ActionMapping): void {
packages/core/src/actions/__tests__/ActionEngine.test.ts:77:      engine.addMapping({ event: 'toolbar:save', actionName: 'save' });
packages/core/src/actions/__tests__/ActionEngine.test.ts:113:      engine.addMapping({ event: 'row:click', actionName: 'log' });
packages/core/src/actions/__tests__/ActionEngine.test.ts:128:      engine.addMapping({
packages/core/src/actions/__tests__/ActionEngine.test.ts:211:      engine.addMapping({ event: 'test', actionName: 'save' });

dispatch is a common word in this repo (116 files match it), so it was scoped to the
method rather than the token. Three checks, all clean:

  • git grep -nE '\.dispatch\(' -- packages apps examples scripts (CHANGELOG excluded) returns exactly three hits, all engine.dispatch(...) in ActionEngine.test.ts.
  • Indirect access: git grep -nE "\[[\"'](dispatch|addMapping)[\"']\]" returns zero hits repo-wide, so nothing reached either method through a string key.
  • packages/react/src/hooks/useActionEngine.ts — the one React wrapper around this class — contains zero occurrences of dispatch, addMapping or ActionMapping, so the hook never re-exposed the event-mapping surface to its consumers.

That last check is what licenses deleting dispatch outright: it reads only this.mappings,
which only addMapping ever wrote, so it serves the event-mapping path and nothing else. The
other four entry points (executeAction by name, getActionsForLocation by location,
handleShortcut by shortcut, executeBulk in bulk) are untouched and keep their callers.

Export layers narrowed

ActionMapping was public API of @object-ui/core, reaching consumers through two star re-exports:

Layer Line Change
packages/core/src/actions/ActionEngine.ts the export interface ActionMapping declaration deleted
packages/core/src/actions/index.ts:10 export * from './ActionEngine.js' no edit — star; narrows automatically
packages/core/src/index.ts:26 export * from './actions/index.js' no edit — star; narrows automatically

Because both barrels are export *, the narrowing happens by deleting the declaration and
there is no barrel line to remove — recorded here so the absence of a barrel diff is not read
as an oversight. packages/core commits no dist/*.d.ts (0 tracked files under dist/), so
no generated surface needed regenerating. ActionMapping does not exist in @object-ui/types,
which was verified before touching that package: it was never in scope.

Deleted

What Where
ActionEngine.addMapping() packages/core/src/actions/ActionEngine.ts
ActionEngine.dispatch() same
private mappings: ActionMapping[] (the registry state) same
the this.mappings line in unregisterAction() same
the this.mappings line in clear() same
export interface ActionMapping same
4 addMapping call sites + the 3-test describe('dispatch') block packages/core/src/actions/__tests__/ActionEngine.test.ts

Two of the four addMapping call sites were in tests that cover something else, so those
tests were kept and only the mapping-specific lines removed — unregisterAction's test
(its assertions were already on getAction / getShortcuts, never on mappings) and
clear's test (same). Both titles lost their now-false mappings clause. The three tests
inside describe('dispatch') covered only the deleted path and went whole.

The three contract inconsistencies die with the path

Per the ruling and the dispatch, the inconsistencies recorded on the card are not fixed
anywhere and are not ported into a doc note as live behaviour:

  • entry gate was a raw truthy check, so condition: false dispatched anyway (08-09 addendum);
  • condition typed as string only, so a { dialect: 'cel', source } envelope could not reach the canonical @objectstack/formula engine;
  • evaluated without throwOnError, so a throwing predicate failed OPEN — the opposite of visible's fail-closed posture in getActionsForLocation.

Aligning the contract of an API nobody calls would only have widened behaviour nobody uses,
which is the reasoning the card itself gave for grading this observation-class.

Retirement record — a doc note at the survivor site, per measured precedent

git log -S on the prior core-export retirements gives the convention. Commit d9d346307
("retire four zero-consumer declared surfaces (#4328) (#4366)") contains all three shapes:

Prior retirement Record it left Why
mergeViewsIntoObjects (core public export, whole file) barrel line + changeset only, no pin, no tombstone the declaration site went away entirely
RoleDefinition.permissions (member of a surviving exported type) expanded docblock on the surviving interface naming what was retired, why, and the issue number the type survives and a reader needs the history
RecordContextValue.loading / error negative pin test added live wiring (a memo dependency list) could half-land silently

This card is the middle shape: members removed from a surviving exported class. So it follows
RoleDefinition's convention — a retirement note on the ActionEngine class docblock naming
the retired methods, the registry behind them, the issue, and the three drifted behaviours as
history, plus a forward instruction that any future event-keyed entry point must be built
on the shared hasDeclaredPredicate + toPredicateInput definitions rather than a fourth
spelling. The file header's own "event-to-action mapping" clause was removed in the same pass.

No negative pin, and that is a measured decision rather than an omission: the pin in the
third row exists because dead wiring could reappear without any gate noticing. Here it cannot —
every residual reference to the deleted registry is a compile error, and the repo-wide
type-check below is the gate that would catch a resurrection. That is the same reasoning
that left mergeViewsIntoObjects and RoleDefinition.permissions unpinned in the precedent
commit.

Post-deletion sweep — each symbol individually

Every residual hit lands in the intentional retirement note; nothing else survives anywhere in the tree.

Symbol Residual hits after deletion
addMapping 1 — ActionEngine.ts:89, inside the retirement docblock
ActionMapping 0 repo-wide
.dispatch( 0 across packages / apps / examples / scripts
mappings in packages/core 1 — ActionEngine.ts:91, the same docblock
event-to-action / event mapping 0 outside the docblock (the remaining tree hits are unrelated drill-event mapping CHANGELOG rows)

No README, guide, ADR or doc page ever documented this API, so nothing needed retargeting —
that absence was measured, not assumed.

Changeset grading

.changeset/retire-action-engine-event-mapping-3368.md@object-ui/core: minor.

Graded by the measured precedent, not by feel. ActionMapping was reachable as
@object-ui/core's public export through the two star barrels above, and addMapping /
dispatch were public methods of the exported ActionEngine class, so the public surface
narrows
and code typing against or calling any of them stops compiling. That is exactly the
case d9d346307 graded '@object-ui/core': minor for mergeViewsIntoObjects, and the same
grading #4403 applied to common.search for narrowing an exported type. It is not patch:
patch would have been right only if the symbols had turned out unexported from the package
barrel, and the export-layer table above measures that they were not. Never major, per the
version-alignment rule (the major tracks @objectstack; breaking changes ship as minor with
the semantics spelled out in the body).

No runtime behaviour changes, because no runtime path reached the deleted code.

Verification

Step Result
pnpm --filter '@object-ui/core^...' build (build closure first, fresh worktree) green
repo-root pnpm exec vitest run packages/core/ --maxWorkers=2 Test Files 82 passed (82), Tests 1713 passed (1713)
pnpm --filter @object-ui/core type-checkboth tsc commands (tsc --noEmit && tsc -p tsconfig.test.json) green; core's tests compile post-tranche-5 and still do
repo-wide pnpm exec turbo run type-check --concurrency=2 78 successful, 78 total
turbo run lint --filter=@object-ui/core 0 errors (529 pre-existing warnings, all no-explicit-any / unused-directive in untouched files)
node scripts/check-control-bytes.mjs OK, 4119 tracked text files
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over the three changed files clean
check-changeset-presence.mjs / check-changeset-no-major.mjs OK / no major

All heavy runs were serialized through the shared flock verification lock and capped at
--max-old-space-size=4096.

The repo-wide type-check is the load-bearing step for this change: an export removal is
only safe if no downstream package typed against it, and 78/78 green across every package,
app and example is that proof. Note the direction — this is the whole workspace, not a
filtered upstream closure, so it necessarily includes every consumer of @object-ui/core.

Reverse verification (direction predicted before running)

Per the #4365 retirement pattern, the zero-caller evidence plus the green ladder after
deletion is the verification, and it is what the table above records. Predicted direction
before running: green throughout, because a deletion with zero callers cannot turn a consumer
red — a red anywhere would have falsified the premise rather than revealed a bug in the patch.
That is what measured.

No negative pin was added (see the grading above), so there is no scratch re-introduction to
show red — the compile-time proof is the repo-wide type-check, whose 78/78 green is the same
assertion a pin would have made, executed against every package rather than one.

Surface discipline

Touched exactly three files, all inside the dispatch's stated surface:
packages/core/src/actions/ActionEngine.ts, its test, and the changeset. No barrel file
needed editing (both are export *), and packages/types was verified not to contain
ActionMapping before being left alone. None of the parallel agents' reserved surfaces —
scripts/** new gates (#4394), the defaults-map files and mirror suite (#4401),
packages/fields/** (#4361), packages/plugin-charts/** (#4405),
packages/app-shell/src/views/metadata-admin/** (#4406) — was read or modified.

No out-of-scope findings.


Generated by Claude Code

…3368)

Implements the maintainer ruling of 2026-08-11 on objectui#3368: retire
`addMapping` under enforce-or-remove — remove the export and its dead
registration path.

Removed from `@object-ui/core`'s public surface: `ActionEngine.addMapping()`,
`ActionEngine.dispatch()`, the private `mappings` registry they shared, and the
exported `ActionMapping` interface. Re-measured at this branch point
(f762f5b): definition plus four call sites, all four in the engine's own test
file, zero production callers. Nothing ever registered a mapping, so
`dispatch()` had no reachable caller either.

The three recorded contract inconsistencies vs `visible` (truthy entry gate,
string-only condition, fail-open evaluation) die with the path rather than being
fixed on it. A retirement note at the survivor site records what left and why,
per the convention measured on d9d3463 (#4328 / PR #4366).

Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 4:04am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-BHI8r8U4.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.20KB 108.43KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.42KB 41.19KB
fields (index.js) 228.33KB 56.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 120.57KB 31.32KB
plugin-designer (index.js) 211.06KB 42.70KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.18KB 26.76KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.99KB 10.74KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #3368, executing the maintainer ruling of 2026-08-11.

  • Premise re-measured at the right granularity: word-boundary on addMapping, method-scoped .dispatch( (3 hits, all tests — not the 116 bare-word files), string-key indirection at zero, and the react wrapper proven to never re-expose the surface. dispatch proven single-purpose before deletion (reads only the registry only addMapping wrote).
  • The no-pin decision is accepted as measured, not skipped: the precedent commit pinned only where live wiring could half-land unseen and left its two clean removals unpinned; here every residual reference is a compile error and the repo-wide 78/78 type-check IS the gate, executed across every consumer. The retirement docblock at the survivor site follows the middle shape of that same commit.
  • Test surgery precision noted (two tests kept minus their mapping lines, titles corrected; only the dispatch block went whole), changeset minor on the two-precedent grading, and the lint-queue handling (draft pushed to let CI converge during a 20-minute flock wait, body updated with real numbers before reporting) is the pipeline discipline working.
  • The external-consumer breaking cost is the enforce-or-remove ruling's accepted cost, spelled out in the changeset. The three contract inconsistencies died with the path, unfixed, as scoped.

Flipping ready + arming auto-merge.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 04:13
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 2459a3e Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3368-retire-event-mapping branch August 12, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActionEngine 的 event-mapping(addMapping / dispatch)零生产调用方,且 condition 求值与 visible 契约不一致

2 participants