From 14dc786953c537d1e2aefc2f49f0801fcc24ff51 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:32:31 +0800 Subject: [PATCH] feat(managedBy): add explicit `engine-owned` lifecycle bucket (tracks framework ADR-0103 addendum, #3343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the framework's engine-owned enum split in the UI type + runtime + badge. - @object-ui/types: `ManagedByBucket` union + `MANAGED_BY_BUCKETS` gain `'engine-owned'` (closed union → missed consumers are compile errors). - @object-ui/core: `resolveCrudAffordances` DEFAULTS gains the engine-owned all-locked row (same matrix as system/append-only), so isObjectInlineEditable and the grid/form gates treat it read-only automatically. - @object-ui/app-shell: ManagedByBadge renders engine-owned with the same read-only "System-managed" copy as a locked `system` object — reuses the existing `managedByBadge.system` i18n key (zero translation churn; the distinction is at the schema level). resolveManagedByEmptyState reuses the `system` engine-owned empty state. Behaviour-preserving — engine-owned resolves to the same locked affordances a locked `system` object did, so rendering is unchanged; the value just makes the schema self-documenting. New unit coverage across resolveCrudAffordances / isObjectInlineEditable / MANAGED_BY_BUCKETS / the empty-state helper. Co-Authored-By: Claude Opus 4.8 --- .changeset/managedby-engine-owned-bucket.md | 13 ++++++++++ .../src/components/ManagedByBadge.tsx | 26 ++++++++++++++++--- .../__tests__/managedByEmptyState.test.ts | 7 +++++ .../src/utils/managedByEmptyState.ts | 4 +++ packages/core/src/utils/managedBy.test.ts | 10 +++---- packages/core/src/utils/managedBy.ts | 11 ++++---- packages/types/src/managed-by.ts | 2 ++ 7 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 .changeset/managedby-engine-owned-bucket.md diff --git a/.changeset/managedby-engine-owned-bucket.md b/.changeset/managedby-engine-owned-bucket.md new file mode 100644 index 000000000..a9a90f5b6 --- /dev/null +++ b/.changeset/managedby-engine-owned-bucket.md @@ -0,0 +1,13 @@ +--- +"@object-ui/types": minor +"@object-ui/core": minor +"@object-ui/app-shell": patch +--- + +**Add the explicit `engine-owned` lifecycle bucket (tracks framework ADR-0103 addendum / #3343).** The framework split the overloaded `managedBy: 'system'` bucket by promoting the engine-owned case to its own enum value; this mirrors it in the UI type + runtime + badge. + +- **`@object-ui/types`** — `ManagedByBucket` union and `MANAGED_BY_BUCKETS` gain `'engine-owned'` (canonical order: `platform, config, system, engine-owned, append-only, better-auth`). The union stays closed, so every consumer that missed the new value is a compile error. +- **`@object-ui/core`** — `resolveCrudAffordances` gains the `engine-owned` default row (identical all-locked matrix as `system`/`append-only`), so `isObjectInlineEditable` / the grid + form gates treat it as read-only automatically. +- **`@object-ui/app-shell`** — the `ManagedByBadge` renders `engine-owned` with the same read-only "System-managed" copy as a locked `system` object (reuses the existing `managedByBadge.system` i18n key — zero translation churn; the distinction is at the schema level, not the user-facing string), and `resolveManagedByEmptyState` reuses the `system` engine-owned empty state. + +Behaviour-preserving: `engine-owned` resolves to the same locked affordances `system` did by default, so nothing about how a locked object renders changes — the value just makes the schema self-documenting. New unit coverage for the bucket in `resolveCrudAffordances` / `isObjectInlineEditable` / `MANAGED_BY_BUCKETS` / the empty-state helper. diff --git a/packages/app-shell/src/components/ManagedByBadge.tsx b/packages/app-shell/src/components/ManagedByBadge.tsx index b553c3297..9a3d7a074 100644 --- a/packages/app-shell/src/components/ManagedByBadge.tsx +++ b/packages/app-shell/src/components/ManagedByBadge.tsx @@ -28,10 +28,12 @@ import { isSystemWritable, type ManagedByBucket } from '../utils/crudAffordances * `@objectstack/spec/data/object.zod.ts` → `ObjectSchemaBase.managedBy`: * - `platform` — User-owned business data. **Renders nothing.** * - `config` — Admin-authored configuration. - * - `system` — Engine-managed runtime rows (default) — but a `system` - * object that opens writes via `userActions` (ADR-0103) is - * platform-defined, admin/user-writable DATA and gets the - * distinct writable-system copy instead. + * - `system` — Engine-managed schema. A `system` object that opens writes + * via `userActions` (ADR-0103) is platform-defined, + * admin/user-writable DATA and gets the distinct + * writable-system copy; a locked one reads engine-owned. + * - `engine-owned` — Runtime rows a platform service owns end to end (ADR-0103); + * the explicit read-only monitoring surface. * - `append-only` — Immutable audit log. * - `better-auth` — Identity tables owned by better-auth driver. * @@ -106,6 +108,22 @@ const VARIANTS: Record = { 'Rows here are created automatically when actions run on the source record. The list below is a read-only monitoring surface — row-level actions (Approve, Recall, Resend, …) live on each row.', tone: 'border-slate-300/60 bg-slate-50 text-slate-900 hover:bg-slate-100 dark:border-slate-500/40 dark:bg-slate-950/40 dark:text-slate-100', }, + // ADR-0103 — the explicit engine-owned bucket: rows a platform service owns end + // to end (jobs, automation runs, approval runtime rows, the metadata store, …). + // To a user this reads identically to a locked `system` object ("the platform + // manages this, read-only"), so it deliberately REUSES the `system` copy/i18n key + // — zero translation churn, consistent UX; the self-documentation is at the + // schema level. (Same object shape as `system`; the distinct bucket value is the + // point, not distinct user-facing copy.) + 'engine-owned': { + icon: Lock, + i18nKey: 'system', + short: 'System-managed', + title: 'Managed by the platform', + body: () => + 'Rows here are created automatically when actions run on the source record. The list below is a read-only monitoring surface — row-level actions (Approve, Recall, Resend, …) live on each row.', + tone: 'border-slate-300/60 bg-slate-50 text-slate-900 hover:bg-slate-100 dark:border-slate-500/40 dark:bg-slate-950/40 dark:text-slate-100', + }, // ADR-0103 — a `system`-bucket object that opened writes via `userActions`: // platform-defined schema, but admin/user-writable DATA (e.g. Notification // Preferences, delegated RBAC assignments). Resolved in the component when the diff --git a/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts b/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts index 169551446..8462c2b47 100644 --- a/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts +++ b/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts @@ -20,6 +20,13 @@ describe('resolveManagedByEmptyState', () => { expect(resolveManagedByEmptyState('append-only', t)?.title).toBe('No events recorded'); }); + // ADR-0103 — the explicit engine-owned bucket reuses the `system` engine-owned + // empty state; it never opens creation, so it always renders that copy. + it('gives the engine-owned bucket the same engine-owned empty state as locked system', () => { + expect(resolveManagedByEmptyState('engine-owned', t)?.title).toBe('Nothing here yet'); + expect(resolveManagedByEmptyState('engine-owned', t, 'sys_automation_run', undefined)?.title).toBe('Nothing here yet'); + }); + // ADR-0103 — a `system` object that opened creation (writable set: Notification // Preferences, delegated RBAC assignments, …) is admin/user-writable data. The // "entries appear automatically" copy would be wrong, so the helper returns diff --git a/packages/app-shell/src/utils/managedByEmptyState.ts b/packages/app-shell/src/utils/managedByEmptyState.ts index 5732d50a9..ae6718847 100644 --- a/packages/app-shell/src/utils/managedByEmptyState.ts +++ b/packages/app-shell/src/utils/managedByEmptyState.ts @@ -43,6 +43,10 @@ export function resolveManagedByEmptyState( userActions?: UserActionsOverride | null, ): ManagedByEmptyState | undefined { switch (managedBy) { + // ADR-0103 — the explicit engine-owned bucket reuses the `system` engine-owned + // empty state ("entries appear automatically"); it never opens creation, so the + // resolveCrudAffordances guard below is a no-op for it. + case 'engine-owned': case 'system': // ADR-0103 — a `system` object that opened creation is admin/user-writable // data (e.g. Notification Preferences). The "entries appear automatically" diff --git a/packages/core/src/utils/managedBy.test.ts b/packages/core/src/utils/managedBy.test.ts index 172119727..d52402549 100644 --- a/packages/core/src/utils/managedBy.test.ts +++ b/packages/core/src/utils/managedBy.test.ts @@ -23,8 +23,8 @@ describe('resolveCrudAffordances (shared source of truth)', () => { }); }); - it('system / append-only / better-auth: export-only by default', () => { - for (const managedBy of ['system', 'append-only', 'better-auth']) { + it('system / engine-owned / append-only / better-auth: export-only by default', () => { + for (const managedBy of ['system', 'engine-owned', 'append-only', 'better-auth']) { expect(resolveCrudAffordances({ managedBy })).toEqual({ create: false, import: false, edit: false, delete: false, exportCsv: true, }); @@ -86,7 +86,7 @@ describe('isSystemWritable (ADR-0103)', () => { describe('isObjectInlineEditable', () => { it('mirrors the resolved edit affordance (replaces the old NON_EDITABLE_BUCKETS set)', () => { // Non-editable buckets by default... - for (const managedBy of ['system', 'append-only', 'better-auth']) { + for (const managedBy of ['system', 'engine-owned', 'append-only', 'better-auth']) { expect(isObjectInlineEditable({ managedBy })).toBe(false); } // ...editable buckets and opened-up system objects. @@ -139,7 +139,7 @@ describe('userActionPredicates', () => { }); describe('MANAGED_BY_BUCKETS', () => { - it('is the closed 5-bucket union in canonical order', () => { - expect(MANAGED_BY_BUCKETS).toEqual(['platform', 'config', 'system', 'append-only', 'better-auth']); + it('is the closed 6-bucket union in canonical order (ADR-0103 engine-owned split)', () => { + expect(MANAGED_BY_BUCKETS).toEqual(['platform', 'config', 'system', 'engine-owned', 'append-only', 'better-auth']); }); }); diff --git a/packages/core/src/utils/managedBy.ts b/packages/core/src/utils/managedBy.ts index 9c29e3869..dacd06baf 100644 --- a/packages/core/src/utils/managedBy.ts +++ b/packages/core/src/utils/managedBy.ts @@ -66,11 +66,12 @@ export interface SchemaLike { } const DEFAULTS: Record = { - platform: { create: true, import: true, edit: true, delete: true, exportCsv: true }, - config: { create: true, import: false, edit: true, delete: true, exportCsv: true }, - system: { create: false, import: false, edit: false, delete: false, exportCsv: true }, - 'append-only': { create: false, import: false, edit: false, delete: false, exportCsv: true }, - 'better-auth': { create: false, import: false, edit: false, delete: false, exportCsv: true }, + platform: { create: true, import: true, edit: true, delete: true, exportCsv: true }, + config: { create: true, import: false, edit: true, delete: true, exportCsv: true }, + system: { create: false, import: false, edit: false, delete: false, exportCsv: true }, + 'engine-owned': { create: false, import: false, edit: false, delete: false, exportCsv: true }, + 'append-only': { create: false, import: false, edit: false, delete: false, exportCsv: true }, + 'better-auth': { create: false, import: false, edit: false, delete: false, exportCsv: true }, }; /** diff --git a/packages/types/src/managed-by.ts b/packages/types/src/managed-by.ts index fd357e481..03be8ca75 100644 --- a/packages/types/src/managed-by.ts +++ b/packages/types/src/managed-by.ts @@ -19,6 +19,7 @@ export type ManagedByBucket = | 'platform' | 'config' | 'system' + | 'engine-owned' | 'append-only' | 'better-auth'; @@ -27,6 +28,7 @@ export const MANAGED_BY_BUCKETS: readonly ManagedByBucket[] = [ 'platform', 'config', 'system', + 'engine-owned', 'append-only', 'better-auth', ];