diff --git a/.changeset/retire-compactlayout-alias.md b/.changeset/retire-compactlayout-alias.md new file mode 100644 index 0000000000..36db9ffa27 --- /dev/null +++ b/.changeset/retire-compactlayout-alias.md @@ -0,0 +1,22 @@ +--- +'@objectstack/spec': minor +'@objectstack/plugin-audit': patch +'@objectstack/plugin-approvals': patch +'@objectstack/plugin-security': patch +'@objectstack/plugin-sharing': patch +'@objectstack/plugin-webhooks': patch +'@objectstack/service-storage': patch +'@objectstack/service-automation': patch +'@objectstack/service-messaging': patch +'@objectstack/service-realtime': patch +--- + +Retire the deprecated `compactLayout` alias for `highlightFields` (framework#2536, closes the ADR-0085 deprecation window). + +- `ObjectSchema` no longer declares `compactLayout`: `create()` rejects it like any unknown key; lenient `parse()` strips it (no silent aliasing). +- The parse-time alias AND the `highlightFields → compactLayout` back-fill transition mirror are removed from `normalizeSemanticRoleAliases`. Served metadata now carries the canonical key only. +- All remaining first-party authors (27 system objects across plugin-audit / approvals / security / sharing / webhooks / service-storage / automation / messaging / realtime — missed by the #2521 sweep, caught by the type gate) renamed to `highlightFields`. +- The downstream smoke pin moves to hotcrm v1.2.2 (hotcrm#424: same rename + deps ^11.7.0). +- Consumers were switched in objectui#2168 and shipped via the console pin bump (#2526); this closes the window scheduled there. The dogfood mirror assertion (#2528) flips to `compactLayout: undefined` in this same change, per the plan it carried. + +Version note: minor, not major — the key was deprecated-with-alias for a full release window, all first-party consumers/authors are migrated, and the spec api-surface gate reports no export changes (same documented-exception path as the ADR-0085 removals in 11.7.0). External metadata still authoring `compactLayout` will now fail `create()` loudly with the standard unknown-key error naming the key. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 400b381b24..5e742b00f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: # ADR-0085 (hotcrm#423) — the intended break, explicitly digested # downstream. Bump this ref whenever a deliberate spec surface # removal ships a matching hotcrm release. - HOTCRM_REF: v1.2.1 + HOTCRM_REF: v1.2.2 run: bash scripts/downstream-smoke.sh - name: Create Release Pull Request or Publish to npm diff --git a/examples/app-showcase/src/objects/semantic-zoo.object.ts b/examples/app-showcase/src/objects/semantic-zoo.object.ts index fc245ea036..ac56728afd 100644 --- a/examples/app-showcase/src/objects/semantic-zoo.object.ts +++ b/examples/app-showcase/src/objects/semantic-zoo.object.ts @@ -9,14 +9,14 @@ * strips nor mangles them. Guarded by * `packages/dogfood/test/semantic-roles.dogfood.test.ts`. * - * Two objects because the alias runs both directions: - * - `SemanticZoo` authors the CANONICAL spellings (highlightFields, - * stageField: 'status', collapse enum) — served meta must also carry the - * deprecated `compactLayout` mirror for pre-11.7 renderers. - * - `SemanticZooLegacy` authors the DEPRECATED spelling (compactLayout) and - * `stageField: false` — served meta must carry the canonical - * `highlightFields`, and `false` must survive (it is the only "stop - * guessing" signal; a falsy-check regression turns the stepper back on). + * Two objects, two role postures: + * - `SemanticZoo` authors the full canonical role set (highlightFields, + * stageField: 'status', collapse enum). + * - `SemanticZooLegacy` carries `stageField: false` — `false` must survive + * serialization strictly (it is the only "stop guessing" signal; a + * falsy-check regression turns the stepper back on). It exercised the + * deprecated `compactLayout` alias during the ADR-0085 window; the alias + * was retired by framework#2536. */ import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -55,7 +55,7 @@ export const SemanticZooLegacy = ObjectSchema.create({ label: 'Semantic Zoo (Legacy)', pluralLabel: 'Semantic Zoo Legacies', icon: 'flask-round', - description: 'ADR-0085 semantic-role runtime fixture (deprecated spellings)', + description: 'ADR-0085 semantic-role runtime fixture (stageField:false suppression)', fields: { name: Field.text({ label: 'Name', required: true }), @@ -71,8 +71,9 @@ export const SemanticZooLegacy = ObjectSchema.create({ amount: Field.number({ label: 'Amount' }), }, - // Deprecated alias on purpose — must surface as highlightFields when served. - compactLayout: ['name', 'amount'], + // (This fixture authored the deprecated `compactLayout` spelling during the + // ADR-0085 alias window; retired by framework#2536.) + highlightFields: ['name', 'amount'], // This status is a color, not a lifecycle. stageField: false, }); diff --git a/packages/dogfood/test/semantic-roles.dogfood.test.ts b/packages/dogfood/test/semantic-roles.dogfood.test.ts index 03e9a9f49f..b8be25bdb8 100644 --- a/packages/dogfood/test/semantic-roles.dogfood.test.ts +++ b/packages/dogfood/test/semantic-roles.dogfood.test.ts @@ -46,11 +46,10 @@ describe('ADR-0085 semantic roles survive the served pipeline', () => { const def = await servedObject('showcase_semantic_zoo'); expect(def.highlightFields).toEqual(['name', 'status', 'amount']); - // Transition mirror for pre-11.7 renderers (ObjectGrid & friends read the - // old spelling). Deliberately asserted: when the deprecated alias is - // retired, this expectation is DELETED IN THE SAME PR — a red here after - // that removal means the retirement missed a consumer. - expect(def.compactLayout).toEqual(['name', 'status', 'amount']); + // Alias retired (framework#2536): served meta carries the canonical key + // ONLY. This flip happened in the same PR that removed the transition + // mirror, per the plan the mirror assertion carried. + expect(def.compactLayout).toBeUndefined(); expect(def.stageField).toBe('status'); @@ -62,13 +61,11 @@ describe('ADR-0085 semantic roles survive the served pipeline', () => { expect(def.fields?.amount?.group).toBe('money'); }); - it('deprecated spellings alias onto the canonical keys when served', async () => { + it('the legacy fixture serves canonical highlightFields only (alias retired)', async () => { const def = await servedObject('showcase_semantic_zoo_legacy'); - // compactLayout (deprecated) must surface as highlightFields… expect(def.highlightFields).toEqual(['name', 'amount']); - // …and stay readable under the old name (ADR-0079 alias pattern). - expect(def.compactLayout).toEqual(['name', 'amount']); + expect(def.compactLayout).toBeUndefined(); }); it('stageField:false survives serialization as a strict false', async () => { diff --git a/packages/plugins/plugin-approvals/src/sys-approval-action.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-action.object.ts index 4d7e9399d1..dbc711de3d 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-action.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-action.object.ts @@ -24,7 +24,7 @@ export const SysApprovalAction = ObjectSchema.create({ displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{action} · {step_name}', - compactLayout: ['request_id', 'step_name', 'action', 'actor_id', 'created_at'], + highlightFields: ['request_id', 'step_name', 'action', 'actor_id', 'created_at'], listViews: { recent: { diff --git a/packages/plugins/plugin-approvals/src/sys-approval-approver.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-approver.object.ts index 01275610ef..0ed2ea51ae 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-approver.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-approver.object.ts @@ -35,7 +35,7 @@ export const SysApprovalApprover = ObjectSchema.create({ displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{approver} · {request_id}', - compactLayout: ['request_id', 'approver', 'created_at'], + highlightFields: ['request_id', 'approver', 'created_at'], fields: { id: Field.text({ label: 'Row ID', required: true, readonly: true, group: 'System' }), diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts index 1a8fb74c85..2aae9109c0 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts @@ -33,7 +33,7 @@ export const SysApprovalRequest = ObjectSchema.create({ displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{process_name} · {record_id}', - compactLayout: ['process_name', 'object_name', 'record_id', 'status', 'current_step', 'submitter_id', 'updated_at'], + highlightFields: ['process_name', 'object_name', 'record_id', 'status', 'current_step', 'submitter_id', 'updated_at'], // Curated built-in list views — render as segmented tabs in the console. // Filters use {current_user_id} substitution wired by the console. diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index ab164f60d9..85923ae51f 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -27,7 +27,7 @@ export const SysActivity = ObjectSchema.create({ displayNameField: 'summary', nameField: 'summary', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{type} · {summary}', - compactLayout: ['timestamp', 'type', 'actor_name', 'summary'], + highlightFields: ['timestamp', 'type', 'actor_name', 'summary'], fields: { id: Field.text({ diff --git a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts index 83c23c2e82..9820717d83 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts @@ -24,7 +24,7 @@ export const SysAuditLog = ObjectSchema.create({ displayNameField: 'action', nameField: 'action', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{action} · {object_name}', - compactLayout: ['created_at', 'action', 'object_name', 'record_id', 'user_id'], + highlightFields: ['created_at', 'action', 'object_name', 'record_id', 'user_id'], listViews: { recent: { diff --git a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts index bfdd0d5947..28aaf25a54 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts @@ -28,7 +28,7 @@ export const SysComment = ObjectSchema.create({ displayNameField: 'body', nameField: 'body', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{author_name}: {body}', - compactLayout: ['created_at', 'author_name', 'body'], + highlightFields: ['created_at', 'author_name', 'body'], fields: { id: Field.text({ diff --git a/packages/plugins/plugin-security/src/objects/sys-capability.object.ts b/packages/plugins/plugin-security/src/objects/sys-capability.object.ts index 0fa21fe9a6..764a2166de 100644 --- a/packages/plugins/plugin-security/src/objects/sys-capability.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-capability.object.ts @@ -41,7 +41,7 @@ export const SysCapability = ObjectSchema.create({ displayNameField: 'label', nameField: 'label', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label}', - compactLayout: ['label', 'name', 'scope', 'managed_by', 'active'], + highlightFields: ['label', 'name', 'scope', 'managed_by', 'active'], actions: [ { diff --git a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts index 0fba1f35df..0fe676e84e 100644 --- a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts @@ -29,7 +29,7 @@ export const SysPermissionSet = ObjectSchema.create({ displayNameField: 'label', nameField: 'label', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label}', - compactLayout: ['label', 'name', 'active'], + highlightFields: ['label', 'name', 'active'], // Custom actions — permission sets are templates assigned to roles or // users (via sys_role_permission_set / sys_user_permission_set). The diff --git a/packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts index 620dfb4bbf..bea408dfad 100644 --- a/packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts @@ -22,7 +22,7 @@ export const SysRolePermissionSet = ObjectSchema.create({ managedBy: 'system', description: 'Binds a permission set to a role.', titleFormat: '{role_id} → {permission_set_id}', - compactLayout: ['role_id', 'permission_set_id'], + highlightFields: ['role_id', 'permission_set_id'], fields: { id: Field.text({ diff --git a/packages/plugins/plugin-security/src/objects/sys-role.object.ts b/packages/plugins/plugin-security/src/objects/sys-role.object.ts index 01759f8002..fb9d55a5f2 100644 --- a/packages/plugins/plugin-security/src/objects/sys-role.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-role.object.ts @@ -32,7 +32,7 @@ export const SysRole = ObjectSchema.create({ displayNameField: 'label', nameField: 'label', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label}', - compactLayout: ['label', 'name', 'active', 'is_default'], + highlightFields: ['label', 'name', 'active', 'is_default'], // Custom actions — system roles drive RBAC and are edited rarely but // require the four high-frequency sysadmin affordances every IdP diff --git a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts index f8db9594c7..5a4edaf1d6 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts @@ -26,7 +26,7 @@ export const SysUserPermissionSet = ObjectSchema.create({ managedBy: 'system', description: 'Direct assignment of a permission set to a user (optionally scoped to an organization).', titleFormat: '{user_id} → {permission_set_id}', - compactLayout: ['user_id', 'permission_set_id', 'organization_id'], + highlightFields: ['user_id', 'permission_set_id', 'organization_id'], fields: { id: Field.text({ diff --git a/packages/plugins/plugin-security/src/objects/sys-user-role.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-role.object.ts index f282a0ebe5..1503647a80 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-role.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-role.object.ts @@ -27,7 +27,7 @@ export const SysUserRole = ObjectSchema.create({ managedBy: 'system', description: 'Assigns an RBAC role (sys_role.name) to a user. Platform-owned (ADR-0057 D4).', titleFormat: '{user_id} → {role}', - compactLayout: ['user_id', 'role', 'organization_id'], + highlightFields: ['user_id', 'role', 'organization_id'], fields: { id: Field.text({ diff --git a/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts index 3784e56932..c771bb3a3d 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts @@ -39,7 +39,7 @@ export const SysRecordShare = ObjectSchema.create({ managedBy: 'system', description: 'Per-record sharing grant — extends OWD with explicit access', titleFormat: '{object_name}/{record_id} → {recipient_id} ({access_level})', - compactLayout: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source'], + highlightFields: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source'], listViews: { granted_to_me: { diff --git a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts index d16ff4a9d0..07e2633802 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts @@ -49,7 +49,7 @@ export const SysShareLink = ObjectSchema.create({ managedBy: 'system', description: 'Opaque capability token granting access to a single record. Notion/Figma-style public link sharing.', titleFormat: '{object_name}/{record_id} ({permission})', - compactLayout: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'revoked_at'], + highlightFields: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'revoked_at'], listViews: { active_links: { diff --git a/packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts index fea3661178..b1f38f81f2 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts @@ -41,7 +41,7 @@ export const SysSharingRule = ObjectSchema.create({ displayNameField: 'name', nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label}', - compactLayout: ['name', 'object_name', 'recipient_type', 'recipient_id', 'access_level', 'active'], + highlightFields: ['name', 'object_name', 'recipient_type', 'recipient_id', 'access_level', 'active'], listViews: { active: { diff --git a/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts b/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts index 45ef97770b..0a07e1f2fe 100644 --- a/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts +++ b/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts @@ -47,7 +47,7 @@ export const SysWebhook = ObjectSchema.create({ displayNameField: 'name', nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label}', - compactLayout: ['name', 'object_name', 'url', 'active', 'updated_at'], + highlightFields: ['name', 'object_name', 'url', 'active', 'updated_at'], listViews: { active: { diff --git a/packages/services/service-automation/src/sys-automation-run.object.ts b/packages/services/service-automation/src/sys-automation-run.object.ts index e4703fa566..ed859e5ca3 100644 --- a/packages/services/service-automation/src/sys-automation-run.object.ts +++ b/packages/services/service-automation/src/sys-automation-run.object.ts @@ -39,7 +39,7 @@ export const SysAutomationRun = ObjectSchema.create({ displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{flow_name} · {node_id}', - compactLayout: ['flow_name', 'node_id', 'status', 'correlation', 'started_at', 'updated_at'], + highlightFields: ['flow_name', 'node_id', 'status', 'correlation', 'started_at', 'updated_at'], fields: { id: Field.text({ label: 'Run ID', required: true, readonly: true, group: 'System' }), diff --git a/packages/services/service-messaging/src/objects/http-delivery.object.ts b/packages/services/service-messaging/src/objects/http-delivery.object.ts index 18b2e52541..648c4b1dd4 100644 --- a/packages/services/service-messaging/src/objects/http-delivery.object.ts +++ b/packages/services/service-messaging/src/objects/http-delivery.object.ts @@ -38,7 +38,7 @@ export const HttpDelivery = ObjectSchema.create({ displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{label} → {url}', - compactLayout: ['source', 'url', 'status', 'attempts', 'next_retry_at'], + highlightFields: ['source', 'url', 'status', 'attempts', 'next_retry_at'], listViews: { recent: { diff --git a/packages/services/service-messaging/src/objects/inbox-message.object.ts b/packages/services/service-messaging/src/objects/inbox-message.object.ts index 5046468cd4..dc780be9f2 100644 --- a/packages/services/service-messaging/src/objects/inbox-message.object.ts +++ b/packages/services/service-messaging/src/objects/inbox-message.object.ts @@ -25,7 +25,7 @@ export const InboxMessage = ObjectSchema.create({ description: 'User-facing in-app notification rows materialized by the inbox messaging channel.', nameField: 'title', // [ADR-0079] canonical primary-title pointer (single-field titleFormat) titleFormat: '{title}', - compactLayout: ['title', 'user_id', 'severity', 'created_at'], + highlightFields: ['title', 'user_id', 'severity', 'created_at'], listViews: { mine: { diff --git a/packages/services/service-messaging/src/objects/notification-delivery.object.ts b/packages/services/service-messaging/src/objects/notification-delivery.object.ts index d3aec2ecdd..99b072a201 100644 --- a/packages/services/service-messaging/src/objects/notification-delivery.object.ts +++ b/packages/services/service-messaging/src/objects/notification-delivery.object.ts @@ -26,7 +26,7 @@ export const NotificationDelivery = ObjectSchema.create({ managedBy: 'system', description: 'Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).', titleFormat: '{channel} → {recipient_id}', - compactLayout: ['notification_id', 'recipient_id', 'channel', 'status', 'attempts'], + highlightFields: ['notification_id', 'recipient_id', 'channel', 'status', 'attempts'], fields: { id: Field.text({ label: 'Delivery ID', required: true, readonly: true }), diff --git a/packages/services/service-messaging/src/objects/notification-preference.object.ts b/packages/services/service-messaging/src/objects/notification-preference.object.ts index 055da424a4..db3e8a3de8 100644 --- a/packages/services/service-messaging/src/objects/notification-preference.object.ts +++ b/packages/services/service-messaging/src/objects/notification-preference.object.ts @@ -28,7 +28,7 @@ export const NotificationPreference = ObjectSchema.create({ managedBy: 'system', description: 'Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.', titleFormat: '{user_id} · {topic} · {channel}', - compactLayout: ['user_id', 'topic', 'channel', 'enabled', 'digest'], + highlightFields: ['user_id', 'topic', 'channel', 'enabled', 'digest'], fields: { id: Field.text({ label: 'Preference ID', required: true, readonly: true }), diff --git a/packages/services/service-messaging/src/objects/notification-receipt.object.ts b/packages/services/service-messaging/src/objects/notification-receipt.object.ts index 2705dd92d8..37418dd635 100644 --- a/packages/services/service-messaging/src/objects/notification-receipt.object.ts +++ b/packages/services/service-messaging/src/objects/notification-receipt.object.ts @@ -28,7 +28,7 @@ export const NotificationReceipt = ObjectSchema.create({ managedBy: 'system', description: 'Per-recipient × channel receipt; the source of truth for notification read-state.', titleFormat: '{state}', - compactLayout: ['notification_id', 'user_id', 'channel', 'state', 'at'], + highlightFields: ['notification_id', 'user_id', 'channel', 'state', 'at'], fields: { id: Field.text({ diff --git a/packages/services/service-messaging/src/objects/notification-subscription.object.ts b/packages/services/service-messaging/src/objects/notification-subscription.object.ts index 84d50bbe66..2b9b3f6607 100644 --- a/packages/services/service-messaging/src/objects/notification-subscription.object.ts +++ b/packages/services/service-messaging/src/objects/notification-subscription.object.ts @@ -26,7 +26,7 @@ export const NotificationSubscription = ObjectSchema.create({ managedBy: 'system', description: 'Standing subscription of a principal (role/team/user) to a notification topic.', titleFormat: '{principal} · {topic}', - compactLayout: ['topic', 'principal', 'enabled', 'created_at'], + highlightFields: ['topic', 'principal', 'enabled', 'created_at'], fields: { id: Field.text({ label: 'Subscription ID', required: true, readonly: true }), diff --git a/packages/services/service-messaging/src/objects/notification-template.object.ts b/packages/services/service-messaging/src/objects/notification-template.object.ts index 5aa7393e8d..0742ad4713 100644 --- a/packages/services/service-messaging/src/objects/notification-template.object.ts +++ b/packages/services/service-messaging/src/objects/notification-template.object.ts @@ -25,7 +25,7 @@ export const NotificationTemplate = ObjectSchema.create({ managedBy: 'system', description: 'Per (topic × channel × locale) render template for notifications.', titleFormat: '{topic} · {channel} · {locale}', - compactLayout: ['topic', 'channel', 'locale', 'is_active'], + highlightFields: ['topic', 'channel', 'locale', 'is_active'], fields: { id: Field.text({ label: 'Template ID', required: true, readonly: true }), diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.ts b/packages/services/service-realtime/src/objects/sys-presence.object.ts index 2906e85fbd..26b1c458f8 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.ts @@ -23,7 +23,7 @@ export const SysPresence = ObjectSchema.create({ managedBy: 'append-only', description: 'Real-time user presence and activity tracking', titleFormat: '{user_id} ({status})', - compactLayout: ['user_id', 'status', 'last_seen'], + highlightFields: ['user_id', 'status', 'last_seen'], fields: { id: Field.text({ diff --git a/packages/services/service-storage/src/objects/system-file.object.ts b/packages/services/service-storage/src/objects/system-file.object.ts index 8a4a263687..94540d8388 100644 --- a/packages/services/service-storage/src/objects/system-file.object.ts +++ b/packages/services/service-storage/src/objects/system-file.object.ts @@ -26,7 +26,7 @@ export const SystemFile = ObjectSchema.create({ description: 'Storage service file metadata (fileId ↔ key mapping)', nameField: 'name', // [ADR-0079] canonical primary-title pointer (single-field titleFormat) titleFormat: '{name}', - compactLayout: ['name', 'mime_type', 'size', 'status', 'created_at'], + highlightFields: ['name', 'mime_type', 'size', 'status', 'created_at'], fields: { id: Field.text({ diff --git a/packages/services/service-storage/src/objects/system-upload-session.object.ts b/packages/services/service-storage/src/objects/system-upload-session.object.ts index 5452095916..5863fb6dea 100644 --- a/packages/services/service-storage/src/objects/system-upload-session.object.ts +++ b/packages/services/service-storage/src/objects/system-upload-session.object.ts @@ -19,7 +19,7 @@ export const SystemUploadSession = ObjectSchema.create({ description: 'Resumable multipart upload sessions tracked by service-storage', nameField: 'filename', // [ADR-0079] canonical primary-title pointer (single-field titleFormat) titleFormat: '{filename}', - compactLayout: ['filename', 'status', 'uploaded_chunks', 'total_chunks', 'expires_at'], + highlightFields: ['filename', 'status', 'uploaded_chunks', 'total_chunks', 'expires_at'], fields: { id: Field.text({ diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index a7172e160e..38e6e5fc97 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -35,10 +35,6 @@ "status": "live", "note": "objectui ({{record.field}} interpolation)." }, - "compactLayout": { - "status": "live", - "note": "[DEPRECATED → highlightFields, ADR-0085] parse-time alias copied onto highlightFields; preserved on output for back-compat." - }, "highlightFields": { "status": "live", "note": "ADR-0085 semantic role (renamed from compactLayout): objectui default list/grid columns (ObjectGrid/ObjectView/InterfaceListPage), child-record previews (first entry), record:details auto layout, detail highlight strip (first 4)." diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index f7a9157053..1b8f17937d 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -897,27 +897,31 @@ describe('ObjectSchema semantic roles (ADR-0085)', () => { expect(ObjectSchema.safeParse({ name: 'lead', fields: {}, stageField: 3 }).success).toBe(false); }); - it('accepts highlightFields and aliases the deprecated compactLayout onto it', () => { + it('accepts highlightFields; the retired compactLayout alias no longer parses through (framework#2536)', () => { const direct = ObjectSchema.parse({ name: 'account', fields: {}, highlightFields: ['name', 'industry'], }); expect(direct.highlightFields).toEqual(['name', 'industry']); - // Transition mirror: old-key readers (current objectui) still see - // compactLayout for metadata authored with the canonical name. - expect(direct.compactLayout).toEqual(['name', 'industry']); + // The transition mirror is gone: output carries the canonical key only. + expect((direct as Record).compactLayout).toBeUndefined(); - const aliased = ObjectSchema.parse({ + // Lenient parse: the retired key is STRIPPED, not aliased — an old-key + // author gets no highlightFields rather than silently working. + const legacy = ObjectSchema.parse({ name: 'account', fields: {}, compactLayout: ['name', 'industry'], }); - expect(aliased.highlightFields).toEqual(['name', 'industry']); - // Deprecated key preserved on output (ADR-0079 alias pattern). - expect(aliased.compactLayout).toEqual(['name', 'industry']); + expect(legacy.highlightFields).toBeUndefined(); + expect((legacy as Record).compactLayout).toBeUndefined(); - // Canonical key wins when both are present. - const both = ObjectSchema.parse({ - name: 'account', fields: {}, highlightFields: ['a'], compactLayout: ['b'], - }); - expect(both.highlightFields).toEqual(['a']); + // Authoring path: create() REJECTS the retired key like any unknown key. + expect(() => + ObjectSchema.create({ + name: 'account', + fields: {}, + // @ts-expect-error — compactLayout was retired by framework#2536 + compactLayout: ['name'], + }), + ).toThrow(/compactLayout/); }); it('rejects the removed detail UI-hints block at create()', () => { diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 4b3083ad84..09abf0bc0c 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -603,13 +603,10 @@ const ObjectSchemaBase = z.object({ * a layout); Salesforce compact-layout semantics. */ highlightFields: z.array(z.string()).optional().describe('[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout.'), - /** - * @deprecated [ADR-0085 → `highlightFields`] Accepted as a parse-time alias: - * copied onto `highlightFields` when that key is absent (both preserved on - * output for cross-repo back-compat, the ADR-0079 alias pattern). New - * metadata sets `highlightFields`. - */ - compactLayout: z.array(z.string()).optional().describe('[DEPRECATED → highlightFields] Accepted as an alias for highlightFields.'), + // `compactLayout` (the pre-ADR-0085 spelling of `highlightFields`) was an + // accepted parse-time alias for one deprecation window and is now RETIRED + // (framework#2536): authoring it is rejected by `create()` like any unknown + // key. All first-party consumers read `highlightFields` since objectui#2168. /** * [ADR-0085] Semantic role: the field that represents the record's LINEAR * lifecycle (an ordered pipeline / stage progression). A string names the @@ -894,12 +891,8 @@ function normalizeNameFieldAlias(input: unknown): unknown { * (same pattern as `normalizeNameFieldAlias`; deprecated keys are PRESERVED * on output for cross-repo back-compat): * - * - `compactLayout` ⇄ `highlightFields`: whichever key is present is mirrored - * onto the other (canonical wins when both exist). The BACK-fill - * (highlightFields → compactLayout) is a transition mirror so renderers - * that still read the old key (current objectui / vendored console) keep - * their default columns for metadata authored with the new name; it is - * removed together with the alias. + * - (`compactLayout` ⇄ `highlightFields` mirrored here during the ADR-0085 + * deprecation window; RETIRED by framework#2536 once objectui#2168 shipped.) * - `fieldGroups[].collapse` derived from the deprecated flags when absent: * the UI-dialect `collapsible`/`collapsed` pair wins over the old * `defaultExpanded` (it is what designer-authored metadata actually @@ -912,13 +905,6 @@ function normalizeSemanticRoleAliases(input: unknown): unknown { const obj = input as Record; let out = obj; - if (obj.highlightFields == null && Array.isArray(obj.compactLayout)) { - out = { ...out, highlightFields: obj.compactLayout }; - } else if (obj.compactLayout == null && Array.isArray(obj.highlightFields)) { - // Transition mirror for old-key readers (see doc comment above). - out = { ...out, compactLayout: obj.highlightFields }; - } - if (Array.isArray(obj.fieldGroups)) { let changed = false; const groups = (obj.fieldGroups as unknown[]).map((g) => {