Skip to content

Commit d328698

Browse files
fix(types,core): one authority for ActionContext (objectui#6349, batch 4) (#12041)
Refs #6349 (batch 4) Clause-②: yes Batch 4 of the one-authority burn-down: the three `core` and `types` rows of `KNOWN_COLLISIONS` (`ActionContext`, `ActionResult`, `ComponentMeta`). **All three rows converge here.** `ActionContext` converged in the first round. `ActionResult` and `ComponentMeta` were stop-and-report in the first round (measured below). The seat's claim amendment `6077155250` answered them (ActionResult **A1**, ComponentMeta **A**), and the second round converged both (the dev's addendum `6077857371`; summary in the next section). Types only: the emitted JavaScript of both packages is byte-identical to the base. Notation: REC means `Record` keyed by `string` with `any` values. Generic arguments are spelled in words because GitHub strips angle-bracket spans from bodies. ## Round 2: the amendment, converged (head `7fc6d14e`) - **Ledger:** `KNOWN_COLLISIONS` had 28 rows at base and has 25 at head. The gate is 11 of 11. - Stale legs on `ActionResult` and `ComponentMeta` read `no longer collides at:`, naming both files. - Fresh legs on `ActionResult`, `ComponentMeta` and the moved `UndoableOperation` read `a NEW colliding name`. - **`ActionResult` (A1):** `@object-ui/types` is the one declaration. - It gains the runner's `reload`, `redirect`, `modal`, `silent` and `undo`, and its unread `refresh` is retired. - `UndoableOperation` moves into `ui-action.ts` and is published from the types entry. It needs no core-only type. - core's `ActionRunner.ts` re-exports `ActionResult`, and `UndoManager.ts` imports and re-exports `UndoableOperation`. - **`ComponentMeta` (A):** core's derived declaration is renamed `RegistryComponentMeta`, and core's `ComponentMeta` re-exports types'. - `register`, `registerLazy`, `getMeta` and the registry's internal readers use the new name, and `PluginScopeImpl.ts` imports it. - The derivation pin is re-aimed, and it gains a pin that core's `ComponentMeta` is the canonical one. - The three `ComponentMeta['labelling']` readers (fields `index.tsx`, app-shell `widgets.tsx`, `DashboardWidgetInspector.tsx`) read `RegistryComponentMeta['labelling']`. - **Published surface, one program per tree:** - core's `ActionResult` and `UndoableOperation` keep identical members. - core's `ComponentMeta` loses exactly `deprecated`, `labelling`, `namespace`, `skipFallback` and `tier`, and core's `RegistryComponentMeta` equals the old one. - types' `ActionResult` gains five members and loses `refresh`. - Exported names only grow: core 512 → 513 (`RegistryComponentMeta`), types 710 → 711 (`UndoableOperation`). - Emitted JS is byte-identical for types, fields and app-shell. In core it differs in one JSDoc line in `registry/Registry.js`, with a code-only diff of 0. - **Consumer impact:** 15 of 15 type-checks exit 0: types, core, react, sdui-parser, fields, plugin-dashboard, plugin-report, plugin-timeline, components, app-shell, plugin-grid, plugin-detail, plugin-view, plugin-designer and console. They cover every package that names these types or drives an `ActionRunner`. - **Suites:** - types + core + the gate: 566 files and 14,054 tests passed; - fields: 242 files and 3,956 tests passed; - app-shell, narrowed to the 37 files that import the two changed modules: 550 tests passed. Its emitted JS is unchanged, and CI runs the full suite. - **Changesets:** `minor` on types, core, fields and app-shell, each with a breaking-change note. ⛔ No `major`. ## `ActionContext`: converged on `@object-ui/types` | member | `core` (`ActionRunner.ts`, before) | `types` (`ui-action.ts`, before) | the one declaration (after) | | :-- | :-- | :-- | :-- | | `data` | optional REC | none (index signature, so `any`) | **optional REC** (core's member moves down) | | `record` | optional `any` | optional REC | optional REC | | `selectedRecords` | optional REC array | same | same | | `pageVariables` | optional REC | same | same | | `user` | optional `any` | optional REC | optional REC | | index signature | string key to `any` | same | same | - **One concept, drifted copies.** Both are the runner's execution data bag. `@object-ui/types` is the dependency-legal authority: it has no `@object-ui/*` dependency, and `core` depends on it. `ActionRunner.ts` now RE-EXPORTS it (`export type { ActionContext } from '@object-ui/types'`), and its row leaves the ledger. - **`data` moved down** because the runner reads it: as the fallback record id in `executeForm`, and as the API request body in `buildApiRequestBody`. **`record` / `user` keep the narrower REC**, because every package below compiles against it. - **Consumers, enumerated before reshaping.** Name importers: 33 import sites of `ActionContext` point at `@object-ui/core` and 7 point at `ActionRunner` relatively, against 2 of the `@object-ui/types` copy (its barrel, and one `action-group-retired` lit control). Implicit users drive an `ActionRunner` without naming the type. `pnpm --filter PKG type-check` is green on 15 packages against the converged shape, with `types` and `core` dist rebuilt first: types, core, react, sdui-parser, fields, plugin-dashboard, plugin-report, plugin-timeline, components, app-shell, plugin-grid, plugin-detail, plugin-view, plugin-designer and console. All 15 read exit 0 with zero `error TS` lines, and each run echoed its script name. The same set was exit 0 at base. ### Published surface, read off the BUILT entry declarations - An acceptance probe compiled against each tree's `dist/index.d.ts`, with predictions written first. All matched. - **Identity** of core's and types' `ActionContext`: base `TS2322` (two types), head clean (one type). - **Narrowings, on base and head:** - core's `record` given a string: base clean, head `TS2322`. - core's `user` given a string: base clean, head `TS2322`. - types' `data` given a string: base clean, head `TS2322`. - **Controls:** - An object-shaped context compiles on both trees. - The negative control (`selectedRecords` given a string) is `TS2322` on both trees, at the same columns. - **Exported name sets are unchanged.** Enumerated through the type checker over `dist/index.d.ts`: core 512 = 512 and types 710 = 710, with zero diff lines. - **Emitted JS is byte-identical**: core 107 of 107 files and types 74 of 74. - **Declaration diffs:** - core: exactly `actions/ActionRunner.d.ts`. - types: `ui-action.d.ts` (+311 tokens, 0 removed: the docblock and `data`). - types: 15 `zod/*.d.ts` files read different, but every one is token-multiset-equal. That is literal-union member ORDER between a cached base build and a fresh head build, not content. - **Grade:** `minor` for both packages, because a member type narrows on each (objectui never declares `major`). The breaking semantics are written into both changesets. ## `ActionResult`: round 1's stop, answered A1 by amendment `6077155250` (the measurement that led there) | member | `core` | `types` | | :-- | :-- | :-- | | `success`, `data`, `error`, `close` | yes | yes | | `reload` | yes | no | | `redirect` | optional `string` | no | | `modal` | optional `any` | no | | `silent` | yes | no | | `undo` | optional `UndoableOperation` (declared in core's `UndoManager.ts`) | no | | `refresh` | no | yes (in-repo readers and writers found: none) | One concept, the result of executing an action, but the only legal direction cannot carry `undo`. Two probes, both run with the mutation proven on disk and restored under a trap (blob equal to HEAD, `git diff HEAD` empty): - **R1, a re-point at today's `types` shape.** `core` `tsc --noEmit` exits 2 with 20 errors, every one on a core-only member: `redirect` ×7, `reload` ×7, `undo` ×4, `modal` ×1, `silent` ×1. - **R2, a widened authority.** `types` gains `reload` / `redirect` / `modal` / `silent`, plus `undo` typed `unknown`, the most `types` can say without reaching core. The `types` and `core` dist were rebuilt inside the trap, and the probe marker read 1 in `types/dist`. The core build itself fails. The `tsconfig.test.json` legs read: core src 1 error, core tests 17, app-shell tests 9. **All 27 are reads of `undo` as an `UndoableOperation`**: the runner's `globalUndoManager.push(result.undo)`, plus tests reading `undoData` / `redoData` / `objectName`. The restore leg rebuilt and read the marker at 0. So the one blocker is that `UndoableOperation` lives in `core`. Converging means declaring it in `@object-ui/types`, which is a new published name, and having `UndoManager.ts` re-export it. Both are outside this claim, so the row stays in the ledger. ## `ComponentMeta`: round 1's stop, answered A by amendment `6077155250` (the measurement that led there) `core`'s is `CanonicalComponentMeta & RegistryComponentMetaExtras`: `types`' declaration plus five registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`, `deprecated`). That is a different, narrower meaning, registration metadata, so the card's remedy is the rename branch. A rename probe was run, with `core` dist rebuilt inside the trap (marker present) and rebuilt again on restore (marker 0). It renamed core's declaration to `RegistryComponentMeta` and re-pointed `@object-ui/core`'s `ComponentMeta` at `types`. The breaks, all outside this claim's files: - fields `src/index.tsx`: 1, `TS2339` on `ComponentMeta['labelling']`; - app-shell `widgets.tsx` and `DashboardWidgetInspector.tsx`: 2, the same read; - core `component-meta-derives-from-canonical.test.ts`: 8, the derivation pin; - sdui-parser, plugin-dashboard, plugin-report and plugin-timeline: 0. `@object-ui/core`'s published `ComponentMeta` would lose five keys, which is a reshaped published name. The row stays in the ledger. ## Verification, round 1 (commit `9495823c2`; round 2's readings are above) - **The gate is green** (`scripts/__tests__/one-authority-per-exported-name-6273.test.ts`, 11 of 11). Its ledger had 28 rows at base and 27 after round 1, and has 25 at head after round 2. The re-derivation instrument is the test itself. - **Gate non-vacuity**, mutations proven and restored by state: - Stale leg: re-adding the `ActionContext` row reads `1 failed | 10 passed` with `ActionContext — no longer collides at:` naming both files. - Fresh leg: re-declaring it in `ActionRunner.ts` reads `ActionContext — a NEW colliding name` with both sites. - The first stale attempt was refused by `ablation-replace` (its replacement contained its own anchor), so it ran nothing. It was redone with a non-overlapping anchor. - **The moved pin.** `actionKeys.pin.test.ts` now reads `ActionContext`'s index signature off `ui-action.ts`. Ablating the signature there reads `ActionContext: false` (red), and the restore is proven. - **Root-form vitest on `packages/types/` + `packages/core/` + the gate**, run at `84f668973`: 565 of 566 files. The one failure was the pin above, which read the runner module; it was repaired in `9495823c2`. At `9495823c2`, the three changed or affected test files read 32 of 32. - **`type-check` (hyphenated, script echoed):** types and core are exit 0. Core's `tsconfig.test.json` is exit 0 at `9495823c2`, and `--listFiles` includes the pin file. `type-check:scripts` is exit 0 and covers the ledger file. - **Other gates:** `eslint` on the 4 touched files has 0 errors (22 warnings, 27 at base). `check:control-bytes`, `check:test-path-roots`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:new-line-citations`, changeset presence and no-major are all exit 0. The governed-queue guard reads NOT GOVERNED. - **Declared narrowings:** - The verify lock was held by other worktrees for most of an hour (repeated queue-timeouts). The final-commit runs of the core test type-check, `type-check:scripts` and the three test files therefore ran outside the lock, on a shared box. - The full `types` + `core` suite was not re-run at `9495823c2`. The only delta since `84f668973` is the one test file, which was run alone. - Downstream type-check covers 15 of the 38 packages that depend on `core`: the name importers plus every package that drives an `ActionRunner`. CI runs the rest. - **Eager first-load bytes:** not affected, because no emitted JS moved. ## Acceptance notes - `types`' `ActionResult.refresh` has no in-repo reader or writer: the runner reads `reload`. A handler typed against the `types` copy that sets `refresh` is ignored at runtime. This belongs to the `ActionResult` decision above, not to this PR. - `ActionExecutor` (in `ui-action.ts`) has no implementer and no importer outside its own barrel. It is an observation only. Session: `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8` --- _Generated by [Claude Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d99b731 commit d328698

17 files changed

Lines changed: 316 additions & 118 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@object-ui/app-shell': minor
3+
---
4+
5+
The metadata-admin widget registry's labelling vocabulary (`WidgetLabelling`) and the dashboard widget inspector's `labelling` prop are typed against `@object-ui/core`'s `RegistryComponentMeta['labelling']` instead of `ComponentMeta['labelling']` (objectui#6349, batch 4). `@object-ui/core`'s `ComponentMeta` is now the general component metadata and no longer carries `labelling`; the registration type that does is `RegistryComponentMeta`.
6+
7+
**Breaking-change note.** Nothing breaks for a consumer of this package: both types still resolve to the same `'control' | 'group'` vocabulary, and no runtime behaviour changes. The breaking half of this rename is `@object-ui/core`'s, recorded in its changeset.

‎.changeset/6349-batch4-core.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/core': minor
3+
---
4+
5+
`ActionContext`, `ActionResult`, `UndoableOperation` and `ComponentMeta` are re-exported from `@object-ui/types` instead of declared a second time here (objectui#6349, batch 4), so an import of any of these names from either package is the same type. The registry's own registration type gets its own name, `RegistryComponentMeta`.
6+
7+
**Type changes, breaking for some consumers.**
8+
9+
- **`ComponentMeta` from this package no longer carries the five registry-only keys** (`tier`, `namespace`, `skipFallback`, `labelling`, `deprecated`). It used to be this package's registration type, `@object-ui/types`' `ComponentMeta` plus those keys, published under the general name. That type is now `RegistryComponentMeta`, with the same members. `ComponentRegistry.register`, `registerLazy` and `getMeta` are typed with it, so a registration literal passed straight to them still compiles. Code that annotates a value as `ComponentMeta` from `@object-ui/core` and writes or reads one of the five keys must use `RegistryComponentMeta` instead (for example `RegistryComponentMeta['labelling']`).
10+
- `ActionContext`: the runner's copy typed `record` and `user` as `any`; the one declaration types both as `Record<string, any>`. A non-object `record` or `user` on a value typed `ActionContext` no longer type-checks, and under `strict` a read such as `context.record.id` now needs optional chaining (`context.record?.id`). `data`, `selectedRecords`, `pageVariables` and the index signature are unchanged.
11+
- `ActionResult` and `UndoableOperation` keep their members exactly; only their declaring package moves.
12+
13+
No runtime behaviour changes.

‎.changeset/6349-batch4-fields.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@object-ui/fields': minor
3+
---
4+
5+
`FIELD_WIDGET_LABELLING` is typed against `@object-ui/core`'s `RegistryComponentMeta['labelling']` instead of `ComponentMeta['labelling']` (objectui#6349, batch 4). `@object-ui/core`'s `ComponentMeta` is now the general component metadata and no longer carries `labelling`; the registration type that does is `RegistryComponentMeta`.
6+
7+
**Breaking-change note.** Nothing breaks for a consumer of this package: the export's value type still resolves to the same `'control' | 'group' | 'display'` vocabulary, and no runtime behaviour changes. The breaking half of this rename is `@object-ui/core`'s, recorded in its changeset.

‎.changeset/6349-batch4-types.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
`ActionContext` and `ActionResult` each have one declaration again, here, and `UndoableOperation` is published from this package (objectui#6349, batch 4). `@object-ui/core` used to declare second copies of `ActionContext`, `ActionResult` and `UndoableOperation`; it now re-exports these, so both packages publish the same types under those names.
6+
7+
**Type changes, breaking for some consumers.**
8+
9+
- `ActionContext` gains a declared `data?: Record<string, any>` member, the one member only the runner's copy declared (the runner reads it as the fallback record id and as an API request body). Before, `data` was reached only through the index signature, as `any`. A write of a non-object `data` on a value typed `ActionContext` no longer type-checks.
10+
- `ActionResult` becomes the runner's contract. It gains `reload`, `redirect`, `modal`, `silent` and `undo` (typed `UndoableOperation`). Its `refresh` member is **removed**: the runner reads `reload`, and nothing read or wrote `refresh`, so a handler that set it was ignored at runtime. A result literal naming `refresh` is now a compile error; write `reload` instead.
11+
- `UndoableOperation` (the `undo` payload: `id`, `type`, `objectName`, `recordId`, `timestamp`, `description`, `undoData`, `redoData`) is a new export, moved down from `@object-ui/core` with the same members.
12+
13+
No runtime behaviour changes.

‎packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
} from '@object-ui/components';
3232
import type { DashboardWidgetSchema, DashboardWidgetTypeName } from '@object-ui/types';
3333
import { completeWidgetLayout, defaultWidgetPlacement } from '@object-ui/types';
34-
import { resolveDashboardFilterDefs, type DashboardFilterDef, type ComponentMeta } from '@object-ui/core';
34+
import { resolveDashboardFilterDefs, type DashboardFilterDef, type RegistryComponentMeta } from '@object-ui/core';
3535
import type { MetadataInspectorProps } from '../inspector-registry.js';
3636
import { t, tFormat } from '../i18n.js';
3737
// The spec's `I18nLabel` resolver (new in @objectstack/spec 17.0.0-rc.6),
@@ -590,7 +590,7 @@ function Field({
590590
id: string;
591591
label: string;
592592
/**
593-
* How `label` is associated with the control (`ComponentMeta.labelling`'s
593+
* How `label` is associated with the control (`RegistryComponentMeta.labelling`'s
594594
* vocabulary, objectui#3961/#4010):
595595
*
596596
* - `'control'` (default) — the child is a LABELABLE element carrying this
@@ -606,15 +606,15 @@ function Field({
606606
* reads as closed. That was objectui#4010's defect on `widget-color`.
607607
*
608608
* DERIVED from the repo-wide vocabulary, not a restatement of it: the 2026-08-17
609-
* ruling (objectui#4857 + objectui#4871) made `ComponentMeta['labelling']` the
609+
* ruling (objectui#4857 + objectui#4871) made `RegistryComponentMeta['labelling']` the
610610
* single answer to "how does a host learn what a widget will render" and
611611
* forbade host-local variants. `'display'` is excluded because this panel has
612612
* no such field — every one of its children is an editable control or a
613613
* container of them — and because it has no display CHANNEL to route one to;
614614
* introducing one is a compile error here rather than a silent degradation.
615615
* Re-spell a member in `packages/core` and this type stops compiling.
616616
*/
617-
labelling?: Exclude<NonNullable<ComponentMeta['labelling']>, 'display'>;
617+
labelling?: Exclude<NonNullable<RegistryComponentMeta['labelling']>, 'display'>;
618618
children: React.ReactNode;
619619
}) {
620620
const group = labelling === 'group';

‎packages/app-shell/src/views/metadata-admin/widgets.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
DialogHeader,
4242
DialogTitle,
4343
} from '@object-ui/components';
44-
import type { ComponentMeta } from '@object-ui/core';
44+
import type { RegistryComponentMeta } from '@object-ui/core';
4545
import { AlertTriangle, ChevronDown, ChevronsUpDown, ChevronUp, Eye, EyeOff, Plus, Search, Trash2 } from 'lucide-react';
4646
import { iconNames } from 'lucide-react/dynamic.mjs';
4747
import { toast } from 'sonner';
@@ -3059,7 +3059,7 @@ export type RegisteredWidgetKey = keyof typeof WIDGETS;
30593059

30603060
/**
30613061
* The labelling vocabulary this host implements, DERIVED from the repo-wide
3062-
* declaration type rather than restated (`ComponentMeta['labelling']`,
3062+
* declaration type rather than restated (`RegistryComponentMeta['labelling']`,
30633063
* objectui#3961 → #4857 → #4871's joint ruling: no host may keep a local
30643064
* variant of it).
30653065
*
@@ -3074,7 +3074,7 @@ export type RegisteredWidgetKey = keyof typeof WIDGETS;
30743074
* nowhere). Deriving by `Exclude` keeps the vocabulary single-sourced: rename or
30753075
* re-spell a member in `packages/core` and this stops compiling.
30763076
*/
3077-
export type WidgetLabelling = Exclude<NonNullable<ComponentMeta['labelling']>, 'display'>;
3077+
export type WidgetLabelling = Exclude<NonNullable<RegistryComponentMeta['labelling']>, 'display'>;
30783078

30793079
/**
30803080
* How the HOST's visible label reaches each registered widget — the reviewed

‎packages/core/src/actions/ActionRunner.ts‎

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,43 @@
2121
* redirect handling, action chaining, custom handler registration.
2222
*/
2323

24-
import type { RunnableActionType, UIActionSchema } from '@object-ui/types';
24+
import type { ActionContext, ActionResult, RunnableActionType, UIActionSchema } from '@object-ui/types';
2525
import type { Action as SpecActionInput } from '@objectstack/spec/ui';
2626
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator.js';
2727
import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
28-
import { globalUndoManager, type UndoableOperation } from './UndoManager.js';
28+
import { globalUndoManager } from './UndoManager.js';
2929
import { warnOnDeprecatedObjectParams, warnOnUnknownActionKeys } from './actionKeys.js';
3030
import { readActionPayload } from './actionResponse.js';
3131
import { toPredicateRecord, type FieldContainerLike } from '../utils/predicate-record.js';
3232

33-
export interface ActionResult {
34-
success: boolean;
35-
data?: any;
36-
error?: string;
37-
reload?: boolean;
38-
close?: boolean;
39-
redirect?: string;
40-
/** Modal schema to render (for type: 'modal') */
41-
modal?: any;
42-
/**
43-
* Suppress the automatic success toast for this result. A handler sets this
44-
* when the action only HANDED OFF to a follow-up UI rather than completing —
45-
* e.g. a `flow` action that paused at a screen and opened the flow-runner. The
46-
* action hasn't "completed yet", so a "success" toast on open would be
47-
* misleading; the follow-up surface owns its own completion messaging.
48-
*/
49-
silent?: boolean;
50-
/**
51-
* An undoable operation captured by the handler (e.g. an `undoable` update
52-
* action's prior field values). When present, the runner pushes it onto the
53-
* global UndoManager and the success toast offers an "Undo" affordance.
54-
*/
55-
undo?: UndoableOperation;
56-
}
33+
/**
34+
* What an action handler returns and the runner reads — ONE authority, in
35+
* `@object-ui/types` (objectui#6349, batch 4), re-exported here exactly as
36+
* {@link ActionContext} is below. The members this module used to declare
37+
* (`reload`, `redirect`, `modal`, `silent`, `undo`) moved down with it, and
38+
* `UndoableOperation`, which types `undo`, moved down beside it
39+
* (`./UndoManager.js` re-exports that one). The gate that counts authorities is
40+
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`; a re-export
41+
* is not one.
42+
*/
43+
export type { ActionResult } from '@object-ui/types';
5744

58-
export interface ActionContext {
59-
data?: Record<string, any>;
60-
record?: any;
61-
selectedRecords?: Record<string, any>[];
62-
/** Live page-variable snapshot (ADR-0049), published by PageVariableActionBridge. */
63-
pageVariables?: Record<string, any>;
64-
user?: any;
65-
[key: string]: any;
66-
}
45+
/**
46+
* The context a runner executes actions in — ONE authority, in
47+
* `@object-ui/types` (objectui#6349, batch 4). This module RE-EXPORTS that
48+
* declaration instead of declaring a second one, so an import of
49+
* `ActionContext` from `@object-ui/core` and one from `@object-ui/types` are
50+
* the same type. `@object-ui/types` is the dependency-legal side: it depends on
51+
* no `@object-ui/*` package, and this package depends on it.
52+
*
53+
* A re-export is not a second authority —
54+
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` counts
55+
* declarations and ALIASING re-exports, never `export type { X } from …` —
56+
* which is why this convergence takes `ActionContext` off that gate's
57+
* `KNOWN_COLLISIONS` baseline. The member drift it removed is recorded on the
58+
* declaration itself.
59+
*/
60+
export type { ActionContext } from '@object-ui/types';
6761

6862
/**
6963
* API configuration for complex requests.

‎packages/core/src/actions/UndoManager.ts‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,15 @@
1414
* Maintains bounded history stacks with subscriber notifications.
1515
*/
1616

17-
/** Represents a single undoable CRUD operation. */
18-
export interface UndoableOperation {
19-
id: string;
20-
type: 'create' | 'update' | 'delete';
21-
objectName: string;
22-
recordId: string;
23-
timestamp: number;
24-
description: string;
25-
/** Data needed to undo: for create=recordId, for update=previousData, for delete=fullRecord */
26-
undoData: Record<string, unknown>;
27-
/** Data needed to redo: for create=newData, for update=newData, for delete=recordId */
28-
redoData: Record<string, unknown>;
29-
}
17+
import type { UndoableOperation } from '@object-ui/types';
18+
19+
/**
20+
* A single undoable CRUD operation — declared once, in `@object-ui/types`
21+
* (objectui#6349, batch 4), beside the `ActionResult` whose `undo` member it
22+
* types, and re-exported here so an import from `@object-ui/core` keeps
23+
* resolving to the same type.
24+
*/
25+
export type { UndoableOperation } from '@object-ui/types';
3026

3127
export interface UndoManagerOptions {
3228
/** Maximum number of operations to retain in history. @default 50 */

‎packages/core/src/actions/__tests__/actionKeys.pin.test.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,25 @@ import {
3737

3838
const RUNNER = join(dirname(fileURLToPath(import.meta.url)), '..', 'ActionRunner.ts');
3939

40-
/** The named interface's members, read off `ActionRunner.ts` itself. */
41-
function interfaceMembers(name: string): readonly ts.TypeElement[] {
42-
const sf = ts.createSourceFile(RUNNER, readFileSync(RUNNER, 'utf8'), ts.ScriptTarget.Latest, true);
40+
/**
41+
* Where `ActionContext` is DECLARED. Since objectui#6349 (batch 4) its one
42+
* declaration lives in `@object-ui/types`' `ui-action.ts` and `ActionRunner.ts`
43+
* re-exports it, so the index-signature half of the pin below reads the
44+
* authority. Reading the runner module would find no interface at all, and
45+
* `interfaceMembers` throws on that rather than reading it as "no signature".
46+
*/
47+
const CONTEXT_AUTHORITY = join(
48+
dirname(fileURLToPath(import.meta.url)),
49+
'..', '..', '..', '..', 'types', 'src', 'ui-action.ts',
50+
);
51+
52+
/** The named interface's members, read off the declaring file itself (`ActionRunner.ts` unless named). */
53+
function interfaceMembers(name: string, file: string = RUNNER): readonly ts.TypeElement[] {
54+
const sf = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
4355
for (const stmt of sf.statements) {
4456
if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === name) return stmt.members;
4557
}
46-
throw new Error(`${name} interface not found in ActionRunner.ts`);
58+
throw new Error(`${name} interface not found in ${file}`);
4759
}
4860

4961
/** `ActionDef`'s declared property names, read off the interface itself. */
@@ -55,8 +67,8 @@ function declaredActionDefKeys(): string[] {
5567
}
5668

5769
/** Does the named interface end with an `[key: string]: any`-style catch-all? */
58-
function hasIndexSignature(name: string): boolean {
59-
return interfaceMembers(name).some(ts.isIndexSignatureDeclaration);
70+
function hasIndexSignature(name: string, file: string = RUNNER): boolean {
71+
return interfaceMembers(name, file).some(ts.isIndexSignatureDeclaration);
6072
}
6173

6274
/**
@@ -107,7 +119,7 @@ describe('action key inventory (objectstack#4075 step 1)', () => {
107119
// through a change that "tidied up" `ActionContext` too.
108120
expect({
109121
ActionDef: hasIndexSignature('ActionDef'),
110-
ActionContext: hasIndexSignature('ActionContext'),
122+
ActionContext: hasIndexSignature('ActionContext', CONTEXT_AUTHORITY),
111123
}).toEqual({ ActionDef: false, ActionContext: true });
112124
});
113125

‎packages/core/src/registry/PluginScopeImpl.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
PluginScopeConfig,
2222
PluginEventHandler
2323
} from '@object-ui/types';
24-
import type { Registry, ComponentMeta as RegistryComponentMeta } from './Registry.js';
24+
import type { Registry, RegistryComponentMeta } from './Registry.js';
2525

2626
/**
2727
* Event Bus for scoped plugin events

0 commit comments

Comments
 (0)