Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/managedby-engine-owned-bucket.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 22 additions & 4 deletions packages/app-shell/src/components/ManagedByBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -106,6 +108,22 @@ const VARIANTS: Record<VariantKey, Variant> = {
'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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/app-shell/src/utils/managedByEmptyState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/utils/managedBy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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']);
});
});
11 changes: 6 additions & 5 deletions packages/core/src/utils/managedBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ export interface SchemaLike {
}

const DEFAULTS: Record<ManagedByBucket, CrudAffordances> = {
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 },
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/managed-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ManagedByBucket =
| 'platform'
| 'config'
| 'system'
| 'engine-owned'
| 'append-only'
| 'better-auth';

Expand All @@ -27,6 +28,7 @@ export const MANAGED_BY_BUCKETS: readonly ManagedByBucket[] = [
'platform',
'config',
'system',
'engine-owned',
'append-only',
'better-auth',
];
Loading