From a6c41c8ad37fc7fcb435a018a96e14aec53994a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 15:12:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(types,core,app-shell)!:=20follow=20the=20`?= =?UTF-8?q?managedBy:=20'system'`=20=E2=86=92=20`'system-data'`=20retireme?= =?UTF-8?q?nt=20(objectstack#3355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI half of the framework's protocol-17 retirement, landing with it so the closed `ManagedByBucket` union stays a mirror rather than a fork. What this deletes is a derivation. v16's `system` doubled as both the engine-owned default and the writable set, so three surfaces had to RECOVER the distinction from `userActions` at render time — `isSystemWritable`, the badge's synthetic `'system-writable'` variant key, and the empty-state resolver's `create`-affordance probe. The bucket now states it, so all three read the value instead of inferring it, and the badge's variant map is 1:1 with the union again (a new bucket becomes a compile error, not a fallthrough). i18n keys are deliberately unchanged (`system`, `systemWritable`), so no locale bundle moves. BREAKING: `ManagedByBadge`'s `userActions` prop and the `ManagedByUserActions` export are removed — the bucket selects the variant now, and keeping an accepted -but-ignored prop would reproduce, one layer up, the dead-metadata defect this change exists to remove. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 --- .changeset/retire-managed-by-system-bucket.md | 42 +++++++++++ .../src/components/ManagedByBadge.tsx | 74 +++++++------------ .../__tests__/managedByEmptyState.test.ts | 44 +++++++---- .../src/utils/managedByEmptyState.ts | 19 ++--- packages/app-shell/src/views/ObjectView.tsx | 2 +- ...ecordDetailView.headerActionGates.test.tsx | 2 +- .../app-shell/src/views/RecordDetailView.tsx | 2 +- .../app-shell/src/views/RecordFormPage.tsx | 1 - packages/core/src/utils/managedBy.test.ts | 37 ++++++---- packages/core/src/utils/managedBy.ts | 24 ++++-- .../record-details.effectiveOps.test.tsx | 2 +- packages/types/src/field-types.ts | 9 ++- packages/types/src/managed-by.ts | 13 +++- 13 files changed, 169 insertions(+), 102 deletions(-) create mode 100644 .changeset/retire-managed-by-system-bucket.md diff --git a/.changeset/retire-managed-by-system-bucket.md b/.changeset/retire-managed-by-system-bucket.md new file mode 100644 index 0000000000..d6c22ef048 --- /dev/null +++ b/.changeset/retire-managed-by-system-bucket.md @@ -0,0 +1,42 @@ +--- +"@object-ui/types": minor +"@object-ui/core": minor +"@object-ui/app-shell": minor +--- + +feat!: follow the framework's `managedBy: 'system'` → `'system-data'` retirement (objectstack#3355) + +**FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** The framework +retired the residual `system` bucket in protocol 17; this is the UI half of that +change, landing with it so the closed `ManagedByBucket` union stays a mirror +rather than a fork. + +ADR-0103 split the overloaded `system` bucket additively in v16 — the +engine-owned objects moved to the explicit `engine-owned`, the admin/user-writable +ones stayed on `system` — which left that value named after the half that had +already moved out. `system-data` names what it actually holds: the SCHEMA is the +platform's, the DATA is the admin's or the user's. + +**The derivation this deletes is the point.** Because v16's `system` doubled as +both the engine-owned default and the writable set, three UI surfaces had to +RECOVER the distinction from `userActions` at render time: + +- `isSystemWritable()` probed `userActions` for any opted-in write. It is now + `managedBy === 'system-data'` — the bucket answers directly. +- `ManagedByBadge` derived a synthetic `'system-writable'` variant key. The + variant map is now 1:1 with the bucket union, so a new bucket is a compile + error to miss instead of a silent fallthrough. The `systemWritable` / + `system` i18n keys are **unchanged**, so no locale bundle moves. +- `resolveManagedByEmptyState()` asked the resolved `create` affordance whether a + `system` list should read "entries appear automatically" or show the New + button. `system-data` now falls through to the generic empty state by + definition; `engine-owned` keeps the automatic-entries copy. + +**Breaking (UI API):** `ManagedByBadge`'s `userActions` prop and the exported +`ManagedByUserActions` interface are **removed**. The bucket alone selects the +variant now, so the prop had become metadata nothing read — the exact defect the +framework change exists to remove; shipping it as an accepted-but-ignored prop +would have reproduced it one layer up. Drop the prop from call sites; no other +change is needed. + +`MANAGED_BY_BUCKETS` and `ManagedByBucket` no longer contain `'system'`. diff --git a/packages/app-shell/src/components/ManagedByBadge.tsx b/packages/app-shell/src/components/ManagedByBadge.tsx index f8d3d88c93..6e33ce4e8f 100644 --- a/packages/app-shell/src/components/ManagedByBadge.tsx +++ b/packages/app-shell/src/components/ManagedByBadge.tsx @@ -8,7 +8,7 @@ import { cn, } from '@object-ui/components'; import { useObjectTranslation } from '@object-ui/i18n'; -import { isSystemWritable, type ManagedByBucket } from '../utils/crudAffordances'; +import { type ManagedByBucket } from '../utils/crudAffordances'; /** * ManagedByBadge — replaces the verbose, full-width `ManagedByBanner` with @@ -28,12 +28,11 @@ 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 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. + * - `system-data` — Platform-defined schema whose DATA is admin/user-writable + * (objectstack#3355; the writable half of the old `system`). * - `engine-owned` — Runtime rows a platform service owns end to end (ADR-0103); - * the explicit read-only monitoring surface. + * the explicit read-only monitoring surface, and the + * engine-owned half of the old `system`. * - `append-only` — Immutable audit log. * - `better-auth` — Identity tables owned by better-auth driver. * @@ -42,32 +41,15 @@ import { isSystemWritable, type ManagedByBucket } from '../utils/crudAffordances * users understand at a glance why those affordances are missing — the * detailed explanation lives in the tooltip. * - * Empty-state guidance for `system`-bucket lists is rendered separately by + * Empty-state guidance for the read-only buckets is rendered separately by * `ObjectView` via `resolveManagedByEmptyState()`. */ type Bucket = ManagedByBucket; -/** - * Subset of `userActions` (ADR-0103) the badge needs to tell an engine-owned - * `system` object apart from an admin/user-writable one. Mirrors the shared - * `resolveEffectiveCrudAffordances` inputs; `edit`/`delete` accept the #2614 object form. - */ -export interface ManagedByUserActions { - create?: boolean; - edit?: boolean | { enabled?: boolean }; - delete?: boolean | { enabled?: boolean }; -} - export interface ManagedByBadgeProps { /** The `managedBy` flag from the object schema. */ managedBy?: string; - /** - * The object's `userActions` (ADR-0103). When a `system`-bucket object opens - * any write here, the badge switches from the engine-owned "read-only - * monitoring surface" copy to the admin/user-writable variant. - */ - userActions?: ManagedByUserActions | null; /** Optional override for the human-readable system name shown in the tooltip. */ label?: string; /** Optional extra classes. */ @@ -86,8 +68,13 @@ interface Variant { tone: string; } -/** Variant keys: the non-platform buckets plus the ADR-0103 writable-system split. */ -type VariantKey = Exclude | 'system-writable'; +/** + * Variant keys: every non-platform bucket. The extra `'system-writable'` key + * ADR-0103 needed is gone — objectstack#3355 made the writable half a real + * bucket (`system-data`), so the variant map is once again 1:1 with the union + * and a new bucket is a compile error to miss. + */ +type VariantKey = Exclude; const VARIANTS: Record = { config: { @@ -99,22 +86,11 @@ const VARIANTS: Record = { 'These rows define how the platform behaves at runtime. Author them here; the runtime data they produce lives in a separate table.', tone: 'border-sky-300/60 bg-sky-50 text-sky-900 hover:bg-sky-100 dark:border-sky-500/40 dark:bg-sky-950/40 dark:text-sky-100', }, - system: { - 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 — 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.) + // Keeps the `system` i18n KEY (not the retired bucket value): the copy is + // unchanged and every locale bundle already carries it, so objectstack#3355 + // costs zero translation churn. The vocabulary fix is at the schema level. 'engine-owned': { icon: Lock, i18nKey: 'system', @@ -124,12 +100,12 @@ 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 — 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 - // bucket is `system` and any write is opted in, so the copy no longer claims a - // "read-only monitoring surface". - 'system-writable': { + // objectstack#3355 — platform-defined schema, admin/user-writable DATA (e.g. + // Notification Preferences, delegated RBAC assignments). Under ADR-0103 this + // was a synthetic `'system-writable'` variant the component had to DERIVE from + // `userActions`; it is now the plain `system-data` bucket. The `systemWritable` + // i18n key is kept so no locale bundle has to change. + 'system-data': { icon: Settings2, i18nKey: 'systemWritable', short: 'Platform schema', @@ -158,13 +134,13 @@ const VARIANTS: Record = { }, }; -export function ManagedByBadge({ managedBy, userActions, label, className }: ManagedByBadgeProps) { +export function ManagedByBadge({ managedBy, label, className }: ManagedByBadgeProps) { const { t } = useObjectTranslation(); if (!managedBy || managedBy === 'platform') return null; // ADR-0103 — a `system` object that opened any write is admin/user-writable // data, not an engine-owned monitoring surface: pick the writable variant/copy. - const systemWritable = isSystemWritable({ managedBy, userActions }); - const variantKey: VariantKey = systemWritable ? 'system-writable' : (managedBy as VariantKey); + // objectstack#3355 — the bucket IS the variant now; nothing to derive. + const variantKey = managedBy as VariantKey; const variant = VARIANTS[variantKey]; if (!variant) return null; const Icon = variant.icon; diff --git a/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts b/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts index 8462c2b477..f82c376903 100644 --- a/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts +++ b/packages/app-shell/src/utils/__tests__/managedByEmptyState.test.ts @@ -15,28 +15,42 @@ describe('resolveManagedByEmptyState', () => { expect(resolveManagedByEmptyState('nope', t)).toBeUndefined(); }); - it('leaves the system / append-only buckets intact', () => { - expect(resolveManagedByEmptyState('system', t)?.title).toBe('Nothing here yet'); + it('leaves the engine-owned / append-only buckets intact', () => { + expect(resolveManagedByEmptyState('engine-owned', t)?.title).toBe('Nothing here yet'); 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', () => { + // ADR-0103 — rows a platform service owns end to end; it never opens creation, + // so it always renders the "entries appear automatically" copy. + it('gives the engine-owned bucket the engine-owned empty state', () => { 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 - // undefined and the caller falls back to the generic empty state (New button). - it('returns undefined for a system object whose userActions opened creation', () => { - expect(resolveManagedByEmptyState('system', t, 'sys_notification_preference', { create: true })).toBeUndefined(); - // A system object that did NOT open creation stays engine-owned. - expect(resolveManagedByEmptyState('system', t, 'sys_automation_run', {})?.title).toBe('Nothing here yet'); - expect(resolveManagedByEmptyState('system', t, 'sys_automation_run', undefined)?.title).toBe('Nothing here yet'); - // append-only is unaffected by userActions.create (audit logs stay locked). + /** + * objectstack#3355 — the writable half is its own bucket now. + * + * Under ADR-0103 this helper had to ASK `userActions` whether a `system` list + * should get the "entries appear automatically" copy or the generic + * New-button empty state. `system-data` answers it by name: it is + * admin/user-writable data, so it falls through to `default` and the caller + * renders the generic empty state — no probe, no derivation. + */ + it('returns undefined for `system-data` — the generic New-button empty state', () => { + expect(resolveManagedByEmptyState('system-data', t, 'sys_notification_preference')).toBeUndefined(); + // …and stays undefined however `userActions` narrows, since the bucket default is full CRUD. + expect(resolveManagedByEmptyState('system-data', t, 'sys_notification_preference', { create: true })).toBeUndefined(); + expect(resolveManagedByEmptyState('system-data', t, 'sys_user_position', {})).toBeUndefined(); + }); + + it('gives the retired `system` value no special-casing at all', () => { + // It falls through to `default` like any unknown bucket — the retired value + // must not keep steering UI copy after objectstack#3355. + expect(resolveManagedByEmptyState('system', t)).toBeUndefined(); + expect(resolveManagedByEmptyState('system', t, 'sys_automation_run', {})).toBeUndefined(); + }); + + it('append-only is unaffected by userActions.create (audit logs stay locked)', () => { expect(resolveManagedByEmptyState('append-only', t, 'sys_audit_log', { create: true })?.title).toBe('No events recorded'); }); diff --git a/packages/app-shell/src/utils/managedByEmptyState.ts b/packages/app-shell/src/utils/managedByEmptyState.ts index ac8d8c4bff..54e4c1f41d 100644 --- a/packages/app-shell/src/utils/managedByEmptyState.ts +++ b/packages/app-shell/src/utils/managedByEmptyState.ts @@ -43,16 +43,17 @@ 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 - // resolveEffectiveCrudAffordances guard below is a no-op for it. + // ADR-0103 — rows a platform service owns end to end: "entries appear + // automatically" is the whole story. It keeps the `system` i18n KEY (the copy + // is unchanged and every locale bundle carries it), not the retired bucket + // VALUE — objectstack#3355 renamed the writable half to `system-data`, which + // now falls through to `default` and gets the generic empty state with its + // New button, exactly as its full-CRUD bucket default implies. 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" - // copy would be wrong; fall back to the generic empty state (which surfaces - // the New button) by returning undefined. The resolved `create` affordance - // (shared @object-ui/core policy) is the one place that reads the override. + // An `engine-owned` object that nonetheless opens creation via `userActions` + // would make the "appear automatically" copy wrong; fall back to the generic + // empty state. The resolved `create` affordance (shared @object-ui/core + // policy) is the one place that reads the override. if (resolveEffectiveCrudAffordances({ managedBy, userActions }).create) return undefined; return { icon: 'Lock', diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index 9518188e57..de53baab9d 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -1682,7 +1682,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an title={ {objectLabel(objectDef)} - + } description={objectDef.description ? objectDesc(objectDef) : undefined} diff --git a/packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx b/packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx index f6e6db9a37..86c8209dfb 100644 --- a/packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx +++ b/packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx @@ -85,7 +85,7 @@ describe('resolveRecordHeaderActionGates — effective API operations (#3546)', // server set must NOT re-open them — the server governs what it will // accept, the bucket governs what the product offers. expect( - resolveRecordHeaderActionGates({ name: 'sys_automation_run', managedBy: 'system' }, [ + resolveRecordHeaderActionGates({ name: 'sys_automation_run', managedBy: 'engine-owned' }, [ 'get', 'list', 'create', diff --git a/packages/app-shell/src/views/RecordDetailView.tsx b/packages/app-shell/src/views/RecordDetailView.tsx index d5fe16085c..f723fce8de 100644 --- a/packages/app-shell/src/views/RecordDetailView.tsx +++ b/packages/app-shell/src/views/RecordDetailView.tsx @@ -2041,7 +2041,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri {recordPresence.length > 0 && ( )} - + diff --git a/packages/core/src/utils/managedBy.test.ts b/packages/core/src/utils/managedBy.test.ts index 1af418c8f0..7d83578475 100644 --- a/packages/core/src/utils/managedBy.test.ts +++ b/packages/core/src/utils/managedBy.test.ts @@ -24,7 +24,7 @@ describe('resolveEffectiveCrudAffordances — bucket half, delegated to the spec }); it('system / engine-owned / append-only / better-auth: export-only by default', () => { - for (const managedBy of ['system', 'engine-owned', 'append-only', 'better-auth']) { + for (const managedBy of ['engine-owned', 'append-only', 'better-auth']) { expect(resolveEffectiveCrudAffordances({ managedBy })).toEqual({ create: false, import: false, edit: false, delete: false, exportCsv: true, }); @@ -66,15 +66,22 @@ describe('isWriteOptedIn', () => { }); }); -describe('isSystemWritable (ADR-0103)', () => { - it('true only for a system object that opened create, edit, or delete', () => { - expect(isSystemWritable({ managedBy: 'system', userActions: { create: true } })).toBe(true); - expect(isSystemWritable({ managedBy: 'system', userActions: { edit: { enabled: true } } })).toBe(true); - expect(isSystemWritable({ managedBy: 'system', userActions: { delete: true } })).toBe(true); - }); - it('false for engine-owned system and for other buckets even with userActions', () => { - expect(isSystemWritable({ managedBy: 'system' })).toBe(false); - expect(isSystemWritable({ managedBy: 'system', userActions: { edit: false } })).toBe(false); +describe('isSystemWritable (ADR-0103, simplified by objectstack#3355)', () => { + it('true for the `system-data` bucket, with or without userActions', () => { + // v17 pin: the BUCKET answers this now. Under ADR-0103 the writable half had + // to be recovered from `userActions` because `system` doubled as the + // engine-owned default; `system-data` states it outright, so a bare + // declaration — the shape all 8 platform objects now use — must be true. + expect(isSystemWritable({ managedBy: 'system-data' })).toBe(true); + expect(isSystemWritable({ managedBy: 'system-data', userActions: { create: true } })).toBe(true); + // …and a NARROW is still platform-schema/user-data, so still true. + expect(isSystemWritable({ managedBy: 'system-data', userActions: { create: false } })).toBe(true); + }); + it('false for the retired `system` value and for every other bucket', () => { + // The retired value must not keep working through this helper — it is exactly + // the silent-absorption path objectstack#3355 removed from the load path. + expect(isSystemWritable({ managedBy: 'system', userActions: { create: true } } as never)).toBe(false); + expect(isSystemWritable({ managedBy: 'engine-owned' })).toBe(false); // append-only / better-auth are never "system-writable" regardless of userActions expect(isSystemWritable({ managedBy: 'append-only', userActions: { create: true } })).toBe(false); expect(isSystemWritable({ managedBy: 'better-auth', userActions: { edit: true } })).toBe(false); @@ -86,7 +93,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', 'engine-owned', 'append-only', 'better-auth']) { + for (const managedBy of ['engine-owned', 'append-only', 'better-auth']) { expect(isObjectInlineEditable({ managedBy })).toBe(false); } // ...editable buckets and opened-up system objects. @@ -139,8 +146,12 @@ describe('userActionPredicates', () => { }); describe('MANAGED_BY_BUCKETS', () => { - 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']); + it('is the closed 6-bucket union in canonical order (objectstack#3355 — `system` → `system-data`)', () => { + expect(MANAGED_BY_BUCKETS).toEqual(['platform', 'config', 'system-data', 'engine-owned', 'append-only', 'better-auth']); + }); + + it('no longer carries the retired `system` value', () => { + expect(MANAGED_BY_BUCKETS).not.toContain('system'); }); }); diff --git a/packages/core/src/utils/managedBy.ts b/packages/core/src/utils/managedBy.ts index 607cbf310e..cefece2e71 100644 --- a/packages/core/src/utils/managedBy.ts +++ b/packages/core/src/utils/managedBy.ts @@ -183,14 +183,26 @@ export function isWriteOptedIn(v: UserActionOverride | undefined | null): boolea } /** - * ADR-0103 — a `system`-bucket object that opened ANY write via `userActions` - * is admin/user-writable DATA, not an engine-owned monitoring surface. Used to - * pick the writable-system badge/empty-state copy. + * True for a platform-defined schema whose DATA is admin/user-writable — the + * `system-data` bucket. Used to pick the writable-system badge/empty-state copy + * instead of the engine-owned "read-only monitoring surface" copy. + * + * **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split + * this had to RECOVER the distinction from `userActions`: `system` doubled as + * both the engine-owned default and the writable set, so "did it open any + * write?" was the only way to tell a Notification Preferences grid from an + * automation-run log. v17 retired that overload — the writable half is now the + * `system-data` bucket (writable by DEFAULT) and the engine-owned half is + * `engine-owned` — so the bucket answers the question directly and the + * `userActions` probe would only re-derive what the value already states. + * + * The narrowing case is deliberately still `true`: a `system-data` grid that + * narrows to edit-only is still platform-schema/user-data and should read as + * such. The spec refuses `system-data` on an object that narrows away every + * write, so there is no "writable bucket with no writes" shape to worry about. */ export function isSystemWritable(obj: SchemaLike | null | undefined): boolean { - if (obj?.managedBy !== 'system') return false; - const o = obj?.userActions ?? {}; - return o.create === true || isWriteOptedIn(o.edit) || isWriteOptedIn(o.delete); + return obj?.managedBy === 'system-data'; } /** diff --git a/packages/plugin-detail/src/renderers/__tests__/record-details.effectiveOps.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-details.effectiveOps.test.tsx index f1483a2d0b..51dd441665 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-details.effectiveOps.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-details.effectiveOps.test.tsx @@ -109,7 +109,7 @@ describe('record:details — inline-edit vs effective API operations (#3546)', ( it('bucket-locked object stays locked even when the server allows `update`', () => { // Intersection, never union: an engine-owned object is not user-editable // regardless of what the caller's effective set permits. - stub.objectSchema = { managedBy: 'system' }; + stub.objectSchema = { managedBy: 'engine-owned' }; stub.effectiveOps = ['get', 'list', 'create', 'update', 'delete']; expect(inlineEditFor()).toBe(false); }); diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index f24f98d6b1..692468179b 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -894,9 +894,12 @@ export interface ObjectSchemaMetadata { * * - `'platform'` (default) — ObjectStack-owned business data; full CRUD. * - `'config'` — admin-authored configuration; New / Edit / Delete, no import. - * - `'system'` — platform-defined schema; engine-owned (read-only) by - * default, unless it declares `userActions` opening writes (the - * admin/user-writable set: RBAC links, prefs, messaging config). + * - `'system-data'` — platform-defined schema holding admin/user-writable + * DATA (RBAC links, prefs, messaging config); full CRUD by default, + * `userActions` NARROWS. Renamed from the residual `'system'` in protocol + * 17 (objectstack#3355). + * - `'engine-owned'` — runtime rows a platform service owns end to end; no + * user writes. * - `'append-only'` — immutable audit trail; View + Export only. * - `'better-auth'` — identity tables owned by the auth driver; generic * user-context writes are suppressed (they bypass password hashing, diff --git a/packages/types/src/managed-by.ts b/packages/types/src/managed-by.ts index 03be8ca75e..da458343c7 100644 --- a/packages/types/src/managed-by.ts +++ b/packages/types/src/managed-by.ts @@ -11,6 +11,15 @@ * lets the schema type reference it and prevents the hand-mirrored bucket lists * that previously drifted. * + * `system-data` replaced the residual `system` in protocol 17 + * (objectstack#3355). The v16 split was additive — the engine-owned objects + * moved out to `engine-owned` and the admin/user-writable ones kept `system`, + * leaving that value named after the half that had left. `system-data` names + * both boundaries it actually holds: the SCHEMA is the platform's (versus + * `platform`, which is tenant-modelled), the DATA is the admin's or the user's + * (versus `engine-owned`, where the engine owns both). It is the only bucket + * besides `platform`/`config` that defaults to writable. + * * NOTE: distinct from the permission-set / metadata-record *provenance* * `managedBy` (`'platform' | 'package' | 'admin'`), which is an unrelated axis * that happens to share the word. @@ -18,7 +27,7 @@ export type ManagedByBucket = | 'platform' | 'config' - | 'system' + | 'system-data' | 'engine-owned' | 'append-only' | 'better-auth'; @@ -27,7 +36,7 @@ export type ManagedByBucket = export const MANAGED_BY_BUCKETS: readonly ManagedByBucket[] = [ 'platform', 'config', - 'system', + 'system-data', 'engine-owned', 'append-only', 'better-auth',