diff --git a/.changeset/kernel-context-preview-mode-retired.md b/.changeset/kernel-context-preview-mode-retired.md
new file mode 100644
index 0000000000..2c894eaf71
--- /dev/null
+++ b/.changeset/kernel-context-preview-mode-retired.md
@@ -0,0 +1,67 @@
+---
+"@objectstack/spec": minor
+---
+
+feat(spec): retire preview mode — the `'preview'` RuntimeMode value and the whole `KernelContext.previewMode` / `PreviewModeConfig` block (#11846, ADR-0049)
+
+
+
+**BREAKING** accept-set narrowing and export removal, landing after the
+v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the
+prescription is registered under protocol major 18 —
+`RETIRED_KEYS_BY_MAJOR[18]` for both walked-shape keys,
+`RETIRED_DEFS_BY_MAJOR[18]` for the def, plus the D3 semantic entry
+`kernel-context-preview-mode-retired` — where `os migrate meta` users will
+look).
+
+The declaration was the sharpest declared-≠-enforced shape on a SECURITY
+surface: the schema promised "bypass auth, simulate admin identity" and named
+a production guard "the runtime must enforce", and NO code path implemented
+either half. Measured zero consumers in objectstack, objectui and cloud
+(cloud#1651, closed 2026-08-26 with positive controls: `RuntimeMode` has zero
+hits repo-wide there, and `ArtifactKernelFactory` — where preview auto-login
+would live if anywhere — has 20+ hits and never touches `previewMode`;
+re-verified in objectstack at dispatch, 2026-08-27). An author — very often
+an AI — could write the six-key block per the reference docs, parse cleanly,
+and get no behaviour and no diagnostic.
+
+FROM → TO:
+
+- `mode: 'preview'` → *(removed value)* — `mode` defaults to `'production'`;
+ use `'development'` for local demo work. The rejection carries the
+ prescription via the enum's own error map (the `HookBodyCapability`
+ precedent); every other mode keeps zod's own message.
+- `previewMode: { … }` on `KernelContext` / `TenantRuntimeContext` →
+ *(removed key)* — tombstoned with `retiredKey()` (the schemas are not
+ `.strict()`, so a bare deletion would be a silent strip): authoring it is
+ now a `tsc` error and a parse error carrying the prescription.
+- `PreviewModeConfigSchema` / `PreviewModeConfig` / `PreviewModeConfigParsed`
+ → *(removed — no replacement)*. The def described behaviour no layer
+ implemented; an exported value schema with no consumer reads as a
+ capability (#3950).
+
+One-line fix: delete the key and the value — neither ever changed runtime
+behaviour, so removing them changes nothing observable. Preview deployment
+ROUTING is untouched: `OS_PREVIEW_MODE` / `OS_PREVIEW_BASE_DOMAINS` keep
+working exactly as documented (deployment routing, never identity). If a
+preview experience becomes a product capability it re-declares fresh, with
+the production-posture hard-refusal as the first-landed half (#11846 ruling
+record, maintainer 2026-08-27).
+
+The retirement kit:
+
+- tombstones at both declarations (`kernel/KernelContext:previewMode` and the
+ `.extend()` copy `kernel/TenantRuntimeContext:previewMode`, both in
+ `RETIRED_KEYS_BY_MAJOR[18]`); the enum value's prescription on
+ `RuntimeMode`'s error map (enum-VALUE retirements register nothing in
+ RETIRED_KEYS_BY_MAJOR and leave the surface ratchets byte-identical)
+- whole-def deletion `kernel/PreviewModeConfig` in `RETIRED_DEFS_BY_MAJOR[18]`
+ (manifest key deliberately removed; the #4725 gate adjudicated it)
+- deliberately NO D2 conversion: a kernel context is constructed by host code
+ at boot — not a stack collection member, never a `sys_metadata` row — so
+ the conversion chain has no seam that would ever see one (the
+ `kernel/Manifest:loading` disposition); the D3 semantic entry carries the
+ prescription
+- pin tests (`kernel/preview-mode-retirement.test.ts`): both rejection sites
+ flip from silent parse to the prescription; zero holders for all 3 retired
+ export names on every public entry; the carrier schemas survive
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index 56e5e99f8e..1fa890ef0a 100644
--- a/content/docs/references/index.mdx
+++ b/content/docs/references/index.mdx
@@ -1,6 +1,6 @@
---
title: Protocol Reference
-description: Every schema published by @objectstack/spec — 1605 schemas across 14 protocol modules
+description: Every schema published by @objectstack/spec — 1604 schemas across 14 protocol modules
---
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -26,14 +26,14 @@ counts are sums of the rows they head. Regenerate with
| [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
| [Integration Protocol](/docs/references/integration) | 1 | 27 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
-| [Kernel Protocol](/docs/references/kernel) | 31 | 171 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
+| [Kernel Protocol](/docs/references/kernel) | 31 | 170 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
| [Security Protocol](/docs/references/security) | 5 | 29 | Permission sets, row-level security, sharing rules, tenancy posture. |
| [Shared Protocol](/docs/references/shared) | 8 | 32 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
-| **Total** | **201** | **1605** | 14 protocol modules |
+| **Total** | **201** | **1604** | 14 protocol modules |
---
@@ -217,7 +217,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo
## Kernel Protocol
-**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **31 pages, 171 schemas**
+**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **31 pages, 170 schemas**
Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry.
@@ -225,7 +225,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser
| :--- | :--- |
| [`cli-extension.zod.ts`](/docs/references/kernel/cli-extension) | `OclifPluginConfig` |
| [`cluster.zod.ts`](/docs/references/kernel/cluster) | `ClusterCapabilityConfig`, `ClusterDriver`, `ClusterTenantIsolation`, `EventClusterOptions`, `EventDeliverySemantics`, `EventScope`, `MetadataChangeOperation`, `ServiceClusterAnnotations`, `ServiceClusterScope`, `ServiceLeaderStrategy` |
-| [`context.zod.ts`](/docs/references/kernel/context) | `KernelContext`, `PreviewModeConfig`, `RuntimeMode`, `TenantRuntimeContext` |
+| [`context.zod.ts`](/docs/references/kernel/context) | `KernelContext`, `RuntimeMode`, `TenantRuntimeContext` |
| [`dependency-resolution.zod.ts`](/docs/references/kernel/dependency-resolution) | `DependencyResolutionResult`, `DependencyStatusEnum`, `RequiredAction`, `ResolvedDependency` |
| [`events/bus.zod.ts`](/docs/references/kernel/events-bus) | `EventBusConfig` |
| [`events/core.zod.ts`](/docs/references/kernel/events-core) | `Event`, `EventMetadata`, `EventPriority`, `EventTypeDefinition` |
diff --git a/content/docs/references/kernel/context.mdx b/content/docs/references/kernel/context.mdx
index 6c5a9a1f4e..7d6ef892c0 100644
--- a/content/docs/references/kernel/context.mdx
+++ b/content/docs/references/kernel/context.mdx
@@ -5,9 +5,6 @@ description: Context protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Runtime Mode Enum
-Defines the operating mode of the kernel
-
**Source:** `packages/spec/src/kernel/context.zod.ts`
@@ -15,8 +12,8 @@ Defines the operating mode of the kernel
## TypeScript Usage
```typescript
-import { KernelContextSchema, PreviewModeConfigSchema, RuntimeMode, TenantRuntimeContextSchema } from '@objectstack/spec/kernel';
-import type { KernelContext, PreviewModeConfig, RuntimeMode, TenantRuntimeContext } from '@objectstack/spec/kernel';
+import { KernelContextSchema, RuntimeMode, TenantRuntimeContextSchema } from '@objectstack/spec/kernel';
+import type { KernelContext, RuntimeMode, TenantRuntimeContext } from '@objectstack/spec/kernel';
// Validate data
const result = KernelContextSchema.parse(data);
@@ -31,41 +28,14 @@ const result = KernelContextSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **instanceId** | `string` | ✅ | Unique UUID for this running kernel process |
-| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning' \| 'preview'>` | optional (default: `"production"`) | Kernel operating mode |
+| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning'>` | optional (default: `"production"`) | Kernel operating mode |
| **version** | `string` | ✅ | Kernel version |
| **appName** | `string` | optional | Host application name |
| **cwd** | `string` | ✅ | Current working directory |
| **workspaceRoot** | `string` | optional | Workspace root if different from cwd |
| **startTime** | `integer` | ✅ | Boot timestamp (ms) |
| **features** | `Record` | optional (default: `{}`) | Global feature toggles |
-| **previewMode** | `{ autoLogin: boolean; simulatedRole: Enum<'admin' \| 'user' \| 'viewer'>; simulatedUserName: string; readOnly: boolean; … }` | optional | Preview/demo mode configuration (used when mode is "preview") |
-
-### Nested Shape: `KernelContext.previewMode`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **autoLogin** | `boolean` | optional (default: `true`) | Auto-login as simulated user, skipping login/registration pages |
-| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | optional (default: `"admin"`) | Permission role for the simulated preview user |
-| **simulatedUserName** | `string` | optional (default: `"Preview User"`) | Display name for the simulated preview user |
-| **readOnly** | `boolean` | optional (default: `false`) | Restrict the preview session to read-only operations |
-| **expiresInSeconds** | `integer` | optional (default: `0`) | Preview session duration in seconds (0 = no expiration) |
-| **bannerMessage** | `string` | optional | Banner message displayed in the UI during preview mode |
-
-
----
-
-## PreviewModeConfig
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **autoLogin** | `boolean` | optional (default: `true`) | Auto-login as simulated user, skipping login/registration pages |
-| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | optional (default: `"admin"`) | Permission role for the simulated preview user |
-| **simulatedUserName** | `string` | optional (default: `"Preview User"`) | Display name for the simulated preview user |
-| **readOnly** | `boolean` | optional (default: `false`) | Restrict the preview session to read-only operations |
-| **expiresInSeconds** | `integer` | optional (default: `0`) | Preview session duration in seconds (0 = no expiration) |
-| **bannerMessage** | `string` | optional | Banner message displayed in the UI during preview mode |
+| **previewMode** | `never` | optional | [REMOVED] `context.previewMode` was removed in @objectstack/spec 17 (#11846, ADR-0049 enforce-or-remove) — nothing ever read the block: none of its six keys (`autoLogin`, `simulatedRole`, `simulatedUserName`, `readOnly`, `expiresInSeconds`, `bannerMessage`) had a consumer in any repo, so an authored block parsed cleanly and configured NOTHING, while its own docstring promised an auth bypass ("skips authentication screens", "simulates an admin identity") and named a production guard no runtime ever received. Delete the key. Preview/demo deployments belong to the deployment layer, which owns auth per-project (`ArtifactKernelFactory` in the cloud distribution); `OS_PREVIEW_MODE` stays there as a routing-only switch. If a preview experience becomes a product capability it re-declares fresh, with the production-posture hard-refusal as the first-landed half (#11846 ruling record). |
---
@@ -80,7 +50,6 @@ Kernel operating mode
* `production`
* `test`
* `provisioning`
-* `preview`
---
@@ -94,31 +63,20 @@ Tenant-aware kernel runtime context
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **instanceId** | `string` | ✅ | Unique UUID for this running kernel process |
-| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning' \| 'preview'>` | optional (default: `"production"`) | Kernel operating mode |
+| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning'>` | optional (default: `"production"`) | Kernel operating mode |
| **version** | `string` | ✅ | Kernel version |
| **appName** | `string` | optional | Host application name |
| **cwd** | `string` | ✅ | Current working directory |
| **workspaceRoot** | `string` | optional | Workspace root if different from cwd |
| **startTime** | `integer` | ✅ | Boot timestamp (ms) |
| **features** | `Record` | optional (default: `{}`) | Global feature toggles |
-| **previewMode** | `{ autoLogin: boolean; simulatedRole: Enum<'admin' \| 'user' \| 'viewer'>; simulatedUserName: string; readOnly: boolean; … }` | optional | Preview/demo mode configuration (used when mode is "preview") |
+| **previewMode** | `never` | optional | [REMOVED] `context.previewMode` was removed in @objectstack/spec 17 (#11846, ADR-0049 enforce-or-remove) — nothing ever read the block: none of its six keys (`autoLogin`, `simulatedRole`, `simulatedUserName`, `readOnly`, `expiresInSeconds`, `bannerMessage`) had a consumer in any repo, so an authored block parsed cleanly and configured NOTHING, while its own docstring promised an auth bypass ("skips authentication screens", "simulates an admin identity") and named a production guard no runtime ever received. Delete the key. Preview/demo deployments belong to the deployment layer, which owns auth per-project (`ArtifactKernelFactory` in the cloud distribution); `OS_PREVIEW_MODE` stays there as a routing-only switch. If a preview experience becomes a product capability it re-declares fresh, with the production-posture hard-refusal as the first-landed half (#11846 ruling record). |
| **tenantId** | `string` | ✅ | Resolved tenant identifier |
| **tenantPlan** | `Enum<'free' \| 'pro' \| 'enterprise'>` | ✅ | Tenant subscription plan |
| **tenantRegion** | `string` | optional | Tenant deployment region |
| **tenantDbUrl** | `string` | ✅ | Tenant database connection URL |
| **tenantQuotas** | `{ maxUsers?: integer; maxStorage?: integer; apiRateLimit?: integer; maxObjects?: integer; … }` | optional | Tenant resource quotas |
-### Nested Shape: `TenantRuntimeContext.previewMode`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **autoLogin** | `boolean` | optional (default: `true`) | Auto-login as simulated user, skipping login/registration pages |
-| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | optional (default: `"admin"`) | Permission role for the simulated preview user |
-| **simulatedUserName** | `string` | optional (default: `"Preview User"`) | Display name for the simulated preview user |
-| **readOnly** | `boolean` | optional (default: `false`) | Restrict the preview session to read-only operations |
-| **expiresInSeconds** | `integer` | optional (default: `0`) | Preview session duration in seconds (0 = no expiration) |
-| **bannerMessage** | `string` | optional | Banner message displayed in the UI during preview mode |
-
### Nested Shape: `TenantRuntimeContext.tenantQuotas`
| Property | Type | Required | Description |
diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
index 628dfb2c8a..9cd7a480d2 100644
--- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
+++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
@@ -261,7 +261,7 @@ directory rather than per file.
| `cloud/` | 83 |
| `identity/` | 32 |
| `integration/` | 10 |
-| `kernel/` | 272 |
+| `kernel/` | 271 |
| `qa/` | 6 |
| `shared/` | 20 |
| `system/` | 364 |
diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json
index 8ce5a68ead..f4c61813bb 100644
--- a/packages/spec/api-surface/kernel.json
+++ b/packages/spec/api-surface/kernel.json
@@ -356,9 +356,6 @@
"PluginVersionMetadata (type)",
"PluginVersionMetadataParsed (type)",
"PluginVersionMetadataSchema (const)",
- "PreviewModeConfig (type)",
- "PreviewModeConfigParsed (type)",
- "PreviewModeConfigSchema (const)",
"ProtocolFeature (type)",
"ProtocolFeatureParsed (type)",
"ProtocolFeatureSchema (const)",
diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json
index 8328219da3..dfda8051b1 100644
--- a/packages/spec/authorable-defaults/kernel.json
+++ b/packages/spec/authorable-defaults/kernel.json
@@ -119,11 +119,6 @@
"kernel/PluginTrustScore:badges = []",
"kernel/PluginVendor:trustLevel = \"unverified\"",
"kernel/PluginVendor:verified = false",
- "kernel/PreviewModeConfig:autoLogin = true",
- "kernel/PreviewModeConfig:expiresInSeconds = 0",
- "kernel/PreviewModeConfig:readOnly = false",
- "kernel/PreviewModeConfig:simulatedRole = \"admin\"",
- "kernel/PreviewModeConfig:simulatedUserName = \"Preview User\"",
"kernel/ProtocolFeature:enabled = true",
"kernel/RealTimeNotificationConfig:enabled = true",
"kernel/RealTimeNotificationConfig:eventPattern = \"*\"",
diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json
index 5bbce3b588..e565acf6c7 100644
--- a/packages/spec/authorable-surface/kernel.json
+++ b/packages/spec/authorable-surface/kernel.json
@@ -250,7 +250,7 @@
"kernel/KernelContext:features",
"kernel/KernelContext:instanceId",
"kernel/KernelContext:mode",
- "kernel/KernelContext:previewMode",
+ "kernel/KernelContext:previewMode [RETIRED]",
"kernel/KernelContext:startTime",
"kernel/KernelContext:version",
"kernel/KernelContext:workspaceRoot",
@@ -654,12 +654,6 @@
"kernel/PluginVersionMetadata:support",
"kernel/PluginVersionMetadata:version",
"kernel/PluginVersionMetadata:versionString",
- "kernel/PreviewModeConfig:autoLogin",
- "kernel/PreviewModeConfig:bannerMessage",
- "kernel/PreviewModeConfig:expiresInSeconds",
- "kernel/PreviewModeConfig:readOnly",
- "kernel/PreviewModeConfig:simulatedRole",
- "kernel/PreviewModeConfig:simulatedUserName",
"kernel/ProtocolFeature:deprecatedSince",
"kernel/ProtocolFeature:description",
"kernel/ProtocolFeature:enabled",
@@ -801,7 +795,7 @@
"kernel/TenantRuntimeContext:features",
"kernel/TenantRuntimeContext:instanceId",
"kernel/TenantRuntimeContext:mode",
- "kernel/TenantRuntimeContext:previewMode",
+ "kernel/TenantRuntimeContext:previewMode [RETIRED]",
"kernel/TenantRuntimeContext:startTime",
"kernel/TenantRuntimeContext:tenantDbUrl",
"kernel/TenantRuntimeContext:tenantId",
diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json
index 3911c719d1..6347ed682a 100644
--- a/packages/spec/export-origins/kernel.json
+++ b/packages/spec/export-origins/kernel.json
@@ -356,9 +356,6 @@
"PluginVersionMetadata": "src/kernel/plugin-versioning.zod.ts#PluginVersionMetadata (type)",
"PluginVersionMetadataParsed": "src/kernel/plugin-versioning.zod.ts#PluginVersionMetadataParsed (type)",
"PluginVersionMetadataSchema": "src/kernel/plugin-versioning.zod.ts#PluginVersionMetadataSchema (const)",
- "PreviewModeConfig": "src/kernel/context.zod.ts#PreviewModeConfig (type)",
- "PreviewModeConfigParsed": "src/kernel/context.zod.ts#PreviewModeConfigParsed (type)",
- "PreviewModeConfigSchema": "src/kernel/context.zod.ts#PreviewModeConfigSchema (const)",
"ProtocolFeature": "src/kernel/plugin-capability.zod.ts#ProtocolFeature (type)",
"ProtocolFeatureParsed": "src/kernel/plugin-capability.zod.ts#ProtocolFeatureParsed (type)",
"ProtocolFeatureSchema": "src/kernel/plugin-capability.zod.ts#ProtocolFeatureSchema (const)",
diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json
index 7f69207949..822c964283 100644
--- a/packages/spec/json-schema.manifest/kernel.json
+++ b/packages/spec/json-schema.manifest/kernel.json
@@ -128,7 +128,6 @@
"kernel/PluginTrustScore",
"kernel/PluginVendor",
"kernel/PluginVersionMetadata",
- "kernel/PreviewModeConfig",
"kernel/ProtocolFeature",
"kernel/ProtocolReference",
"kernel/ProtocolVersion",
diff --git a/packages/spec/src/kernel/context.test.ts b/packages/spec/src/kernel/context.test.ts
index b7ea6ca81f..723fabe960 100644
--- a/packages/spec/src/kernel/context.test.ts
+++ b/packages/spec/src/kernel/context.test.ts
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
import {
RuntimeMode,
KernelContextSchema,
- PreviewModeConfigSchema,
TenantRuntimeContextSchema,
type KernelContext,
type TenantRuntimeContext,
@@ -14,13 +13,15 @@ describe('RuntimeMode', () => {
expect(() => RuntimeMode.parse('production')).not.toThrow();
expect(() => RuntimeMode.parse('test')).not.toThrow();
expect(() => RuntimeMode.parse('provisioning')).not.toThrow();
- expect(() => RuntimeMode.parse('preview')).not.toThrow();
});
it('should reject invalid runtime modes', () => {
expect(() => RuntimeMode.parse('staging')).toThrow();
expect(() => RuntimeMode.parse('debug')).toThrow();
expect(() => RuntimeMode.parse('')).toThrow();
+ // 'preview' was RETIRED (#11846) — the full prescription pins live in
+ // preview-mode-retirement.test.ts; this list only records the narrowing.
+ expect(() => RuntimeMode.parse('preview')).toThrow();
});
});
@@ -91,82 +92,19 @@ describe('KernelContextSchema', () => {
});
it('should accept all runtime modes in context', () => {
- const modes = ['development', 'production', 'test', 'provisioning', 'preview'] as const;
+ // 'preview' left this list in #11846 — see preview-mode-retirement.test.ts.
+ const modes = ['development', 'production', 'test', 'provisioning'] as const;
modes.forEach(mode => {
const parsed = KernelContextSchema.parse({ ...validContext, mode });
expect(parsed.mode).toBe(mode);
});
});
- it('should accept preview mode with previewMode config', () => {
- const parsed = KernelContextSchema.parse({
- ...validContext,
- mode: 'preview',
- previewMode: {
- autoLogin: true,
- simulatedRole: 'admin',
- simulatedUserName: 'Demo Admin',
- readOnly: true,
- expiresInSeconds: 3600,
- bannerMessage: 'You are viewing a demo of this application.',
- },
- });
- expect(parsed.mode).toBe('preview');
- expect(parsed.previewMode?.autoLogin).toBe(true);
- expect(parsed.previewMode?.simulatedRole).toBe('admin');
- expect(parsed.previewMode?.simulatedUserName).toBe('Demo Admin');
- expect(parsed.previewMode?.readOnly).toBe(true);
- expect(parsed.previewMode?.expiresInSeconds).toBe(3600);
- expect(parsed.previewMode?.bannerMessage).toContain('demo');
- });
-
- it('should accept context without previewMode (optional)', () => {
+ it('should accept a context that does not author previewMode (retired key, absence is legal)', () => {
const parsed = KernelContextSchema.parse(validContext);
- expect(parsed.previewMode).toBeUndefined();
- });
-});
-
-describe('PreviewModeConfigSchema', () => {
- it('should apply defaults for zero-config preview', () => {
- const parsed = PreviewModeConfigSchema.parse({});
- expect(parsed.autoLogin).toBe(true);
- expect(parsed.simulatedRole).toBe('admin');
- expect(parsed.simulatedUserName).toBe('Preview User');
- expect(parsed.readOnly).toBe(false);
- expect(parsed.expiresInSeconds).toBe(0);
- expect(parsed.bannerMessage).toBeUndefined();
- });
-
- it('should accept all simulated roles', () => {
- const roles = ['admin', 'user', 'viewer'] as const;
- roles.forEach(role => {
- const parsed = PreviewModeConfigSchema.parse({ simulatedRole: role });
- expect(parsed.simulatedRole).toBe(role);
- });
- });
-
- it('should reject invalid simulated role', () => {
- expect(() => PreviewModeConfigSchema.parse({ simulatedRole: 'superadmin' })).toThrow();
- });
-
- it('should accept read-only preview for marketplace demos', () => {
- const parsed = PreviewModeConfigSchema.parse({
- autoLogin: true,
- simulatedRole: 'viewer',
- readOnly: true,
- bannerMessage: 'This is a preview. Sign up to get started!',
- });
- expect(parsed.readOnly).toBe(true);
- expect(parsed.simulatedRole).toBe('viewer');
- expect(parsed.bannerMessage).toContain('preview');
- });
-
- it('should reject negative expiresInSeconds', () => {
- expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: -1 })).toThrow();
- });
-
- it('should reject non-integer expiresInSeconds', () => {
- expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: 1.5 })).toThrow();
+ // The non-strict strip path: absence must stay absence. The retirement
+ // pins for the AUTHORED case live in preview-mode-retirement.test.ts.
+ expect(parsed).not.toHaveProperty('previewMode');
});
});
diff --git a/packages/spec/src/kernel/context.zod.ts b/packages/spec/src/kernel/context.zod.ts
index 50d630178b..6b0678a709 100644
--- a/packages/spec/src/kernel/context.zod.ts
+++ b/packages/spec/src/kernel/context.zod.ts
@@ -2,95 +2,80 @@
import { z } from 'zod';
import { TenantQuotaSchema } from '../system/tenant.zod.js';
+import { lazySchema } from '../shared/lazy-schema';
+import { retiredKey } from '../shared/retired-key';
+
+// Retirement prescriptions (#11846, ADR-0049 enforce-or-remove; maintainer
+// ruling 2026-08-27). Declared with `//` (never `/** */`) and ABOVE the enum's
+// JSDoc on purpose — build-docs takes the file's FIRST JSDoc as the reference
+// page's module blurb (the hook-body.zod.ts placement precedent).
+//
+// No `os migrate meta` sentence in either string, deliberately: there is no D2
+// conversion behind this retirement — a kernel context is constructed by HOST
+// CODE at boot, never authored in a stack collection or stored as a
+// `sys_metadata` row, so the conversion chain has no seam that would ever see
+// one (the `kernel/Manifest:loading` precedent). The prescription reaches
+// authors through these two rejection sites plus the D3 semantic entry
+// `kernel-context-preview-mode-retired`.
+const RUNTIME_MODE_PREVIEW_RETIRED =
+ "`context.mode: 'preview'` was removed from `RuntimeMode` in @objectstack/spec 17 "
+ + '(#11846, ADR-0049 enforce-or-remove) — no layer of the platform ever branched on it: '
+ + 'the value promised "bypass auth, simulate admin identity" and no code path implemented '
+ + 'either half, so a deployment declaring it ran with ordinary production behaviour under '
+ + 'a misleading label. Delete the value — `mode` defaults to `production`; use '
+ + '`development` for local demo work. Preview DEPLOYMENTS are the deployment layer\'s '
+ + 'job (`OS_PREVIEW_MODE` is routing-only and never touched identity). If a preview '
+ + 'experience becomes a product capability it re-declares fresh, with the '
+ + 'production-posture hard-refusal as the first-landed half (#11846 ruling record).';
+const PREVIEW_MODE_RETIRED =
+ '`context.previewMode` was removed in @objectstack/spec 17 (#11846, ADR-0049 '
+ + 'enforce-or-remove) — nothing ever read the block: none of its six keys (`autoLogin`, '
+ + '`simulatedRole`, `simulatedUserName`, `readOnly`, `expiresInSeconds`, `bannerMessage`) '
+ + 'had a consumer in any repo, so an authored block parsed cleanly and configured '
+ + 'NOTHING, while its own docstring promised an auth bypass ("skips authentication '
+ + 'screens", "simulates an admin identity") and named a production guard no runtime ever '
+ + 'received. Delete the key. Preview/demo deployments belong to the deployment layer, '
+ + 'which owns auth per-project (`ArtifactKernelFactory` in the cloud distribution); '
+ + '`OS_PREVIEW_MODE` stays there as a routing-only switch. If a preview experience '
+ + 'becomes a product capability it re-declares fresh, with the production-posture '
+ + 'hard-refusal as the first-landed half (#11846 ruling record).';
/**
* Runtime Mode Enum
* Defines the operating mode of the kernel
*/
-import { lazySchema } from '../shared/lazy-schema';
export const RuntimeMode = z.enum([
'development', // Hot-reload, verbose logging
'production', // Optimized, strict security
'test', // Mocked interfaces
'provisioning', // Setup/Migration mode
- 'preview', // Demo/preview mode — bypass auth, simulate admin identity
-]).describe('Kernel operating mode');
+ // 'preview' was RETIRED in #11846 — see RUNTIME_MODE_PREVIEW_RETIRED above.
+], {
+ // Only the value that USED to be legal gets the retirement prescription —
+ // telling the author of a typo that their mode "was removed" would
+ // misinform. Everything else keeps zod's own enum message, which already
+ // lists the legal values. (The `HookBodyCapability` / `managedBy: 'system'`
+ // precedent.)
+ error: (issue) => (issue.input === 'preview' ? RUNTIME_MODE_PREVIEW_RETIRED : undefined),
+}).describe('Kernel operating mode');
export type RuntimeMode = z.input;
-/**
- * Preview Mode Configuration Schema
- *
- * Configures the kernel's preview/demo mode behaviour.
- * When `mode` is set to `'preview'`, the platform skips authentication
- * screens and optionally simulates an admin identity so that visitors
- * (e.g. app-marketplace customers) can explore the system without
- * registering or logging in.
- *
- * **Security note:** preview mode should NEVER be used in production.
- * The runtime must enforce this constraint.
- *
- * @example
- * ```ts
- * const ctx = KernelContextSchema.parse({
- * instanceId: '550e8400-e29b-41d4-a716-446655440000',
- * mode: 'preview',
- * version: '1.0.0',
- * cwd: '/app',
- * startTime: Date.now(),
- * previewMode: {
- * autoLogin: true,
- * simulatedRole: 'admin',
- * },
- * });
- * ```
- */
-export const PreviewModeConfigSchema = lazySchema(() => z.object({
- /**
- * Automatically log in as a simulated user on startup.
- * When enabled, the frontend skips login/registration screens entirely.
- */
- autoLogin: z.boolean().default(true)
- .describe('Auto-login as simulated user, skipping login/registration pages'),
-
- /**
- * Role of the simulated user.
- * Determines the permission level of the auto-created preview session.
- */
- simulatedRole: z.enum(['admin', 'user', 'viewer']).default('admin')
- .describe('Permission role for the simulated preview user'),
-
- /**
- * Display name for the simulated user shown in the UI.
- */
- simulatedUserName: z.string().default('Preview User')
- .describe('Display name for the simulated preview user'),
-
- /**
- * Whether the preview session is read-only.
- * When true, all write operations (create, update, delete) are blocked.
- */
- readOnly: z.boolean().default(false)
- .describe('Restrict the preview session to read-only operations'),
-
- /**
- * Session duration in seconds. After expiry the preview session ends.
- * 0 means no expiration.
- */
- expiresInSeconds: z.number().int().min(0).default(0)
- .describe('Preview session duration in seconds (0 = no expiration)'),
-
- /**
- * Optional banner message shown in the UI to indicate preview mode.
- * Useful for marketplace demos so visitors know they are in a sandbox.
- */
- bannerMessage: z.string().optional()
- .describe('Banner message displayed in the UI during preview mode'),
-}));
-
-export type PreviewModeConfig = z.input;
-/** Post-parse shape of {@link PreviewModeConfig} — defaults applied, transforms run (ADR-0122). */
-export type PreviewModeConfigParsed = z.infer;
+// ── `PreviewModeConfigSchema` was RETIRED here (#11846, ADR-0049) ────────────
+//
+// The whole def — six authorable keys (`autoLogin` default true,
+// `simulatedRole` default 'admin', `simulatedUserName`, `readOnly`,
+// `expiresInSeconds`, `bannerMessage`) — left the published set together with
+// its `PreviewModeConfig` / `PreviewModeConfigParsed` types: its only carrier
+// key (`KernelContext.previewMode`, tombstoned below) is retired, and an
+// exported value schema with no consumer reads as a capability (#3950, the
+// `PerformanceConfigSchema` rule). Registered as `kernel/PreviewModeConfig` in
+// `RETIRED_DEFS_BY_MAJOR[18]`; the prescription lives on the tombstone below,
+// the `RuntimeMode` error map above, and the D3 semantic entry
+// `kernel-context-preview-mode-retired`. What actually produces preview
+// deployments is the deployment layer — the cloud distribution owns auth
+// per-project (`ArtifactKernelFactory`), and `OS_PREVIEW_MODE` there is
+// routing-only.
/**
* Kernel Context Schema
@@ -126,12 +111,15 @@ export const KernelContextSchema = lazySchema(() => z.object({
features: z.record(z.string(), z.boolean()).default({}).describe('Global feature toggles'),
/**
- * Preview Mode Configuration.
- * Only relevant when `mode` is `'preview'`. Configures auto-login,
- * simulated identity, read-only restrictions, and UI banner.
+ * RETIRED (#11846, ADR-0049 enforce-or-remove): the `previewMode` block —
+ * declared as an auth bypass, enforced by nothing — is unwritable. The
+ * schema is not `.strict()`, so a bare deletion would have Zod silently
+ * STRIP the key (#3733, ADR-0104); the tombstone keeps the removal audible
+ * in both channels (`tsc` types the key `never`; the parse raises the
+ * prescription). `TenantRuntimeContextSchema` extends this shape and
+ * inherits the tombstone.
*/
- previewMode: PreviewModeConfigSchema.optional()
- .describe('Preview/demo mode configuration (used when mode is "preview")'),
+ previewMode: retiredKey(PREVIEW_MODE_RETIRED),
}));
export type KernelContext = z.input;
diff --git a/packages/spec/src/kernel/preview-mode-retirement.test.ts b/packages/spec/src/kernel/preview-mode-retirement.test.ts
new file mode 100644
index 0000000000..1af469a3f5
--- /dev/null
+++ b/packages/spec/src/kernel/preview-mode-retirement.test.ts
@@ -0,0 +1,216 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { describe, it, expect } from 'vitest';
+
+import {
+ RuntimeMode,
+ KernelContextSchema,
+ TenantRuntimeContextSchema,
+} from './context.zod';
+import {
+ EXPORT_ENTRY_POINTS,
+ exportNamesOf,
+ holdersOf,
+} from '../../scripts/lib/export-origins-testkit';
+
+// ─── [#11846] Preview mode is RETIRED: the `'preview'` RuntimeMode value and
+// the whole `previewMode` / `PreviewModeConfig` block ────────────────────
+//
+// ADR-0049 enforce-or-remove; maintainer ruling 2026-08-27 (Option A: remove).
+// The declaration promised an auth bypass — "skips authentication screens",
+// "simulates an admin identity", with a docstring naming a production guard
+// "the runtime must enforce" — and NOTHING implemented any of it. Measured
+// three-repo zero consumers (objectstack in-card + re-verified at dispatch;
+// objectui in-card; cloud#1651 with positive controls on 2026-08-26): no
+// runtime branches on the mode, nothing reads any of the six config keys, and
+// `OS_PREVIEW_MODE` is deployment ROUTING, unrelated to identity — it stays.
+//
+// Two rejection sites, three bookkeeping shapes:
+//
+// 1. `mode: 'preview'` — enum-VALUE narrowing. Invisible to the four
+// surface ratchets (no def changed), so the prescription hangs on the
+// enum's own error map (the `HookBodyCapability` / `crypto.hash`
+// precedent), dispatched by `issue.input`.
+// 2. `previewMode:` — `retiredKey()` tombstone on the non-strict
+// `KernelContextSchema` (a bare deletion would be a SILENT STRIP,
+// #3733 / ADR-0104). `TenantRuntimeContextSchema` extends the shape, so
+// the tombstone lands in that walked shape too; both keys are registered
+// in `RETIRED_KEYS_BY_MAJOR[18]`.
+// 3. `PreviewModeConfigSchema` + its 2 types — orphan value schema, def
+// removed whole (`kernel/PreviewModeConfig` in `RETIRED_DEFS_BY_MAJOR[18]`;
+// an exported value schema with no consumer reads as a capability, #3950).
+//
+// No D2 conversion, deliberately: a kernel context is constructed by HOST CODE
+// at boot — it is not a stack collection member and nothing stores one as a
+// `sys_metadata` row, so the conversion chain has no seam that would ever see
+// it (the `kernel/Manifest:loading` precedent). The D3 semantic entry
+// `kernel-context-preview-mode-retired` carries the prescription outward.
+//
+// On the assertion set (the #8586 precedent, same reasoning): a schema refusal
+// raises a `ZodError` whose issues carry `code` and `path` but no ADR-0112
+// `status` — that envelope belongs to the API error surface. So these pins
+// assert the strongest set this surface really has: refusal, the issue `code`,
+// the `path` naming WHICH site refused, and the prescription text (#5240:
+// where the wording is the contract, pin the wording).
+describe("[#11846] RuntimeMode 'preview' retirement", () => {
+ it("no longer offers 'preview' as a mode", () => {
+ expect(RuntimeMode.options).toEqual([
+ 'development',
+ 'production',
+ 'test',
+ 'provisioning',
+ ]);
+ expect(RuntimeMode.options).not.toContain('preview');
+ });
+
+ it("REJECTS mode: 'preview', carrying the retirement prescription", () => {
+ const result = RuntimeMode.safeParse('preview');
+ expect(result.success).toBe(false);
+ if (result.success) return; // narrowing; the assertion above already failed
+
+ const message = JSON.stringify(result.error.issues);
+ // The prescription itself, not a bare "invalid enum value": it must name
+ // the value, say it was removed, and tell the author what to do instead.
+ expect(message).toMatch(/`context\.mode: 'preview'`.*was removed.*17.*#11846/s);
+ expect(message).toMatch(/no layer of the platform ever branched on it/s);
+ expect(message).toMatch(/Delete the value/s);
+ // The live mechanism must be named: preview DEPLOYMENTS are the
+ // deployment layer's job, and the env var that stays is routing-only.
+ expect(message).toMatch(/OS_PREVIEW_MODE.*routing-only/s);
+ // The recorded fallback: re-declaring is fresh, production refusal first.
+ expect(message).toMatch(/production-posture hard-refusal.*first-landed half/s);
+ });
+
+ it('REJECTS it through the context embed too, at path `mode`', () => {
+ const result = KernelContextSchema.safeParse({
+ instanceId: '550e8400-e29b-41d4-a716-446655440000',
+ mode: 'preview',
+ version: '1.0.0',
+ cwd: '/app',
+ startTime: Date.now(),
+ });
+ expect(result.success).toBe(false);
+ if (result.success) return;
+
+ const issue = result.error.issues.find((i) => i.path[0] === 'mode');
+ expect(issue, 'the refusal must land at `mode`').toBeDefined();
+ expect(issue!.message).toMatch(/`context\.mode: 'preview'`.*was removed.*#11846/s);
+ });
+
+ it("gives an UNKNOWN mode zod's own message, not the retirement one", () => {
+ // Only the value that used to be legal gets "was removed" — telling the
+ // author of a typo that their mode was retired would misinform.
+ const result = RuntimeMode.safeParse('previeww');
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(JSON.stringify(result.error.issues)).not.toMatch(/was removed/);
+ });
+});
+
+describe('[#11846] KernelContext.previewMode retirement', () => {
+ const baseContext = {
+ instanceId: '550e8400-e29b-41d4-a716-446655440000',
+ mode: 'production',
+ version: '1.0.0',
+ cwd: '/app',
+ startTime: Date.now(),
+ } as const;
+
+ /** The block exactly as the retired docs taught authors to write it. */
+ const authoredBlock = { autoLogin: true, simulatedRole: 'admin' } as const;
+
+ it('REJECTS an authored `previewMode` block, naming the key and carrying the fix', () => {
+ const result = KernelContextSchema.safeParse({
+ ...baseContext,
+ previewMode: authoredBlock,
+ });
+ expect(result.success).toBe(false);
+ if (result.success) return;
+
+ const issue = result.error.issues.find((i) => i.path[0] === 'previewMode');
+ expect(issue, 'the refusal must name `previewMode`').toBeDefined();
+ // The machine-readable half of the envelope this surface actually has:
+ // a `retiredKey()` tombstone raises `invalid_type` from its `z.never()`.
+ expect(issue!.code).toBe('invalid_type');
+ expect(issue!.path).toEqual(['previewMode']);
+ // The prescription IS the migration doc for whoever hits it — contract,
+ // not commentary.
+ expect(issue!.message).toMatch(/`context\.previewMode`.*was removed.*17.*#11846/s);
+ expect(issue!.message).toMatch(/nothing ever read the block/s);
+ expect(issue!.message).toMatch(/Delete the key/s);
+ // The live mechanism: the deployment layer owns preview deployments.
+ expect(issue!.message).toMatch(/deployment layer/s);
+ expect(issue!.message).toMatch(/OS_PREVIEW_MODE.*routing-only/s);
+ });
+
+ it('REJECTS it through TenantRuntimeContextSchema too (the `.extend()` copy)', () => {
+ const result = TenantRuntimeContextSchema.safeParse({
+ ...baseContext,
+ tenantId: 'tenant_abc',
+ tenantPlan: 'pro',
+ tenantDbUrl: 'libsql://tenant-abc-myorg.turso.io',
+ previewMode: authoredBlock,
+ });
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ const issue = result.error.issues.find((i) => i.path[0] === 'previewMode');
+ expect(issue, 'the refusal must surface on the tenant context too').toBeDefined();
+ expect(issue!.code).toBe('invalid_type');
+ expect(issue!.message).toMatch(/`context\.previewMode`.*was removed/s);
+ });
+
+ it('parses cleanly once the key is deleted, and grows no `previewMode` property', () => {
+ const parsed = KernelContextSchema.parse({ ...baseContext });
+ expect(parsed.mode).toBe('production'); // control: the live keys still work
+ // The non-strict strip path: absence must stay absence. If the tombstone
+ // were ever replaced by a plain deletion, an authored `previewMode` would
+ // be stripped here in silence — this pin plus the rejections above are
+ // what make that regression loud.
+ expect(parsed).not.toHaveProperty('previewMode');
+ });
+});
+
+describe('[#11846] kernel/PreviewModeConfig def retirement', () => {
+ /** The 3 names the retired def exported (1 schema const + 2 types). */
+ const RETIRED_NAMES = [
+ 'PreviewModeConfigSchema',
+ 'PreviewModeConfig',
+ 'PreviewModeConfigParsed',
+ ] as const;
+
+ it('every retired name has ZERO holders on any public entry; the carrier survives', () => {
+ // Anti-vacuity: the baseline must cover the real surface.
+ for (const needed of ['.', './kernel']) {
+ expect(EXPORT_ENTRY_POINTS, `exports map must include ${needed}`).toContain(needed);
+ }
+ expect(
+ exportNamesOf('./kernel').length,
+ './kernel must export a non-trivial surface',
+ ).toBeGreaterThan(50);
+
+ // ── ABSENCE (every entry, not just ./kernel) ──────────────────────────
+ for (const name of RETIRED_NAMES) {
+ expect(holdersOf(name), `${name} must have zero holders after #11846`).toEqual([]);
+ }
+
+ // ── SURVIVAL ──────────────────────────────────────────────────────────
+ // The context module itself stays: the carrier def and its neighbours are
+ // untouched — this retirement is a narrowing, not a module sweep.
+ const kernelNames = exportNamesOf('./kernel');
+ for (const name of [
+ 'RuntimeMode',
+ 'KernelContextSchema',
+ 'TenantRuntimeContextSchema',
+ ]) {
+ expect(kernelNames, `${name} must SURVIVE this retirement`).toContain(name);
+ }
+ });
+
+ it('the kernel barrel resolves without the retired schema and keeps the survivors', async () => {
+ const kernel = await import('./index');
+ expect(kernel).not.toHaveProperty('PreviewModeConfigSchema');
+ // Anti-vacuity: the barrel really resolved and still exports the carrier.
+ expect(kernel).toHaveProperty('KernelContextSchema');
+ expect(kernel).toHaveProperty('RuntimeMode');
+ });
+});
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__PreviewModeConfig.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__PreviewModeConfig.ts
new file mode 100644
index 0000000000..0fcebc95d9
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__PreviewModeConfig.ts
@@ -0,0 +1,20 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #11846 — `kernel/PreviewModeConfig` (the six-key preview/demo config block:
+// `autoLogin` default true, `simulatedRole` default 'admin',
+// `simulatedUserName`, `readOnly`, `expiresInSeconds`, `bannerMessage`). Its
+// only carrier key, `KernelContext.previewMode`, is tombstoned in this same
+// major (both walked-shape copies — see the two `…:previewMode` entries in
+// RETIRED_KEYS_BY_MAJOR[18]), and an exported value schema with no consumer
+// reads as a capability (#3950, the `PerformanceConfigSchema` rule) — so the
+// def leaves the emitted set whole, with its `PreviewModeConfig` /
+// `PreviewModeConfigParsed` types. Measured before removal: zero consumers of
+// the schema or any of its six keys in objectstack, objectui or cloud
+// (cloud#1651, 2026-08-26, positive controls on record). The declared
+// behaviour (auto-login as a simulated admin, a read-only demo session) was
+// never implemented by any layer; preview DEPLOYMENTS belong to the
+// deployment layer, whose `OS_PREVIEW_MODE` is routing-only and stays. If a
+// preview experience becomes a product capability it re-declares fresh, with
+// the production-posture hard-refusal as the first-landed half (#11846 ruling
+// record).
+export const entry = 'kernel/PreviewModeConfig';
diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__KernelContext__previewMode.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__KernelContext__previewMode.ts
new file mode 100644
index 0000000000..95421b82cc
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__KernelContext__previewMode.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #11846 — ADR-0049 enforce-or-remove on the preview-mode block (maintainer
+// ruling 2026-08-27, Option A: remove). The key was declared as an auth bypass
+// — its docstring promised auto-login as a simulated admin and named a
+// production guard "the runtime must enforce" — and NOTHING implemented any of
+// it: zero consumers measured in objectstack, objectui and cloud (cloud#1651,
+// 2026-08-26, with positive controls; `ArtifactKernelFactory` — where serve.ts
+// predicted preview auto-login would live if anywhere — never touches it). An
+// author could write the six-key block today, parse cleanly, and get no
+// behaviour and no diagnostic, while the reference docs said the capability
+// existed. Tombstoned with `retiredKey()` — `KernelContextSchema` is not
+// `.strict()`, so a bare deletion would be a silent strip (#3733, ADR-0104).
+//
+// Registered here but NOT in `src/conversions/registry.ts`, the
+// `kernel/Manifest:loading` reasoning: a kernel context is constructed by HOST
+// CODE at boot — it is not a stack collection member (`PLURAL_TO_SINGULAR` has
+// no entry for it) and nothing stores one as a `sys_metadata` row, so a
+// MetadataConversion would be a transform with no seam that ever runs. The
+// prescription reaches authors through the tombstone plus the D3 semantic
+// entry `kernel-context-preview-mode-retired`.
+//
+// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
+// removal ships on the 17.x line (launch-window convention: accept-set
+// narrowings ride minor releases) and the prescription lives at the major
+// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent).
+export const entry = 'kernel/KernelContext:previewMode';
diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__TenantRuntimeContext__previewMode.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__TenantRuntimeContext__previewMode.ts
new file mode 100644
index 0000000000..2ac1b87a48
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__TenantRuntimeContext__previewMode.ts
@@ -0,0 +1,10 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #11846 — the `TenantRuntimeContextSchema` copy of
+// `kernel/KernelContext:previewMode`: the def is `KernelContextSchema.extend(…)`,
+// so the tombstone lands in this walked shape too and `authorable-surface/`
+// marks it `[RETIRED]` separately. Registered per key, as gate (b) reads them —
+// nothing radiates from the base (the `shared/FieldMapping:transform`
+// precedent). See the base entry for the full record and the
+// no-D2-conversion reasoning.
+export const entry = 'kernel/TenantRuntimeContext:previewMode';
diff --git a/packages/spec/src/migrations/entries/semantic/18.kernel-context-preview-mode-retired.ts b/packages/spec/src/migrations/entries/semantic/18.kernel-context-preview-mode-retired.ts
new file mode 100644
index 0000000000..6c206bcfe0
--- /dev/null
+++ b/packages/spec/src/migrations/entries/semantic/18.kernel-context-preview-mode-retired.ts
@@ -0,0 +1,83 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SemanticMigration } from '../../types.js';
+
+export const entry: SemanticMigration = {
+ id: 'kernel-context-preview-mode-retired',
+ // No backticks in `surface` — build-upgrade-guide.ts renders it inside a
+ // code span AND a table cell.
+ surface:
+ "context.mode — the value 'preview' left the RuntimeMode enum — and "
+ + 'context.previewMode, the whole PreviewModeConfig block it keyed '
+ + '(autoLogin / simulatedRole / simulatedUserName / readOnly / '
+ + 'expiresInSeconds / bannerMessage, declared on KernelContext and on the '
+ + 'TenantRuntimeContext extension). The exported '
+ + 'PreviewModeConfigSchema / PreviewModeConfig / PreviewModeConfigParsed '
+ + 'names left with the def',
+ replacement:
+ 'nothing declarative — the capability the block described was never '
+ + 'implemented by any layer, so there is no working configuration to '
+ + 'migrate to. Preview/demo DEPLOYMENTS belong to the deployment layer, '
+ + 'which owns auth per-project (ArtifactKernelFactory in the cloud '
+ + 'distribution); the OS_PREVIEW_MODE environment variable stays exactly '
+ + 'as it is — deployment ROUTING (widening the trusted-origin list for '
+ + 'preview subdomains), unrelated to identity. If a preview experience '
+ + 'becomes a product capability it re-declares fresh, with the '
+ + 'production-posture hard-refusal as the first-landed half (#11846 '
+ + 'ruling record)',
+ reason:
+ 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-27 on #11846 '
+ + '(decision-inbox batch 2, Option A: remove — all four decision facets '
+ + 'pointed the same way). The declaration was the sharpest '
+ + 'declared-≠-enforced shape on a SECURITY surface: the schema promised '
+ + '"bypass auth, simulate admin identity" and named a production guard '
+ + '"the runtime must enforce", and NO code path implemented either half. '
+ + 'Measured zero consumers in all three repos, each leg with positive '
+ + 'controls: objectstack — no runtime branches on the mode; the only '
+ + "non-declaration hits for RuntimeMode or mode === 'preview' are the "
+ + 'schema unit test, a type-alias pin and a measurement-test comment '
+ + '(re-verified at dispatch, 2026-08-27, origin/main 15bf9e8). objectui — '
+ + 'the #11846 card records the measurement. cloud — cloud#1651 (closed '
+ + '2026-08-26): previewMode appears only as a local variable for '
+ + 'OS_PREVIEW_MODE whose effect is adding preview-domain wildcards to '
+ + "better-auth's CSRF trusted origins; RuntimeMode has zero hits "
+ + 'repo-wide; the positive control ArtifactKernelFactory (where serve.ts '
+ + 'predicted preview auto-login would live if it existed) has 20+ hits '
+ + 'and never touches previewMode. An author — very often an AI (ADR-0033) '
+ + '— could write the six-key block per the reference docs, parse cleanly, '
+ + 'and get no behaviour and no diagnostic, while a reader of the docs had '
+ + 'no way to tell the block from the keys that work. Bookkeeping: the '
+ + "enum-VALUE half ('preview') puts nothing in RETIRED_KEYS_BY_MAJOR and "
+ + 'leaves the four surface ratchets untouched by itself — its '
+ + "prescription hangs on the enum's own error map (the HookBodyCapability "
+ + 'precedent); the KEY half is tombstoned with retiredKey() on the '
+ + 'non-strict KernelContextSchema (both walked-shape copies registered in '
+ + 'RETIRED_KEYS_BY_MAJOR[18]); the DEF half (kernel/PreviewModeConfig, '
+ + 'with no carrier left) is registered in RETIRED_DEFS_BY_MAJOR[18]. It '
+ + 'is a SEMANTIC entry rather than a D2 conversion because there is no '
+ + 'source to rewrite: a kernel context is constructed by host code at '
+ + 'boot — not a stack collection member, never stored as a sys_metadata '
+ + 'row — so the conversion chain has no seam that would ever see one '
+ + '(the kernel/Manifest:loading disposition). ADR-0049 / ADR-0087, '
+ + '#11846.',
+ acceptanceCriteria:
+ "No host constructs a kernel context with mode: 'preview' or a "
+ + 'previewMode block: both now fail tsc at the authoring site and fail '
+ + 'the parse with the prescription (pinned in '
+ + 'kernel/preview-mode-retirement.test.ts). Concretely, check three '
+ + "places. (1) Host boot code composing a KernelContext: delete `mode: "
+ + "'preview'` (mode defaults to production; use development for local "
+ + 'demo work) and delete any previewMode block — neither ever changed '
+ + 'runtime behaviour, so removing them changes nothing observable. '
+ + '(2) Code importing PreviewModeConfigSchema, PreviewModeConfig or '
+ + 'PreviewModeConfigParsed from @objectstack/spec or @objectstack/spec/'
+ + 'kernel: every one is TS2305 after upgrade; no working replacement '
+ + 'exists to point at, because the vocabulary described nothing real. '
+ + "(3) TypeScript branching on the RuntimeMode type (a mode === "
+ + "'preview' arm, a switch over modes): the arm is now unreachable and "
+ + 'an exhaustiveness check will fail to compile if it stays — that '
+ + 'compile error is the enforced channel for TypeScript consumers. '
+ + 'Preview deployment ROUTING is untouched: OS_PREVIEW_MODE and '
+ + 'OS_PREVIEW_BASE_DOMAINS keep working exactly as documented '
+ + '(deployment routing, never identity).',
+};
diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts
index 00c85d28e8..71b1b31ea3 100644
--- a/packages/spec/src/migrations/registry.ts
+++ b/packages/spec/src/migrations/registry.ts
@@ -6422,6 +6422,85 @@ const step18: MigrationStep = {
+ 'read the schema, so removing it removes no behaviour — mint and verify '
+ 'work byte-identically before and after.',
},
+ {
+ id: 'kernel-context-preview-mode-retired',
+ // No backticks in `surface` — build-upgrade-guide.ts renders it inside a
+ // code span AND a table cell.
+ surface:
+ "context.mode — the value 'preview' left the RuntimeMode enum — and "
+ + 'context.previewMode, the whole PreviewModeConfig block it keyed '
+ + '(autoLogin / simulatedRole / simulatedUserName / readOnly / '
+ + 'expiresInSeconds / bannerMessage, declared on KernelContext and on the '
+ + 'TenantRuntimeContext extension). The exported '
+ + 'PreviewModeConfigSchema / PreviewModeConfig / PreviewModeConfigParsed '
+ + 'names left with the def',
+ replacement:
+ 'nothing declarative — the capability the block described was never '
+ + 'implemented by any layer, so there is no working configuration to '
+ + 'migrate to. Preview/demo DEPLOYMENTS belong to the deployment layer, '
+ + 'which owns auth per-project (ArtifactKernelFactory in the cloud '
+ + 'distribution); the OS_PREVIEW_MODE environment variable stays exactly '
+ + 'as it is — deployment ROUTING (widening the trusted-origin list for '
+ + 'preview subdomains), unrelated to identity. If a preview experience '
+ + 'becomes a product capability it re-declares fresh, with the '
+ + 'production-posture hard-refusal as the first-landed half (#11846 '
+ + 'ruling record)',
+ reason:
+ 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-27 on #11846 '
+ + '(decision-inbox batch 2, Option A: remove — all four decision facets '
+ + 'pointed the same way). The declaration was the sharpest '
+ + 'declared-≠-enforced shape on a SECURITY surface: the schema promised '
+ + '"bypass auth, simulate admin identity" and named a production guard '
+ + '"the runtime must enforce", and NO code path implemented either half. '
+ + 'Measured zero consumers in all three repos, each leg with positive '
+ + 'controls: objectstack — no runtime branches on the mode; the only '
+ + "non-declaration hits for RuntimeMode or mode === 'preview' are the "
+ + 'schema unit test, a type-alias pin and a measurement-test comment '
+ + '(re-verified at dispatch, 2026-08-27, origin/main 15bf9e8). objectui — '
+ + 'the #11846 card records the measurement. cloud — cloud#1651 (closed '
+ + '2026-08-26): previewMode appears only as a local variable for '
+ + 'OS_PREVIEW_MODE whose effect is adding preview-domain wildcards to '
+ + "better-auth's CSRF trusted origins; RuntimeMode has zero hits "
+ + 'repo-wide; the positive control ArtifactKernelFactory (where serve.ts '
+ + 'predicted preview auto-login would live if it existed) has 20+ hits '
+ + 'and never touches previewMode. An author — very often an AI (ADR-0033) '
+ + '— could write the six-key block per the reference docs, parse cleanly, '
+ + 'and get no behaviour and no diagnostic, while a reader of the docs had '
+ + 'no way to tell the block from the keys that work. Bookkeeping: the '
+ + "enum-VALUE half ('preview') puts nothing in RETIRED_KEYS_BY_MAJOR and "
+ + 'leaves the four surface ratchets untouched by itself — its '
+ + "prescription hangs on the enum's own error map (the HookBodyCapability "
+ + 'precedent); the KEY half is tombstoned with retiredKey() on the '
+ + 'non-strict KernelContextSchema (both walked-shape copies registered in '
+ + 'RETIRED_KEYS_BY_MAJOR[18]); the DEF half (kernel/PreviewModeConfig, '
+ + 'with no carrier left) is registered in RETIRED_DEFS_BY_MAJOR[18]. It '
+ + 'is a SEMANTIC entry rather than a D2 conversion because there is no '
+ + 'source to rewrite: a kernel context is constructed by host code at '
+ + 'boot — not a stack collection member, never stored as a sys_metadata '
+ + 'row — so the conversion chain has no seam that would ever see one '
+ + '(the kernel/Manifest:loading disposition). ADR-0049 / ADR-0087, '
+ + '#11846.',
+ acceptanceCriteria:
+ "No host constructs a kernel context with mode: 'preview' or a "
+ + 'previewMode block: both now fail tsc at the authoring site and fail '
+ + 'the parse with the prescription (pinned in '
+ + 'kernel/preview-mode-retirement.test.ts). Concretely, check three '
+ + "places. (1) Host boot code composing a KernelContext: delete `mode: "
+ + "'preview'` (mode defaults to production; use development for local "
+ + 'demo work) and delete any previewMode block — neither ever changed '
+ + 'runtime behaviour, so removing them changes nothing observable. '
+ + '(2) Code importing PreviewModeConfigSchema, PreviewModeConfig or '
+ + 'PreviewModeConfigParsed from @objectstack/spec or @objectstack/spec/'
+ + 'kernel: every one is TS2305 after upgrade; no working replacement '
+ + 'exists to point at, because the vocabulary described nothing real. '
+ + "(3) TypeScript branching on the RuntimeMode type (a mode === "
+ + "'preview' arm, a switch over modes): the arm is now unreachable and "
+ + 'an exhaustiveness check will fail to compile if it stays — that '
+ + 'compile error is the enforced channel for TypeScript consumers. '
+ + 'Preview deployment ROUTING is untouched: OS_PREVIEW_MODE and '
+ + 'OS_PREVIEW_BASE_DOMAINS keep working exactly as documented '
+ + '(deployment routing, never identity).',
+ },
{
id: 'memory-persistence-placeholder-refused',
surface: 'memory driver config `persistence.path` (file persistence and the `auto` ' +
@@ -7714,6 +7793,31 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly>
// declaration, and the registration-time refusal in
// `HotReloadManager.registerPlugin` is the door for the audience that exists.
'kernel/HotReloadConfig:watchPatterns',
+ // #11846 — ADR-0049 enforce-or-remove on the preview-mode block (maintainer
+ // ruling 2026-08-27, Option A: remove). The key was declared as an auth bypass
+ // — its docstring promised auto-login as a simulated admin and named a
+ // production guard "the runtime must enforce" — and NOTHING implemented any of
+ // it: zero consumers measured in objectstack, objectui and cloud (cloud#1651,
+ // 2026-08-26, with positive controls; `ArtifactKernelFactory` — where serve.ts
+ // predicted preview auto-login would live if anywhere — never touches it). An
+ // author could write the six-key block today, parse cleanly, and get no
+ // behaviour and no diagnostic, while the reference docs said the capability
+ // existed. Tombstoned with `retiredKey()` — `KernelContextSchema` is not
+ // `.strict()`, so a bare deletion would be a silent strip (#3733, ADR-0104).
+ //
+ // Registered here but NOT in `src/conversions/registry.ts`, the
+ // `kernel/Manifest:loading` reasoning: a kernel context is constructed by HOST
+ // CODE at boot — it is not a stack collection member (`PLURAL_TO_SINGULAR` has
+ // no entry for it) and nothing stores one as a `sys_metadata` row, so a
+ // MetadataConversion would be a transform with no seam that ever runs. The
+ // prescription reaches authors through the tombstone plus the D3 semantic
+ // entry `kernel-context-preview-mode-retired`.
+ //
+ // Registered under 18, not 17: v17.0.0 was cut before this landed, so the
+ // removal ships on the 17.x line (launch-window convention: accept-set
+ // narrowings ride minor releases) and the prescription lives at the major
+ // boundary where `migrate meta` users look (the #8495 / PR #8666 precedent).
+ 'kernel/KernelContext:previewMode',
// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes`
// block; one of NINE members tombstoned together. Census, registration major,
// and the why-no-D2-conversion reasoning are recorded once in the sibling
@@ -8065,6 +8169,14 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly>
// registration-time refusal in `PluginHealthMonitor.registerPlugin` is the door
// for the audience that exists.
'kernel/PluginHealthCheck:restartBackoff',
+ // #11846 — the `TenantRuntimeContextSchema` copy of
+ // `kernel/KernelContext:previewMode`: the def is `KernelContextSchema.extend(…)`,
+ // so the tombstone lands in this walked shape too and `authorable-surface/`
+ // marks it `[RETIRED]` separately. Registered per key, as gate (b) reads them —
+ // nothing radiates from the base (the `shared/FieldMapping:transform`
+ // precedent). See the base entry for the full record and the
+ // no-D2-conversion reasoning.
+ 'kernel/TenantRuntimeContext:previewMode',
// #12497 — the RESPONSE-side face of `security/ObjectPermission:allowPurge`
// (see that entry for the full rationale: ADR-0049 enforce-or-remove,
// maintainer ruling 2026-08-26 accepting #1883's recommendation B; the key
@@ -8959,6 +9071,24 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly>
// implementation first. See `18.kernel__AdvancedPluginLifecycleConfig.ts`
// for the family record.
'kernel/PluginUpdateStrategy',
+ // #11846 — `kernel/PreviewModeConfig` (the six-key preview/demo config block:
+ // `autoLogin` default true, `simulatedRole` default 'admin',
+ // `simulatedUserName`, `readOnly`, `expiresInSeconds`, `bannerMessage`). Its
+ // only carrier key, `KernelContext.previewMode`, is tombstoned in this same
+ // major (both walked-shape copies — see the two `…:previewMode` entries in
+ // RETIRED_KEYS_BY_MAJOR[18]), and an exported value schema with no consumer
+ // reads as a capability (#3950, the `PerformanceConfigSchema` rule) — so the
+ // def leaves the emitted set whole, with its `PreviewModeConfig` /
+ // `PreviewModeConfigParsed` types. Measured before removal: zero consumers of
+ // the schema or any of its six keys in objectstack, objectui or cloud
+ // (cloud#1651, 2026-08-26, positive controls on record). The declared
+ // behaviour (auto-login as a simulated admin, a read-only demo session) was
+ // never implemented by any layer; preview DEPLOYMENTS belong to the
+ // deployment layer, whose `OS_PREVIEW_MODE` is routing-only and stays. If a
+ // preview experience becomes a product capability it re-declares fresh, with
+ // the production-posture hard-refusal as the first-landed half (#11846 ruling
+ // record).
+ 'kernel/PreviewModeConfig',
// #10485 — `ui/BorderRadius` (the border-radius scale sub-block) left with `ui/Theme`:
// its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an
// exported value schema with no consumer reads as a capability). See
diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md
index b0e81065fe..55dd5f05e6 100644
--- a/skills/objectstack-platform/references/_index.md
+++ b/skills/objectstack-platform/references/_index.md
@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier
- `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Seed Import Strategy
-- `node_modules/@objectstack/spec/src/kernel/context.zod.ts` — Runtime Mode Enum
+- `node_modules/@objectstack/spec/src/kernel/context.zod.ts` — Exports: RuntimeMode, KernelContextSchema, TenantRuntimeContextSchema
- `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Exports: PluginPermissionsSchema, ManifestPermissionsSchema, PluginEnginesSchema, PluginRuntimeSchema, PluginPackagingSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-plugin.zod.ts` — Metadata Plugin Protocol
- `node_modules/@objectstack/spec/src/kernel/plugin-capability.zod.ts` — Plugin Capability Protocol