diff --git a/.changeset/identifier-schemas-declared-unenforced-retired.md b/.changeset/identifier-schemas-declared-unenforced-retired.md
new file mode 100644
index 0000000000..05574f5814
--- /dev/null
+++ b/.changeset/identifier-schemas-declared-unenforced-retired.md
@@ -0,0 +1,60 @@
+---
+"@objectstack/spec": minor
+---
+
+feat(spec): retire the six branded identifier schemas and EventNameSchema — declared-but-unenforced identifier layers removed (#13612, #13613)
+
+
+
+**BREAKING** published-export removals from `@objectstack/spec/shared`,
+shipped as `minor` under the repo's launch-window convention for breaking
+changes; both migration prescriptions are registered under protocol
+major 18. Maintainer ruling 2026-09-01 (director decision batch C, verbatim
+「同意」: retire) on both cards, under ADR-0049 enforce-or-remove.
+
+**#13612 — the six branded identifier schemas** (`shared/branded-types.zod.ts`,
+removed whole): `ObjectNameSchema`, `FieldNameSchema`, `ViewNameSchema`,
+`AppNameSchema`, `FlowNameSchema`, `RoleNameSchema`, with their type exports
+(`ObjectName`/`ObjectNameParsed` through `RoleName`/`RoleNameParsed`). The
+brands promised compile-time safety no consumer could obtain — no schema in
+either repository ever composed one, so nothing produced or accepted a
+branded value — while the surfaces they were named for are validated by
+inline regexes (`data/object.zod.ts`, `data/field.zod.ts`,
+`automation/flow.zod.ts`) or bare `SnakeCaseIdentifierSchema`
+(`ui/app.zod.ts`, `identity/position.zod.ts`). Those five real validators
+are the contract of record and are untouched. Binding was weighed and not
+adopted: it would silently change five surfaces' accept sets (the inline
+regexes admit a leading underscore the brand base does not).
+
+**#13613 — `EventNameSchema`** and its `EventName` type
+(`shared/identifiers.zod.ts`). Its only three binding fields
+(`EventTypeDefinitionSchema.name`, `EventSchema.name`,
+`EventMessageSchema.eventName`) had zero runtime consumers; the vocabulary
+the platform actually checks is the closed literal enums `DataEventType` /
+`BulkDataEventType` (`api/events.zod.ts`), which never referenced it. The
+three fields stay and widen to plain `z.string()` — every previously valid
+document stays valid. The enums are byte-for-byte untouched and stand as the
+only event-name contract; `WebSocketEventSchema.channel` stays a deliberate
+bare `z.string()` (the ruling adds no constraint there).
+
+FROM → TO:
+
+- `import { ObjectNameSchema, … , RoleNameSchema } from '@objectstack/spec/shared'`
+ → removed, no replacement brand layer (TS2305 on upgrade). Fix: parse an
+ identifier through the schema of the surface that stores it; for a
+ standalone check use `SnakeCaseIdentifierSchema` or
+ `SystemIdentifierSchema`, both still published from the same subpath.
+- `import { EventNameSchema } from '@objectstack/spec/shared'` → removed
+ (TS2305 on upgrade). Fix: delete the import; to validate platform event
+ names, parse through `DataEventType` / `BulkDataEventType` from
+ `@objectstack/spec/api`.
+- Documents parsed by `EventTypeDefinitionSchema`, `EventSchema` or
+ `EventMessageSchema`: no change required — the `name`/`eventName` accept
+ set widens from the dot-notation grammar to any string, so no stored
+ metadata breaks and no source rewrite ships.
+
+No authored document ever embedded a branded value and the event-field
+change is a widening, so there is no tombstone, no D2 conversion, and
+nothing for `objectstack migrate meta` to rewrite; the
+`RETIRED_DEFS_BY_MAJOR` rows (seven `shared/*` defs) plus the two D3
+semantic entries are the declaration.
diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx
index 2c6c34f213..f93099406f 100644
--- a/content/docs/getting-started/quick-reference.mdx
+++ b/content/docs/getting-started/quick-reference.mdx
@@ -206,7 +206,7 @@ from the provider itself, not from hand-written spec files.
|:---------|:-----------|:------------|:--------|
| **[Connector](/docs/references/integration/connector)** | `connector.zod.ts` | Connector | The connector protocol — auth, sync, webhooks, rate limiting |
-## Shared Protocol (5 of 8 schemas)
+## Shared Protocol (5 of 7 schemas)
Common utilities used across all protocols.
diff --git a/content/docs/references/api/websocket.mdx b/content/docs/references/api/websocket.mdx
index 4d8825fcb2..d28cc178e4 100644
--- a/content/docs/references/api/websocket.mdx
+++ b/content/docs/references/api/websocket.mdx
@@ -213,7 +213,7 @@ const result = AckMessageSchema.parse(data);
| **type** | `'event'` | ✅ | |
| **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent |
| **subscriptionId** | `string` | ✅ | Subscription ID this event belongs to |
-| **eventName** | `string` | ✅ | Event name |
+| **eventName** | `string` | ✅ | Event name (dot notation by convention; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **object** | `string` | optional | Object name the event relates to |
| **payload** | `any` | ✅ | Event payload data |
| **userId** | `string` | optional | User who triggered the event |
@@ -519,7 +519,7 @@ This schema accepts one of the following structures:
| **type** | `'event'` | ✅ | |
| **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent |
| **subscriptionId** | `string` | ✅ | Subscription ID this event belongs to |
-| **eventName** | `string` | ✅ | Event name |
+| **eventName** | `string` | ✅ | Event name (dot notation by convention; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **object** | `string` | optional | Object name the event relates to |
| **payload** | `any` | ✅ | Event payload data |
| **userId** | `string` | optional | User who triggered the event |
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index 713b5ab4c6..0182cb8fd3 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 — 1598 schemas across 14 protocol modules
+description: Every schema published by @objectstack/spec — 1591 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/. */}
@@ -29,11 +29,11 @@ counts are sums of the rows they head. Regenerate with
| [Kernel Protocol](/docs/references/kernel) | 30 | 163 | 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. |
+| [Shared Protocol](/docs/references/shared) | 7 | 25 | 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 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
-| **Total** | **200** | **1598** | 14 protocol modules |
+| **Total** | **199** | **1591** | 14 protocol modules |
---
@@ -286,17 +286,16 @@ Permission sets, row-level security, sharing rules, tenancy posture.
## Shared Protocol
-**Source:** `packages/spec/src/shared/` · **Import:** `@objectstack/spec/shared` · **8 pages, 32 schemas**
+**Source:** `packages/spec/src/shared/` · **Import:** `@objectstack/spec/shared` · **7 pages, 25 schemas**
Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums.
| File | Schemas |
| :--- | :--- |
-| [`branded-types.zod.ts`](/docs/references/shared/branded-types) | `AppName`, `FieldName`, `FlowName`, `ObjectName`, `RoleName`, `ViewName` |
| [`enums.zod.ts`](/docs/references/shared/enums) | `IsolationLevelEnum`, `MutationEventEnum`, `SortDirectionEnum`, `SortItem` |
| [`expression.zod.ts`](/docs/references/shared/expression) | `CronExpressionInput`, `Expression`, `ExpressionDialect`, `ExpressionInput`, `ExpressionMeta`, `Predicate`, `PredicateInput`, `TemplateExpressionInput` |
| [`http.zod.ts`](/docs/references/shared/http) | `CorsConfig`, `HttpMethod`, `HttpMethodSubset`, `HttpRequest`, `RateLimitConfig`, `StaticMount` |
-| [`identifiers.zod.ts`](/docs/references/shared/identifiers) | `EventName`, `MetadataItemName`, `SnakeCaseIdentifier`, `SystemIdentifier` |
+| [`identifiers.zod.ts`](/docs/references/shared/identifiers) | `MetadataItemName`, `SnakeCaseIdentifier`, `SystemIdentifier` |
| [`mapping.zod.ts`](/docs/references/shared/mapping) | `FieldMapping` |
| [`metadata-types.zod.ts`](/docs/references/shared/metadata-types) | `BaseMetadataRecord`, `MetadataFormat` |
| [`protection.zod.ts`](/docs/references/shared/protection) | `Protection` |
diff --git a/content/docs/references/kernel/events-bus.mdx b/content/docs/references/kernel/events-bus.mdx
index dc55194361..03b4c077e4 100644
--- a/content/docs/references/kernel/events-bus.mdx
+++ b/content/docs/references/kernel/events-bus.mdx
@@ -118,7 +118,7 @@ const result = EventBusConfigSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Event type name (lowercase with dots) |
+| **name** | `string` | ✅ | Event type name (dot notation by convention, e.g. order.created; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **version** | `string` | optional (default: `"1.0.0"`) | Event schema version |
| **schema** | `any` | optional | JSON Schema for event payload validation |
| **description** | `string` | optional | Event type description |
diff --git a/content/docs/references/kernel/events-core.mdx b/content/docs/references/kernel/events-core.mdx
index 4ecf7fc87f..ec20938ee3 100644
--- a/content/docs/references/kernel/events-core.mdx
+++ b/content/docs/references/kernel/events-core.mdx
@@ -28,7 +28,7 @@ const result = EventSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | optional | Unique event identifier |
-| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) |
+| **name** | `string` | ✅ | Event name (dot notation by convention, e.g. user.created, order.paid; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **payload** | `any` | ✅ | Event payload schema |
| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata |
@@ -93,7 +93,7 @@ const result = EventSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
-| **name** | `string` | ✅ | Event type name (lowercase with dots) |
+| **name** | `string` | ✅ | Event type name (dot notation by convention, e.g. order.created; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **version** | `string` | optional (default: `"1.0.0"`) | Event schema version |
| **schema** | `any` | optional | JSON Schema for event payload validation |
| **description** | `string` | optional | Event type description |
diff --git a/content/docs/references/kernel/events-dlq.mdx b/content/docs/references/kernel/events-dlq.mdx
index 6c1f84cbbc..cf4c08b938 100644
--- a/content/docs/references/kernel/events-dlq.mdx
+++ b/content/docs/references/kernel/events-dlq.mdx
@@ -40,7 +40,7 @@ const result = DeadLetterQueueEntrySchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | optional | Unique event identifier |
-| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) |
+| **name** | `string` | ✅ | Event name (dot notation by convention, e.g. user.created, order.paid; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **payload** | `any` | ✅ | Event payload schema |
| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata |
@@ -74,7 +74,7 @@ const result = DeadLetterQueueEntrySchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | optional | Unique event identifier |
-| **name** | `string` | ✅ | Event name (lowercase with dots, e.g., user.created, order.paid) |
+| **name** | `string` | ✅ | Event name (dot notation by convention, e.g. user.created, order.paid; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums) |
| **payload** | `any` | ✅ | Event payload schema |
| **metadata** | `{ source: string; timestamp: string; userId?: string; tenantId?: string; … }` | ✅ | Event metadata |
diff --git a/content/docs/references/shared/branded-types.mdx b/content/docs/references/shared/branded-types.mdx
deleted file mode 100644
index 04b43d9a86..0000000000
--- a/content/docs/references/shared/branded-types.mdx
+++ /dev/null
@@ -1,94 +0,0 @@
----
-title: Branded Types
-description: Branded Types protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-Branded Types for ObjectStack Identifiers
-
-Branded types provide compile-time safety by preventing accidental mixing
-of different identifier kinds. For example, you cannot pass an ObjectName
-where a FieldName is expected, even though both are strings at runtime.
-
-@example
-```ts
-import { ObjectNameSchema, FieldNameSchema } from '@objectstack/spec/shared';
-
-const objName = ObjectNameSchema.parse('project_task'); // ObjectName
-const fieldName = FieldNameSchema.parse('task_name'); // FieldName
-
-// TypeScript will catch this at compile time:
-// const fn: FieldName = objName; // Error!
-```
-
-
-**Source:** `packages/spec/src/shared/branded-types.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { AppNameSchema, FieldNameSchema, FlowNameSchema, ObjectNameSchema, RoleNameSchema, ViewNameSchema } from '@objectstack/spec/shared';
-import type { AppName, FieldName, FlowName, ObjectName, RoleName, ViewName } from '@objectstack/spec/shared';
-
-// Validate data
-const result = AppNameSchema.parse(data);
-```
-
----
-
-## AppName
-
-Branded app name (system identifier)
-
-**Type:** `string`
-
-
----
-
-## FieldName
-
-Branded field name (snake_case, no dots)
-
-**Type:** `string`
-
-
----
-
-## FlowName
-
-Branded flow name (system identifier)
-
-**Type:** `string`
-
-
----
-
-## ObjectName
-
-Branded object name (snake_case, no dots)
-
-**Type:** `string`
-
-
----
-
-## RoleName
-
-Branded role name (system identifier)
-
-**Type:** `string`
-
-
----
-
-## ViewName
-
-Branded view name (system identifier)
-
-**Type:** `string`
-
-
----
-
diff --git a/content/docs/references/shared/identifiers.mdx b/content/docs/references/shared/identifiers.mdx
index 1aa860a889..606e858075 100644
--- a/content/docs/references/shared/identifiers.mdx
+++ b/content/docs/references/shared/identifiers.mdx
@@ -12,22 +12,13 @@ description: Identifiers protocol schemas
## TypeScript Usage
```typescript
-import { EventNameSchema, MetadataItemNameSchema, SnakeCaseIdentifierSchema, SystemIdentifierSchema } from '@objectstack/spec/shared';
-import type { EventName, MetadataItemName, SnakeCaseIdentifier, SystemIdentifier } from '@objectstack/spec/shared';
+import { MetadataItemNameSchema, SnakeCaseIdentifierSchema, SystemIdentifierSchema } from '@objectstack/spec/shared';
+import type { MetadataItemName, SnakeCaseIdentifier, SystemIdentifier } from '@objectstack/spec/shared';
// Validate data
-const result = EventNameSchema.parse(data);
+const result = MetadataItemNameSchema.parse(data);
```
----
-
-## EventName
-
-Event name (lowercase with dot notation for namespacing)
-
-**Type:** `string`
-
-
---
## MetadataItemName
diff --git a/content/docs/references/shared/index.mdx b/content/docs/references/shared/index.mdx
index 10d4b6a4ff..8d51706398 100644
--- a/content/docs/references/shared/index.mdx
+++ b/content/docs/references/shared/index.mdx
@@ -6,7 +6,6 @@ description: Complete reference for all shared protocol schemas
This section contains all protocol schemas for the shared layer of ObjectStack.
-
diff --git a/content/docs/references/shared/meta.json b/content/docs/references/shared/meta.json
index 39f5f91ea3..32f137891d 100644
--- a/content/docs/references/shared/meta.json
+++ b/content/docs/references/shared/meta.json
@@ -1,7 +1,6 @@
{
"title": "Shared Protocol",
"pages": [
- "branded-types",
"enums",
"expression",
"http",
diff --git a/packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md b/packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md
index a4e3cefdf9..0474069d46 100644
--- a/packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md
+++ b/packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md
@@ -643,7 +643,7 @@ ObjectStack UI protocol provides three paths:
**Strengths:**
- Perfect foundational layer — small, focused, widely imported
-- `identifiers.zod.ts` is the naming convention enforcer (SystemIdentifierSchema, SnakeCaseIdentifierSchema, EventNameSchema)
+- `identifiers.zod.ts` is the naming convention enforcer (SystemIdentifierSchema, SnakeCaseIdentifierSchema)
- `connector-auth.zod.ts` uses `z.discriminatedUnion` perfectly (5 auth types)
**Key Files:**
diff --git a/packages/spec/api-surface/shared.json b/packages/spec/api-surface/shared.json
index 2767daa95e..b1272f7333 100644
--- a/packages/spec/api-surface/shared.json
+++ b/packages/spec/api-surface/shared.json
@@ -2,9 +2,6 @@
"description": "Every exported `name (kind)` of one published entry point of @objectstack/spec — the breadth half of the ADR-0059 backward-compatibility gate. Sharded by entry point (#5837) so two PRs touching different entry points never share a file. Reads the BUILT dist/*.d.ts: regenerate with `pnpm --filter @objectstack/spec gen:api-surface` after a real build.",
"entry": "./shared",
"exports": [
- "AppName (type)",
- "AppNameParsed (type)",
- "AppNameSchema (const)",
"ApplyProtectionContext (interface)",
"BaseMetadataRecord (type)",
"BaseMetadataRecordSchema (const)",
@@ -15,8 +12,6 @@
"CronExpressionInputSchema (const)",
"EXTERNAL_ERROR_CODES (const)",
"EXTERNAL_ERROR_HTTP_STATUS (const)",
- "EventName (type)",
- "EventNameSchema (const)",
"Expression (type)",
"ExpressionDialect (type)",
"ExpressionInput (type)",
@@ -31,12 +26,6 @@
"F (const)",
"FieldMapping (type)",
"FieldMappingSchema (const)",
- "FieldName (type)",
- "FieldNameParsed (type)",
- "FieldNameSchema (const)",
- "FlowName (type)",
- "FlowNameParsed (type)",
- "FlowNameSchema (const)",
"HttpMethod (type)",
"HttpMethodSubset (type)",
"HttpMethodSubsetSchema (const)",
@@ -59,9 +48,6 @@
"MutationEvent (type)",
"MutationEventEnum (const)",
"NormalizeStackInputOptions (interface)",
- "ObjectName (type)",
- "ObjectNameParsed (type)",
- "ObjectNameSchema (const)",
"ObjectStackRawIssue (type)",
"P (const)",
"PLURAL_TO_SINGULAR (const)",
@@ -76,9 +62,6 @@
"RateLimitConfigParsed (type)",
"RateLimitConfigSchema (const)",
"ResilientFetchOptions (interface)",
- "RoleName (type)",
- "RoleNameParsed (type)",
- "RoleNameSchema (const)",
"SINGULAR_TO_PLURAL (const)",
"SchemaDiffEntry (interface)",
"SchemaDiffEntryKind (type)",
@@ -97,9 +80,6 @@
"TemplateExpressionInputSchema (const)",
"VISIBILITY_ALIAS_KEYS (const)",
"VISIBILITY_STRICT_OPTIONS (const)",
- "ViewName (type)",
- "ViewNameParsed (type)",
- "ViewNameSchema (const)",
"applyProtection (function)",
"canonicalMetaUrlType (function)",
"cel (function)",
diff --git a/packages/spec/declaration-map/shared.json b/packages/spec/declaration-map/shared.json
index 510bd4dfb0..1f58d35aa8 100644
--- a/packages/spec/declaration-map/shared.json
+++ b/packages/spec/declaration-map/shared.json
@@ -2,16 +2,12 @@
"description": "TS declaration name → spec registry name (def key) for the schemas @objectstack/spec publishes: entries['ObjectSchemaBase'] === 'data/Object'. Composed from json-schema.manifest/ (the def keys), export-origins/ (which declaration each export resolves to), and a syntactic unwinding of wrapper initializers that recovers module-private base declarations (the ObjectSchemaBase case — see scripts/build-declaration-map.ts). A name that maps to two different def keys is DROPPED into `collisions` rather than guessed, so a lookup miss means \"not known to be an authorable container\". Consumers hold a changed line’s enclosing declaration name and ask which authorable container it declares (docs-audit anchor qualification is the funding one). Generated — never hand-edited; regenerate with `pnpm --filter @objectstack/spec gen:declaration-map` and read the diff.",
"category": "shared",
"entries": {
- "AppName": "shared/AppName",
- "AppNameSchema": "shared/AppName",
"BaseMetadataRecord": "shared/BaseMetadataRecord",
"BaseMetadataRecordSchema": "shared/BaseMetadataRecord",
"CorsConfig": "shared/CorsConfig",
"CorsConfigSchema": "shared/CorsConfig",
"CronExpressionInput": "shared/CronExpressionInput",
"CronExpressionInputSchema": "shared/CronExpressionInput",
- "EventName": "shared/EventName",
- "EventNameSchema": "shared/EventName",
"Expression": "shared/Expression",
"ExpressionDialect": "shared/ExpressionDialect",
"ExpressionInput": "shared/ExpressionInput",
@@ -21,10 +17,6 @@
"ExpressionSchema": "shared/Expression",
"FieldMapping": "shared/FieldMapping",
"FieldMappingSchema": "shared/FieldMapping",
- "FieldName": "shared/FieldName",
- "FieldNameSchema": "shared/FieldName",
- "FlowName": "shared/FlowName",
- "FlowNameSchema": "shared/FlowName",
"HttpMethod": "shared/HttpMethod",
"HttpMethodSubset": "shared/HttpMethodSubset",
"HttpMethodSubsetSchema": "shared/HttpMethodSubset",
@@ -36,8 +28,6 @@
"MetadataItemName": "shared/MetadataItemName",
"MetadataItemNameSchema": "shared/MetadataItemName",
"MutationEventEnum": "shared/MutationEventEnum",
- "ObjectName": "shared/ObjectName",
- "ObjectNameSchema": "shared/ObjectName",
"Predicate": "shared/Predicate",
"PredicateInput": "shared/PredicateInput",
"PredicateInputSchema": "shared/PredicateInput",
@@ -46,8 +36,6 @@
"ProtectionSchema": "shared/Protection",
"RateLimitConfig": "shared/RateLimitConfig",
"RateLimitConfigSchema": "shared/RateLimitConfig",
- "RoleName": "shared/RoleName",
- "RoleNameSchema": "shared/RoleName",
"SnakeCaseIdentifier": "shared/SnakeCaseIdentifier",
"SnakeCaseIdentifierSchema": "shared/SnakeCaseIdentifier",
"SortDirectionEnum": "shared/SortDirectionEnum",
@@ -58,9 +46,7 @@
"SystemIdentifier": "shared/SystemIdentifier",
"SystemIdentifierSchema": "shared/SystemIdentifier",
"TemplateExpressionInput": "shared/TemplateExpressionInput",
- "TemplateExpressionInputSchema": "shared/TemplateExpressionInput",
- "ViewName": "shared/ViewName",
- "ViewNameSchema": "shared/ViewName"
+ "TemplateExpressionInputSchema": "shared/TemplateExpressionInput"
},
"collisions": []
}
diff --git a/packages/spec/export-origins/shared.json b/packages/spec/export-origins/shared.json
index d20ab3a4d4..322504cfb7 100644
--- a/packages/spec/export-origins/shared.json
+++ b/packages/spec/export-origins/shared.json
@@ -2,9 +2,6 @@
"description": "Which SOURCE DECLARATION each name exported by one public entry point of @objectstack/spec resolves to, after its alias chain is unwound: `# ()`. Two exports share an origin string iff they are the same declaration — so equal origins across two entries are a harmless re-export, and different origins under one name are the #4411 dual-source trap. Generated from src/ (no build needed) and read by the export-surface pin tests, which compare against it instead of each building their own ts.createProgram — that was ~55s of compilation per CI lap and a non-deterministic timeout that ejected unrelated PRs from the merge queue (#4796). Sharded by entry point (#5837) so two retirement PRs never share a file. Carries NO line numbers: the pins asserted the line as `\\d+`, and recording it would rewrite this artifact on every edit that shifts a line in any .zod.ts. Regenerate with `pnpm --filter @objectstack/spec gen:export-origins` and read the diff.",
"entry": "./shared",
"exports": {
- "AppName": "src/shared/branded-types.zod.ts#AppName (type)",
- "AppNameParsed": "src/shared/branded-types.zod.ts#AppNameParsed (type)",
- "AppNameSchema": "src/shared/branded-types.zod.ts#AppNameSchema (const)",
"ApplyProtectionContext": "src/shared/protection.zod.ts#ApplyProtectionContext (interface)",
"BaseMetadataRecord": "src/shared/metadata-types.zod.ts#BaseMetadataRecord (type)",
"BaseMetadataRecordSchema": "src/shared/metadata-types.zod.ts#BaseMetadataRecordSchema (const)",
@@ -15,8 +12,6 @@
"CronExpressionInputSchema": "src/shared/expression.zod.ts#CronExpressionInputSchema (const)",
"EXTERNAL_ERROR_CODES": "src/shared/external-errors.ts#EXTERNAL_ERROR_CODES (const)",
"EXTERNAL_ERROR_HTTP_STATUS": "src/shared/external-errors.ts#EXTERNAL_ERROR_HTTP_STATUS (const)",
- "EventName": "src/shared/identifiers.zod.ts#EventName (type)",
- "EventNameSchema": "src/shared/identifiers.zod.ts#EventNameSchema (const)",
"Expression": "src/shared/expression.zod.ts#Expression (type)",
"ExpressionDialect": "src/shared/expression.zod.ts#ExpressionDialect (type)",
"ExpressionInput": "src/shared/expression.zod.ts#ExpressionInput (type)",
@@ -31,12 +26,6 @@
"F": "src/shared/expression.zod.ts#F (const)",
"FieldMapping": "src/shared/mapping.zod.ts#FieldMapping (type)",
"FieldMappingSchema": "src/shared/mapping.zod.ts#FieldMappingSchema (const)",
- "FieldName": "src/shared/branded-types.zod.ts#FieldName (type)",
- "FieldNameParsed": "src/shared/branded-types.zod.ts#FieldNameParsed (type)",
- "FieldNameSchema": "src/shared/branded-types.zod.ts#FieldNameSchema (const)",
- "FlowName": "src/shared/branded-types.zod.ts#FlowName (type)",
- "FlowNameParsed": "src/shared/branded-types.zod.ts#FlowNameParsed (type)",
- "FlowNameSchema": "src/shared/branded-types.zod.ts#FlowNameSchema (const)",
"HttpMethod": "src/shared/http.zod.ts#HttpMethod (type)",
"HttpMethodSubset": "src/shared/http.zod.ts#HttpMethodSubset (type)",
"HttpMethodSubsetSchema": "src/shared/http.zod.ts#HttpMethodSubsetSchema (const)",
@@ -59,9 +48,6 @@
"MutationEvent": "src/shared/enums.zod.ts#MutationEvent (type)",
"MutationEventEnum": "src/shared/enums.zod.ts#MutationEventEnum (const)",
"NormalizeStackInputOptions": "src/shared/metadata-collection.zod.ts#NormalizeStackInputOptions (interface)",
- "ObjectName": "src/shared/branded-types.zod.ts#ObjectName (type)",
- "ObjectNameParsed": "src/shared/branded-types.zod.ts#ObjectNameParsed (type)",
- "ObjectNameSchema": "src/shared/branded-types.zod.ts#ObjectNameSchema (const)",
"ObjectStackRawIssue": "src/shared/error-map.zod.ts#ObjectStackRawIssue (type)",
"P": "src/shared/expression.zod.ts#P (const)",
"PLURAL_TO_SINGULAR": "src/meta-spelling/manifest-collection-spelling.ts#PLURAL_TO_SINGULAR (const)",
@@ -76,9 +62,6 @@
"RateLimitConfigParsed": "src/shared/http.zod.ts#RateLimitConfigParsed (type)",
"RateLimitConfigSchema": "src/shared/http.zod.ts#RateLimitConfigSchema (const)",
"ResilientFetchOptions": "src/shared/resilient-fetch.ts#ResilientFetchOptions (interface)",
- "RoleName": "src/shared/branded-types.zod.ts#RoleName (type)",
- "RoleNameParsed": "src/shared/branded-types.zod.ts#RoleNameParsed (type)",
- "RoleNameSchema": "src/shared/branded-types.zod.ts#RoleNameSchema (const)",
"SINGULAR_TO_PLURAL": "src/meta-spelling/manifest-collection-spelling.ts#SINGULAR_TO_PLURAL (const)",
"SchemaDiffEntry": "src/shared/external-errors.ts#SchemaDiffEntry (interface)",
"SchemaDiffEntryKind": "src/shared/external-errors.ts#SchemaDiffEntryKind (type)",
@@ -97,9 +80,6 @@
"TemplateExpressionInputSchema": "src/shared/expression.zod.ts#TemplateExpressionInputSchema (const)",
"VISIBILITY_ALIAS_KEYS": "src/shared/visibility.ts#VISIBILITY_ALIAS_KEYS (const)",
"VISIBILITY_STRICT_OPTIONS": "src/shared/visibility.ts#VISIBILITY_STRICT_OPTIONS (const)",
- "ViewName": "src/shared/branded-types.zod.ts#ViewName (type)",
- "ViewNameParsed": "src/shared/branded-types.zod.ts#ViewNameParsed (type)",
- "ViewNameSchema": "src/shared/branded-types.zod.ts#ViewNameSchema (const)",
"applyProtection": "src/shared/protection.zod.ts#applyProtection (function)",
"canonicalMetaUrlType": "src/meta-spelling/metadata-url-spelling.ts#canonicalMetaUrlType (function)",
"cel": "src/shared/expression.zod.ts#cel (function)",
diff --git a/packages/spec/json-schema.manifest/shared.json b/packages/spec/json-schema.manifest/shared.json
index f29ff66bb5..15eb80fe54 100644
--- a/packages/spec/json-schema.manifest/shared.json
+++ b/packages/spec/json-schema.manifest/shared.json
@@ -2,18 +2,14 @@
"description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts for one category. Sharded by category (#5837); the gate reads the whole json-schema.manifest/ directory as ONE set. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema. DELETING a key is gated too (#4725): the removal is measured against this directory at the merge base with origin/main — which the commit under test cannot rewrite — and every def that leaves the published set must be declared in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts), or in RENAMED_DEFS (scripts/lib/renamed-defs.ts) when it is a rename rather than a removal. See #2978, #4725.",
"category": "shared",
"schemas": [
- "shared/AppName",
"shared/BaseMetadataRecord",
"shared/CorsConfig",
"shared/CronExpressionInput",
- "shared/EventName",
"shared/Expression",
"shared/ExpressionDialect",
"shared/ExpressionInput",
"shared/ExpressionMeta",
"shared/FieldMapping",
- "shared/FieldName",
- "shared/FlowName",
"shared/HttpMethod",
"shared/HttpMethodSubset",
"shared/HttpRequest",
@@ -21,18 +17,15 @@
"shared/MetadataFormat",
"shared/MetadataItemName",
"shared/MutationEventEnum",
- "shared/ObjectName",
"shared/Predicate",
"shared/PredicateInput",
"shared/Protection",
"shared/RateLimitConfig",
- "shared/RoleName",
"shared/SnakeCaseIdentifier",
"shared/SortDirectionEnum",
"shared/SortItem",
"shared/StaticMount",
"shared/SystemIdentifier",
- "shared/TemplateExpressionInput",
- "shared/ViewName"
+ "shared/TemplateExpressionInput"
]
}
diff --git a/packages/spec/llms.txt b/packages/spec/llms.txt
index d50fc335ae..b0b5b3aa74 100644
--- a/packages/spec/llms.txt
+++ b/packages/spec/llms.txt
@@ -77,7 +77,7 @@ const query = {
---
-## 3. Schema Inventory by Domain (207 schemas)
+## 3. Schema Inventory by Domain (206 schemas)
Counted as `*.zod.ts` modules under `packages/spec/src//` — the sources
that ship in this tarball (`files` includes `src/**/*.zod.ts`), so every number
@@ -91,7 +91,7 @@ here is verifiable from the installed package.
| api | 30 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope, Package Lifecycle |
| ui | 18 | View, App, Action, Dashboard, Page, Chart, Component, Animation |
| automation | 13 | Flow, Approval, BPMN Interop, Control Flow, State Machine, Webhook |
-| shared | 13 | Enums, HTTP, Identifiers, Mapping, Metadata Types, Connector Auth, Retry Policy |
+| shared | 12 | Enums, HTTP, Identifiers, Mapping, Metadata Types, Connector Auth, Retry Policy |
| ai | 11 | Agent, Conversation, Knowledge Source/Document, Model Registry, MCP, Skill, Tool |
| cloud | 11 | Marketplace, Developer Portal, App Store, Environment, Package, Tenant |
| identity | 5 | Identity, Organization, Position, SCIM, Eval User |
diff --git a/packages/spec/src/api/websocket.zod.ts b/packages/spec/src/api/websocket.zod.ts
index 90ccce3243..d8f3f2cbab 100644
--- a/packages/spec/src/api/websocket.zod.ts
+++ b/packages/spec/src/api/websocket.zod.ts
@@ -1,7 +1,6 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
-import { EventNameSchema } from '../shared/identifiers.zod';
import { PresenceStatus } from './realtime-shared.zod';
// Re-export shared PresenceStatus for backward compatibility
@@ -296,7 +295,11 @@ export type UnsubscribeMessage = z.input;
export const EventMessageSchema = lazySchema(() => BaseWebSocketMessage.extend({
type: z.literal('event'),
subscriptionId: z.string().uuid().describe('Subscription ID this event belongs to'),
- eventName: EventNameSchema.describe('Event name'),
+ // [#13613] Carried `EventNameSchema` until ADR-0049 enforce-or-remove
+ // retired it — no runtime consumer ever parsed through this schema, and the
+ // platform-checked event vocabulary is the closed DataEventType /
+ // BulkDataEventType enums (api/events.zod.ts).
+ eventName: z.string().describe('Event name (dot notation by convention; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums)'),
object: z.string().optional().describe('Object name the event relates to'),
payload: z.unknown().describe('Event payload data'),
userId: z.string().optional().describe('User who triggered the event'),
diff --git a/packages/spec/src/kernel/events.test.ts b/packages/spec/src/kernel/events.test.ts
index 830778a004..3c91808ea6 100644
--- a/packages/spec/src/kernel/events.test.ts
+++ b/packages/spec/src/kernel/events.test.ts
@@ -128,16 +128,30 @@ describe('EventSchema', () => {
expect(() => EventSchema.parse(event)).not.toThrow();
});
- it('should validate event name format (snake_case with dots)', () => {
- const validNames = [
+ // [#13613] `name` carried `EventNameSchema`'s dot-notation grammar until
+ // ADR-0049 enforce-or-remove retired it: no runtime consumer ever parsed
+ // through this schema, and the platform-checked event vocabulary is the
+ // closed `DataEventType` / `BulkDataEventType` enums (api/events.zod.ts).
+ // This pin holds the WIDENING: spellings the retired grammar refused
+ // (PascalCase, kebab-case, leading digit/dot) now parse — a regression to a
+ // schema-level grammar here should be a deliberate ruling, not drift.
+ it('accepts event names as plain strings — the dot-notation grammar is retired (#13613)', () => {
+ const names = [
+ // conventional dot-notation spellings (valid before and after)
'user.created',
'account.updated',
'opportunity.stage.changed',
'payment.webhook.received',
'data_import.completed',
+ // spellings the retired EventNameSchema grammar refused
+ 'User.Created', // PascalCase
+ 'user-created', // kebab-case
+ 'userCreated', // camelCase
+ '123.invalid', // starts with number
+ '.invalid', // starts with dot
];
- validNames.forEach(name => {
+ names.forEach(name => {
const event = {
name,
payload: {},
@@ -150,24 +164,6 @@ describe('EventSchema', () => {
});
});
- it('should reject invalid event name formats', () => {
- const invalidNames = [
- 'User.Created', // PascalCase
- 'user-created', // kebab-case
- 'userCreated', // camelCase
- '123.invalid', // starts with number
- '.invalid', // starts with dot
- ];
-
- invalidNames.forEach(name => {
- expect(() => EventSchema.parse({
- name,
- payload: {},
- metadata: { source: 'system', timestamp: '2024-01-15T10:30:00Z' },
- })).toThrow();
- });
- });
-
it('should accept various payload types', () => {
const payloads = [
{ id: '123', name: 'Test' },
diff --git a/packages/spec/src/kernel/events/core.zod.ts b/packages/spec/src/kernel/events/core.zod.ts
index 4d9ed70c6b..b4874f5ba7 100644
--- a/packages/spec/src/kernel/events/core.zod.ts
+++ b/packages/spec/src/kernel/events/core.zod.ts
@@ -1,7 +1,12 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
-import { EventNameSchema } from '../../shared/identifiers.zod';
+// [#13613] The `name` fields below carried `EventNameSchema` (a dot-notation
+// grammar from shared/identifiers.zod) until ADR-0049 enforce-or-remove
+// retired it: no runtime consumer ever parsed through these schemas, and the
+// event vocabulary the platform actually checks is the closed literal enums
+// `DataEventType` / `BulkDataEventType` (`api/events.zod.ts`). The fields stay
+// as plain strings; the enums are the only event-name contract.
// ==========================================
// Event Priority
@@ -91,7 +96,7 @@ export const EventMetadataSchema = lazySchema(() => z.object({
* }
*/
export const EventTypeDefinitionSchema = lazySchema(() => z.object({
- name: EventNameSchema.describe('Event type name (lowercase with dots)'),
+ name: z.string().describe('Event type name (dot notation by convention, e.g. order.created; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums)'),
version: z.string().default('1.0.0').describe('Event schema version'),
schema: z.unknown().optional().describe('JSON Schema for event payload validation'),
description: z.string().optional().describe('Event type description'),
@@ -119,7 +124,7 @@ export const EventSchema = lazySchema(() => z.object({
/**
* Event name
*/
- name: EventNameSchema.describe('Event name (lowercase with dots, e.g., user.created, order.paid)'),
+ name: z.string().describe('Event name (dot notation by convention, e.g. user.created, order.paid; the platform-checked event vocabulary is the closed DataEventType / BulkDataEventType enums)'),
/**
* Event payload
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__AppName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__AppName.ts
new file mode 100644
index 0000000000..5db02a2735
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__AppName.ts
@@ -0,0 +1,12 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. No schema in either repo ever composed `AppNameSchema`,
+// so the promised brand safety was unobtainable. The real app-name contract
+// is the bare `SnakeCaseIdentifierSchema` at `ui/app.zod.ts` — untouched by
+// this retirement. No tombstone and no D2 conversion (no authored document
+// ever embedded a branded value); this table plus the D3 semantic entry
+// `branded-identifier-schemas-retired` are the declaration.
+export const entry = 'shared/AppName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__EventName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__EventName.ts
new file mode 100644
index 0000000000..5bfadec370
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__EventName.ts
@@ -0,0 +1,16 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13613 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire). `EventNameSchema` presented itself as the platform's
+// event-name grammar while nothing that runs consumed it: its only three
+// binding schemas (`EventTypeDefinitionSchema.name`, `EventSchema.name`,
+// `EventMessageSchema.eventName`) have zero runtime consumers, and the
+// vocabulary the platform actually checks is the closed literal enums
+// `DataEventType` / `BulkDataEventType` (`api/events.zod.ts`), which never
+// referenced it. The three fields stay as plain `z.string()`; the enums are
+// the only event-name contract. `WebSocketEventSchema.channel` stays a
+// deliberate `z.string()` — the ruling adds no constraint there. No authored
+// document is invalidated (the accept set at the three fields widens), so no
+// tombstone and no D2 conversion — this table plus the D3 semantic entry
+// `event-name-schema-retired` are the declaration (the #8715 route-3 shape).
+export const entry = 'shared/EventName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__FieldName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__FieldName.ts
new file mode 100644
index 0000000000..0a4d7284c6
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__FieldName.ts
@@ -0,0 +1,13 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. No schema in either repo ever composed `FieldNameSchema`,
+// so the promised brand safety was unobtainable. The real field-name contract
+// is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+// `data/field.zod.ts` — untouched by this retirement. No tombstone and no D2
+// conversion (no authored document ever embedded a branded value); this table
+// plus the D3 semantic entry `branded-identifier-schemas-retired` are the
+// declaration.
+export const entry = 'shared/FieldName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__FlowName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__FlowName.ts
new file mode 100644
index 0000000000..1bc7d9a1df
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__FlowName.ts
@@ -0,0 +1,13 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. No schema in either repo ever composed `FlowNameSchema`,
+// so the promised brand safety was unobtainable. The real flow-name contract
+// is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+// `automation/flow.zod.ts` — untouched by this retirement. No tombstone and
+// no D2 conversion (no authored document ever embedded a branded value); this
+// table plus the D3 semantic entry `branded-identifier-schemas-retired` are
+// the declaration.
+export const entry = 'shared/FlowName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__ObjectName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__ObjectName.ts
new file mode 100644
index 0000000000..bf687e46a9
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__ObjectName.ts
@@ -0,0 +1,16 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. The brand promised compile-time safety no consumer could
+// obtain: no schema in either repo ever composed `ObjectNameSchema`, so
+// nothing produced or accepted a branded value. The real object-name contract
+// is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+// `data/object.zod.ts` — untouched by this retirement (the ruling keeps the
+// five surfaces' real validators as the contract of record). No authored
+// document ever embedded a branded value, so no tombstone and no D2
+// conversion — this table plus the D3 semantic entry
+// `branded-identifier-schemas-retired` are the declaration (the #8715
+// route-3 shape).
+export const entry = 'shared/ObjectName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__RoleName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__RoleName.ts
new file mode 100644
index 0000000000..fec3e78c21
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__RoleName.ts
@@ -0,0 +1,13 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. No schema in either repo ever composed `RoleNameSchema`,
+// so the promised brand safety was unobtainable. The real role/position-name
+// contract is the bare `SnakeCaseIdentifierSchema` at
+// `identity/position.zod.ts` — untouched by this retirement. No tombstone and
+// no D2 conversion (no authored document ever embedded a branded value); this
+// table plus the D3 semantic entry `branded-identifier-schemas-retired` are
+// the declaration.
+export const entry = 'shared/RoleName';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.shared__ViewName.ts b/packages/spec/src/migrations/entries/retired-defs/18.shared__ViewName.ts
new file mode 100644
index 0000000000..17d6bedae9
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.shared__ViewName.ts
@@ -0,0 +1,12 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+// batch C: retire; binding was weighed and not adopted). One of the six
+// branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+// with the module. No schema in either repo ever composed `ViewNameSchema`,
+// so the promised brand safety was unobtainable. View names are validated
+// where views are declared, not through a brand. No tombstone and no D2
+// conversion (no authored document ever embedded a branded value); this table
+// plus the D3 semantic entry `branded-identifier-schemas-retired` are the
+// declaration.
+export const entry = 'shared/ViewName';
diff --git a/packages/spec/src/migrations/entries/semantic/18.branded-identifier-schemas-retired.ts b/packages/spec/src/migrations/entries/semantic/18.branded-identifier-schemas-retired.ts
new file mode 100644
index 0000000000..05640b74a3
--- /dev/null
+++ b/packages/spec/src/migrations/entries/semantic/18.branded-identifier-schemas-retired.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SemanticMigration } from '../../types.js';
+
+export const entry: SemanticMigration = {
+ id: 'branded-identifier-schemas-retired',
+ surface:
+ 'the six branded identifier schemas of `@objectstack/spec/shared` '
+ + '(`shared/branded-types.zod.ts`, removed whole): `ObjectNameSchema`, '
+ + '`FieldNameSchema`, `ViewNameSchema`, `AppNameSchema`, `FlowNameSchema`, '
+ + '`RoleNameSchema`, and their type exports (`ObjectName`/`ObjectNameParsed` '
+ + 'through `RoleName`/`RoleNameParsed`).',
+ replacement:
+ '(removed — no replacement brand layer. Parse an identifier through the '
+ + 'schema of the surface that stores it: object and field names through '
+ + '`ObjectSchema`/`FieldSchema` (inline snake_case regex), flow names '
+ + 'through `FlowSchema`, app names through `AppSchema` '
+ + '(`SnakeCaseIdentifierSchema`), position/role names through '
+ + '`PositionSchema`. A caller that wants a standalone identifier check '
+ + 'uses `SnakeCaseIdentifierSchema` or `SystemIdentifierSchema` from '
+ + '`@objectstack/spec/shared` directly — both stay published.)',
+ reason:
+ 'Maintainer ruling 2026-09-01 on #13612 (director decision batch C, '
+ + 'verbatim 「同意」: retire) — ADR-0049 enforce-or-remove. The brands '
+ + 'promised compile-time safety ("you cannot pass an ObjectName where a '
+ + 'FieldName is expected") that no consumer could obtain: no schema in '
+ + 'either repository ever composed a brand, so nothing produced or '
+ + 'accepted a branded value, while the surfaces the brands were named for '
+ + 'are validated by inline regexes or bare `SnakeCaseIdentifierSchema` '
+ + 'three files away. Binding was weighed and not adopted: zero consumers '
+ + 'exist, binding would silently change five surfaces\' accept sets (the '
+ + 'inline regexes admit a leading underscore the brand base does not), '
+ + 'and a future real need for centralized identifier grammar re-opens '
+ + 'freely against actual pull.',
+ acceptanceCriteria:
+ 'No code imports any of the six schemas or their types from '
+ + '`@objectstack/spec/shared` (TS2305 after upgrade — the module is '
+ + 'removed, not stubbed); the five surfaces\' validators are byte-for-byte '
+ + 'untouched (inline regexes at `data/object.zod.ts`, `data/field.zod.ts`, '
+ + '`automation/flow.zod.ts`; bare `SnakeCaseIdentifierSchema` at '
+ + '`ui/app.zod.ts`, `identity/position.zod.ts`); '
+ + '`SnakeCaseIdentifierSchema` and `SystemIdentifierSchema` themselves '
+ + 'remain published and unchanged; the six def keys (`shared/ObjectName`, '
+ + '`shared/FieldName`, `shared/ViewName`, `shared/AppName`, '
+ + '`shared/FlowName`, `shared/RoleName`) leave '
+ + '`json-schema.manifest/shared.json` in the same change that registers '
+ + 'this entry. No authored metadata document ever embedded a branded '
+ + 'value, so no source rewrite ships and `objectstack migrate meta` has '
+ + 'nothing to visit.',
+};
diff --git a/packages/spec/src/migrations/entries/semantic/18.event-name-schema-retired.ts b/packages/spec/src/migrations/entries/semantic/18.event-name-schema-retired.ts
new file mode 100644
index 0000000000..d6d60040ad
--- /dev/null
+++ b/packages/spec/src/migrations/entries/semantic/18.event-name-schema-retired.ts
@@ -0,0 +1,47 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SemanticMigration } from '../../types.js';
+
+export const entry: SemanticMigration = {
+ id: 'event-name-schema-retired',
+ surface:
+ '`EventNameSchema` and its `EventName` type '
+ + '(`@objectstack/spec/shared`, `shared/identifiers.zod.ts`), and the '
+ + 'dot-notation grammar it imposed on its only three binding fields: '
+ + '`EventTypeDefinitionSchema.name` and `EventSchema.name` '
+ + '(`kernel/events/core.zod.ts`) and `EventMessageSchema.eventName` '
+ + '(`api/websocket.zod.ts`).',
+ replacement:
+ '(removed — no replacement grammar layer. The three binding fields stay '
+ + 'and widen to plain `z.string()`; the event vocabulary the platform '
+ + 'actually checks is the closed literal enums `DataEventType` / '
+ + '`BulkDataEventType` (`@objectstack/spec/api`, `api/events.zod.ts`), '
+ + 'which stand as the only event-name contract. A caller that imported '
+ + '`EventNameSchema` for standalone validation deletes the import; if it '
+ + 'was validating platform event names, it parses through the enums '
+ + 'instead.)',
+ reason:
+ 'Maintainer ruling 2026-09-01 on #13613 (director decision batch C, '
+ + 'verbatim 「同意」: retire) — ADR-0049 enforce-or-remove. The schema '
+ + 'presented itself as the platform\'s event-name grammar while nothing '
+ + 'that runs consumed its three binding schemas, and the closed enums '
+ + 'that do the real checking never referenced it. The event surface is '
+ + 'platform-defined, not author-extensible, so a grammar layer for a '
+ + 'hypothetical extension surface is a trap, not a reserve: a generator '
+ + 'satisfying `EventNameSchema` has satisfied nothing the platform will '
+ + 'check, while one emitting outside the closed enums is refused by a '
+ + 'rule the identifier file never mentioned.',
+ acceptanceCriteria:
+ 'No code imports `EventNameSchema` or `EventName` from '
+ + '`@objectstack/spec/shared` (TS2305 after upgrade); '
+ + '`EventTypeDefinitionSchema.name`, `EventSchema.name` and '
+ + '`EventMessageSchema.eventName` parse as plain strings (the accept set '
+ + 'at those three fields widens — every previously valid document stays '
+ + 'valid, so no source rewrite ships and `objectstack migrate meta` has '
+ + 'nothing to visit); `DataEventType` / `BulkDataEventType` are '
+ + 'byte-for-byte untouched; `WebSocketEventSchema.channel` remains a '
+ + 'deliberate bare `z.string()` (the ruling adds no constraint there); '
+ + 'the `shared/EventName` def key leaves '
+ + '`json-schema.manifest/shared.json` in the same change that registers '
+ + 'this entry.',
+};
diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts
index d755c8224f..9682d3cfed 100644
--- a/packages/spec/src/migrations/registry.ts
+++ b/packages/spec/src/migrations/registry.ts
@@ -5510,6 +5510,52 @@ const step18: MigrationStep = {
'(invitation, admin create-user / import, SCIM, or an operator-registered identity provider) ' +
'and that anonymous sign-up now answers 403 SELF_REGISTRATION_CLOSED.',
},
+ {
+ id: 'branded-identifier-schemas-retired',
+ surface:
+ 'the six branded identifier schemas of `@objectstack/spec/shared` '
+ + '(`shared/branded-types.zod.ts`, removed whole): `ObjectNameSchema`, '
+ + '`FieldNameSchema`, `ViewNameSchema`, `AppNameSchema`, `FlowNameSchema`, '
+ + '`RoleNameSchema`, and their type exports (`ObjectName`/`ObjectNameParsed` '
+ + 'through `RoleName`/`RoleNameParsed`).',
+ replacement:
+ '(removed — no replacement brand layer. Parse an identifier through the '
+ + 'schema of the surface that stores it: object and field names through '
+ + '`ObjectSchema`/`FieldSchema` (inline snake_case regex), flow names '
+ + 'through `FlowSchema`, app names through `AppSchema` '
+ + '(`SnakeCaseIdentifierSchema`), position/role names through '
+ + '`PositionSchema`. A caller that wants a standalone identifier check '
+ + 'uses `SnakeCaseIdentifierSchema` or `SystemIdentifierSchema` from '
+ + '`@objectstack/spec/shared` directly — both stay published.)',
+ reason:
+ 'Maintainer ruling 2026-09-01 on #13612 (director decision batch C, '
+ + 'verbatim 「同意」: retire) — ADR-0049 enforce-or-remove. The brands '
+ + 'promised compile-time safety ("you cannot pass an ObjectName where a '
+ + 'FieldName is expected") that no consumer could obtain: no schema in '
+ + 'either repository ever composed a brand, so nothing produced or '
+ + 'accepted a branded value, while the surfaces the brands were named for '
+ + 'are validated by inline regexes or bare `SnakeCaseIdentifierSchema` '
+ + 'three files away. Binding was weighed and not adopted: zero consumers '
+ + 'exist, binding would silently change five surfaces\' accept sets (the '
+ + 'inline regexes admit a leading underscore the brand base does not), '
+ + 'and a future real need for centralized identifier grammar re-opens '
+ + 'freely against actual pull.',
+ acceptanceCriteria:
+ 'No code imports any of the six schemas or their types from '
+ + '`@objectstack/spec/shared` (TS2305 after upgrade — the module is '
+ + 'removed, not stubbed); the five surfaces\' validators are byte-for-byte '
+ + 'untouched (inline regexes at `data/object.zod.ts`, `data/field.zod.ts`, '
+ + '`automation/flow.zod.ts`; bare `SnakeCaseIdentifierSchema` at '
+ + '`ui/app.zod.ts`, `identity/position.zod.ts`); '
+ + '`SnakeCaseIdentifierSchema` and `SystemIdentifierSchema` themselves '
+ + 'remain published and unchanged; the six def keys (`shared/ObjectName`, '
+ + '`shared/FieldName`, `shared/ViewName`, `shared/AppName`, '
+ + '`shared/FlowName`, `shared/RoleName`) leave '
+ + '`json-schema.manifest/shared.json` in the same change that registers '
+ + 'this entry. No authored metadata document ever embedded a branded '
+ + 'value, so no source rewrite ships and `objectstack migrate meta` has '
+ + 'nothing to visit.',
+ },
{
id: 'cbp-master-detail-required-forced',
surface: 'object.fields..required on a `master_detail` reference under '
@@ -6187,6 +6233,49 @@ const step18: MigrationStep = {
+ '(`{"address.city": …}`) needs NO action — it is deliberately not judged. Reads complete '
+ 'with no `INVALID_FIELD` naming a dotted filter key, at either door.',
},
+ {
+ id: 'event-name-schema-retired',
+ surface:
+ '`EventNameSchema` and its `EventName` type '
+ + '(`@objectstack/spec/shared`, `shared/identifiers.zod.ts`), and the '
+ + 'dot-notation grammar it imposed on its only three binding fields: '
+ + '`EventTypeDefinitionSchema.name` and `EventSchema.name` '
+ + '(`kernel/events/core.zod.ts`) and `EventMessageSchema.eventName` '
+ + '(`api/websocket.zod.ts`).',
+ replacement:
+ '(removed — no replacement grammar layer. The three binding fields stay '
+ + 'and widen to plain `z.string()`; the event vocabulary the platform '
+ + 'actually checks is the closed literal enums `DataEventType` / '
+ + '`BulkDataEventType` (`@objectstack/spec/api`, `api/events.zod.ts`), '
+ + 'which stand as the only event-name contract. A caller that imported '
+ + '`EventNameSchema` for standalone validation deletes the import; if it '
+ + 'was validating platform event names, it parses through the enums '
+ + 'instead.)',
+ reason:
+ 'Maintainer ruling 2026-09-01 on #13613 (director decision batch C, '
+ + 'verbatim 「同意」: retire) — ADR-0049 enforce-or-remove. The schema '
+ + 'presented itself as the platform\'s event-name grammar while nothing '
+ + 'that runs consumed its three binding schemas, and the closed enums '
+ + 'that do the real checking never referenced it. The event surface is '
+ + 'platform-defined, not author-extensible, so a grammar layer for a '
+ + 'hypothetical extension surface is a trap, not a reserve: a generator '
+ + 'satisfying `EventNameSchema` has satisfied nothing the platform will '
+ + 'check, while one emitting outside the closed enums is refused by a '
+ + 'rule the identifier file never mentioned.',
+ acceptanceCriteria:
+ 'No code imports `EventNameSchema` or `EventName` from '
+ + '`@objectstack/spec/shared` (TS2305 after upgrade); '
+ + '`EventTypeDefinitionSchema.name`, `EventSchema.name` and '
+ + '`EventMessageSchema.eventName` parse as plain strings (the accept set '
+ + 'at those three fields widens — every previously valid document stays '
+ + 'valid, so no source rewrite ships and `objectstack migrate meta` has '
+ + 'nothing to visit); `DataEventType` / `BulkDataEventType` are '
+ + 'byte-for-byte untouched; `WebSocketEventSchema.channel` remains a '
+ + 'deliberate bare `z.string()` (the ruling adds no constraint there); '
+ + 'the `shared/EventName` def key leaves '
+ + '`json-schema.manifest/shared.json` in the same change that registers '
+ + 'this entry.',
+ },
{
id: 'field-master-detail-set-null-refused',
surface: "object field `deleteBehavior: 'set_null'` authored on a `master_detail` field",
@@ -9843,6 +9932,87 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly>
// the production-posture hard-refusal as the first-landed half (#11846 ruling
// record).
'kernel/PreviewModeConfig',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. No schema in either repo ever composed `AppNameSchema`,
+ // so the promised brand safety was unobtainable. The real app-name contract
+ // is the bare `SnakeCaseIdentifierSchema` at `ui/app.zod.ts` — untouched by
+ // this retirement. No tombstone and no D2 conversion (no authored document
+ // ever embedded a branded value); this table plus the D3 semantic entry
+ // `branded-identifier-schemas-retired` are the declaration.
+ 'shared/AppName',
+ // #13613 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire). `EventNameSchema` presented itself as the platform's
+ // event-name grammar while nothing that runs consumed it: its only three
+ // binding schemas (`EventTypeDefinitionSchema.name`, `EventSchema.name`,
+ // `EventMessageSchema.eventName`) have zero runtime consumers, and the
+ // vocabulary the platform actually checks is the closed literal enums
+ // `DataEventType` / `BulkDataEventType` (`api/events.zod.ts`), which never
+ // referenced it. The three fields stay as plain `z.string()`; the enums are
+ // the only event-name contract. `WebSocketEventSchema.channel` stays a
+ // deliberate `z.string()` — the ruling adds no constraint there. No authored
+ // document is invalidated (the accept set at the three fields widens), so no
+ // tombstone and no D2 conversion — this table plus the D3 semantic entry
+ // `event-name-schema-retired` are the declaration (the #8715 route-3 shape).
+ 'shared/EventName',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. No schema in either repo ever composed `FieldNameSchema`,
+ // so the promised brand safety was unobtainable. The real field-name contract
+ // is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+ // `data/field.zod.ts` — untouched by this retirement. No tombstone and no D2
+ // conversion (no authored document ever embedded a branded value); this table
+ // plus the D3 semantic entry `branded-identifier-schemas-retired` are the
+ // declaration.
+ 'shared/FieldName',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. No schema in either repo ever composed `FlowNameSchema`,
+ // so the promised brand safety was unobtainable. The real flow-name contract
+ // is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+ // `automation/flow.zod.ts` — untouched by this retirement. No tombstone and
+ // no D2 conversion (no authored document ever embedded a branded value); this
+ // table plus the D3 semantic entry `branded-identifier-schemas-retired` are
+ // the declaration.
+ 'shared/FlowName',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. The brand promised compile-time safety no consumer could
+ // obtain: no schema in either repo ever composed `ObjectNameSchema`, so
+ // nothing produced or accepted a branded value. The real object-name contract
+ // is the inline `z.string().regex(/^[a-z_][a-z0-9_]*$/)` at
+ // `data/object.zod.ts` — untouched by this retirement (the ruling keeps the
+ // five surfaces' real validators as the contract of record). No authored
+ // document ever embedded a branded value, so no tombstone and no D2
+ // conversion — this table plus the D3 semantic entry
+ // `branded-identifier-schemas-retired` are the declaration (the #8715
+ // route-3 shape).
+ 'shared/ObjectName',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. No schema in either repo ever composed `RoleNameSchema`,
+ // so the promised brand safety was unobtainable. The real role/position-name
+ // contract is the bare `SnakeCaseIdentifierSchema` at
+ // `identity/position.zod.ts` — untouched by this retirement. No tombstone and
+ // no D2 conversion (no authored document ever embedded a branded value); this
+ // table plus the D3 semantic entry `branded-identifier-schemas-retired` are
+ // the declaration.
+ 'shared/RoleName',
+ // #13612 — ADR-0049 enforce-or-remove (maintainer ruling 2026-09-01, director
+ // batch C: retire; binding was weighed and not adopted). One of the six
+ // branded identifier schemas of `shared/branded-types.zod.ts`, removed whole
+ // with the module. No schema in either repo ever composed `ViewNameSchema`,
+ // so the promised brand safety was unobtainable. View names are validated
+ // where views are declared, not through a brand. No tombstone and no D2
+ // conversion (no authored document ever embedded a branded value); this table
+ // plus the D3 semantic entry `branded-identifier-schemas-retired` are the
+ // declaration.
+ 'shared/ViewName',
// #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/packages/spec/src/shared/branded-types.test.ts b/packages/spec/src/shared/branded-types.test.ts
deleted file mode 100644
index 1dd2d74440..0000000000
--- a/packages/spec/src/shared/branded-types.test.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import {
- ObjectNameSchema,
- FieldNameSchema,
- ViewNameSchema,
- AppNameSchema,
- FlowNameSchema,
- RoleNameSchema,
- type ObjectName,
- type FieldName,
- type ViewName,
- type AppName,
- type FlowName,
- type RoleName,
-} from './branded-types.zod';
-
-describe('ObjectNameSchema', () => {
- it('should accept valid snake_case object names', () => {
- const validNames = ['account', 'project_task', 'crm_lead', 'user_profile'];
- validNames.forEach((name) => {
- expect(() => ObjectNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should reject dots (not allowed in object names)', () => {
- expect(() => ObjectNameSchema.parse('user.created')).toThrow();
- });
-
- it('should reject uppercase', () => {
- expect(() => ObjectNameSchema.parse('Account')).toThrow();
- expect(() => ObjectNameSchema.parse('projectTask')).toThrow();
- });
-
- it('should reject too short names', () => {
- expect(() => ObjectNameSchema.parse('a')).toThrow();
- });
-
- it('should produce branded type at runtime', () => {
- const name: ObjectName = ObjectNameSchema.parse('my_object');
- expect(name).toBe('my_object');
- });
-});
-
-describe('FieldNameSchema', () => {
- it('should accept valid snake_case field names', () => {
- const validNames = ['first_name', 'created_at', 'total_amount', 'is_active'];
- validNames.forEach((name) => {
- expect(() => FieldNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should reject dots', () => {
- expect(() => FieldNameSchema.parse('user.name')).toThrow();
- });
-
- it('should reject camelCase', () => {
- expect(() => FieldNameSchema.parse('firstName')).toThrow();
- });
-
- it('should produce branded type at runtime', () => {
- const name: FieldName = FieldNameSchema.parse('task_name');
- expect(name).toBe('task_name');
- });
-});
-
-describe('ViewNameSchema', () => {
- it('should accept valid system identifiers', () => {
- const validNames = ['all_tasks', 'my_open_deals', 'contact.recent'];
- validNames.forEach((name) => {
- expect(() => ViewNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should allow dots (namespacing)', () => {
- const name: ViewName = ViewNameSchema.parse('contact.recent');
- expect(name).toBe('contact.recent');
- });
-
- it('should reject uppercase', () => {
- expect(() => ViewNameSchema.parse('AllTasks')).toThrow();
- });
-});
-
-describe('AppNameSchema', () => {
- it('should accept valid app names', () => {
- const validNames = ['crm', 'helpdesk', 'project_management'];
- validNames.forEach((name) => {
- expect(() => AppNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should produce branded type at runtime', () => {
- const name: AppName = AppNameSchema.parse('crm');
- expect(name).toBe('crm');
- });
-});
-
-describe('FlowNameSchema', () => {
- it('should accept valid flow names', () => {
- const validNames = ['approval_flow', 'onboarding_wizard', 'lead_qualification'];
- validNames.forEach((name) => {
- expect(() => FlowNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should produce branded type at runtime', () => {
- const name: FlowName = FlowNameSchema.parse('approval_flow');
- expect(name).toBe('approval_flow');
- });
-});
-
-describe('RoleNameSchema', () => {
- it('should accept valid role names', () => {
- const validNames = ['admin', 'sales_manager', 'read_only'];
- validNames.forEach((name) => {
- expect(() => RoleNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should produce branded type at runtime', () => {
- const name: RoleName = RoleNameSchema.parse('admin');
- expect(name).toBe('admin');
- });
-});
-
-describe('Branded type safety', () => {
- it('branded types parse the same underlying strings', () => {
- const objectName = ObjectNameSchema.parse('my_entity');
- const fieldName = FieldNameSchema.parse('my_entity');
- // At runtime both are 'my_entity' strings but TS types differ
- expect(objectName).toBe('my_entity');
- expect(fieldName).toBe('my_entity');
- });
-});
diff --git a/packages/spec/src/shared/branded-types.zod.ts b/packages/spec/src/shared/branded-types.zod.ts
deleted file mode 100644
index e649d44d60..0000000000
--- a/packages/spec/src/shared/branded-types.zod.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { z } from 'zod';
-import { SnakeCaseIdentifierSchema, SystemIdentifierSchema } from './identifiers.zod';
-
-/**
- * Branded Types for ObjectStack Identifiers
- *
- * Branded types provide compile-time safety by preventing accidental mixing
- * of different identifier kinds. For example, you cannot pass an ObjectName
- * where a FieldName is expected, even though both are strings at runtime.
- *
- * @example
- *
- * ```ts
- * import { ObjectNameSchema, FieldNameSchema } from '@objectstack/spec/shared';
- *
- * const objName = ObjectNameSchema.parse('project_task'); // ObjectName
- * const fieldName = FieldNameSchema.parse('task_name'); // FieldName
- *
- * // TypeScript will catch this at compile time:
- * // const fn: FieldName = objName; // Error!
- * ```
- */
-
-/**
- * ObjectName — Branded type for business object names.
- *
- * Must be snake_case (no dots). Used for table/collection names.
- *
- * @example 'project_task', 'crm_account', 'user_profile'
- */
-import { lazySchema } from './lazy-schema';
-export const ObjectNameSchema = lazySchema(() => SnakeCaseIdentifierSchema
- .brand<'ObjectName'>()
- .describe('Branded object name (snake_case, no dots)'));
-
-export type ObjectName = z.input;
-/** Post-parse shape of {@link ObjectName} — defaults applied, transforms run (ADR-0122). */
-export type ObjectNameParsed = z.infer;
-
-/**
- * FieldName — Branded type for field (column) names.
- *
- * Must be snake_case (no dots). Used for column/property names within objects.
- *
- * @example 'first_name', 'created_at', 'total_amount'
- */
-export const FieldNameSchema = lazySchema(() => SnakeCaseIdentifierSchema
- .brand<'FieldName'>()
- .describe('Branded field name (snake_case, no dots)'));
-
-export type FieldName = z.input;
-/** Post-parse shape of {@link FieldName} — defaults applied, transforms run (ADR-0122). */
-export type FieldNameParsed = z.infer;
-
-/**
- * ViewName — Branded type for view identifiers.
- *
- * Must be a valid system identifier (lowercase, may contain dots for namespacing).
- *
- * @example 'all_tasks', 'my_open_deals', 'contact.recent'
- */
-export const ViewNameSchema = lazySchema(() => SystemIdentifierSchema
- .brand<'ViewName'>()
- .describe('Branded view name (system identifier)'));
-
-export type ViewName = z.input;
-/** Post-parse shape of {@link ViewName} — defaults applied, transforms run (ADR-0122). */
-export type ViewNameParsed = z.infer;
-
-/**
- * AppName — Branded type for application identifiers.
- *
- * Must be a valid system identifier.
- *
- * @example 'crm', 'helpdesk', 'project_management'
- */
-export const AppNameSchema = lazySchema(() => SystemIdentifierSchema
- .brand<'AppName'>()
- .describe('Branded app name (system identifier)'));
-
-export type AppName = z.input;
-/** Post-parse shape of {@link AppName} — defaults applied, transforms run (ADR-0122). */
-export type AppNameParsed = z.infer;
-
-/**
- * FlowName — Branded type for flow identifiers.
- *
- * Must be a valid system identifier.
- *
- * @example 'approval_flow', 'onboarding_wizard', 'lead_qualification'
- */
-export const FlowNameSchema = lazySchema(() => SystemIdentifierSchema
- .brand<'FlowName'>()
- .describe('Branded flow name (system identifier)'));
-
-export type FlowName = z.input;
-/** Post-parse shape of {@link FlowName} — defaults applied, transforms run (ADR-0122). */
-export type FlowNameParsed = z.infer;
-
-/**
- * RoleName — Branded type for role identifiers.
- *
- * Must be a valid system identifier.
- *
- * @example 'admin', 'sales_manager', 'read_only'
- */
-export const RoleNameSchema = lazySchema(() => SystemIdentifierSchema
- .brand<'RoleName'>()
- .describe('Branded role name (system identifier)'));
-
-export type RoleName = z.input;
-/** Post-parse shape of {@link RoleName} — defaults applied, transforms run (ADR-0122). */
-export type RoleNameParsed = z.infer;
diff --git a/packages/spec/src/shared/identifiers.test.ts b/packages/spec/src/shared/identifiers.test.ts
index 14b0a84418..a31528948d 100644
--- a/packages/spec/src/shared/identifiers.test.ts
+++ b/packages/spec/src/shared/identifiers.test.ts
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
import {
SystemIdentifierSchema,
SnakeCaseIdentifierSchema,
- EventNameSchema,
} from './identifiers.zod';
describe('SystemIdentifierSchema', () => {
@@ -184,60 +183,3 @@ describe('SnakeCaseIdentifierSchema', () => {
});
});
-describe('EventNameSchema', () => {
- describe('valid event names', () => {
- it('should accept lowercase with dots', () => {
- const validEventNames = [
- 'user.created',
- 'order.paid',
- 'user.login_success',
- 'alarm.high_cpu',
- 'order.created',
- 'user.login',
- ];
-
- validEventNames.forEach((name) => {
- expect(() => EventNameSchema.parse(name)).not.toThrow();
- });
- });
-
- it('should accept lowercase with underscores', () => {
- const validEventNames = [
- 'user_created',
- 'order_paid',
- 'login_success',
- ];
-
- validEventNames.forEach((name) => {
- expect(() => EventNameSchema.parse(name)).not.toThrow();
- });
- });
- });
-
- describe('invalid event names', () => {
- it('should reject uppercase', () => {
- const invalidEventNames = [
- 'User.Created',
- 'Order.Paid',
- 'UserCreated',
- 'OrderPaid',
- ];
-
- invalidEventNames.forEach((name) => {
- expect(() => EventNameSchema.parse(name)).toThrow();
- });
- });
-
- it('should reject camelCase', () => {
- const invalidEventNames = [
- 'userCreated',
- 'orderPaid',
- 'loginSuccess',
- ];
-
- invalidEventNames.forEach((name) => {
- expect(() => EventNameSchema.parse(name)).toThrow();
- });
- });
- });
-});
diff --git a/packages/spec/src/shared/identifiers.zod.ts b/packages/spec/src/shared/identifiers.zod.ts
index badf06416f..08409d7087 100644
--- a/packages/spec/src/shared/identifiers.zod.ts
+++ b/packages/spec/src/shared/identifiers.zod.ts
@@ -171,38 +171,13 @@ export const MetadataItemNameSchema = lazySchema(() => z
})
.describe('Metadata item name (lowercase snake_case segments, optionally dot-qualified)'));
-/**
- * Event Name Identifier
- *
- * Specialized identifier for event names that encourages dot notation.
- * Used in event-driven systems, message queues, and webhooks.
- *
- * Pattern: `namespace.action` or `entity.event_type`
- *
- * @example Valid
- * - 'user.created'
- * - 'order.paid'
- * - 'user.login_success'
- * - 'alarm.high_cpu'
- *
- * @example Invalid
- * - 'UserCreated' (camelCase)
- * - 'user_created' (should use dots for namespacing)
- *
- * No `.max()` is declared, deliberately — identifier length ceilings are
- * storage-owned; see the length-ceiling note on
- * {@link SystemIdentifierSchema} and issue #12144. (No bounded storage column
- * among #12144's measured set stores an event name, so no ceiling has been
- * measured for this schema at all.)
- */
-export const EventNameSchema = lazySchema(() => z
- .string()
- .min(3, { message: 'Event name must be at least 3 characters' })
- .regex(/^[a-z][a-z0-9_.]*$/, {
- message:
- 'Event name must be lowercase with dots for namespacing (e.g., "user.created", "order.paid")',
- })
- .describe('Event name (lowercase with dot notation for namespacing)'));
+// [#13613] `EventNameSchema` (and its `EventName` type) was retired under
+// ADR-0049 enforce-or-remove. It presented itself as the platform's
+// event-name grammar while nothing that runs consumed its three binding
+// schemas; the vocabulary the platform actually checks is the closed literal
+// enums `DataEventType` / `BulkDataEventType` (`api/events.zod.ts`) — the
+// event surface is platform-defined, not author-extensible, so a grammar
+// layer for a hypothetical extension surface misleads in both directions.
/**
* Type Exports
@@ -210,4 +185,3 @@ export const EventNameSchema = lazySchema(() => z
export type SystemIdentifier = z.input;
export type SnakeCaseIdentifier = z.input;
export type MetadataItemName = z.input;
-export type EventName = z.input;
diff --git a/packages/spec/src/shared/index.ts b/packages/spec/src/shared/index.ts
index f400f4b514..68dfd7517a 100644
--- a/packages/spec/src/shared/index.ts
+++ b/packages/spec/src/shared/index.ts
@@ -10,7 +10,11 @@ export * from './mapping.zod';
export * from './http.zod';
export * from './enums.zod';
export * from './metadata-types.zod';
-export * from './branded-types.zod';
+// [#13612] `branded-types.zod` (the six branded identifier schemas) was retired
+// under ADR-0049 enforce-or-remove — no schema ever composed a brand, so the
+// promised compile-time safety was unobtainable. The real identifier contracts
+// live at the surfaces themselves: inline regexes on object/field/flow names,
+// bare `SnakeCaseIdentifierSchema` on app and position names.
export * from './suggestions.zod';
export * from './error-map.zod';
export * from './external-errors';
diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts
index dac7ae4a14..0fc9ac3f8e 100644
--- a/packages/spec/src/type-alias-convention.pin.test.ts
+++ b/packages/spec/src/type-alias-convention.pin.test.ts
@@ -269,7 +269,7 @@ import type * as M170 from './ui/component.zod.js';
import type * as M183 from './api/sortability.zod.js';
// ---------------------------------------------------------------------------
-// 836 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared.
+// 835 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared.
//
// That number is machine-checked, not hand-kept. The runtime companion at the
// bottom of this file recomputes the pin count from the source and asserts that
@@ -1038,7 +1038,6 @@ export type Iso496 = Assert, z.infe
// shared/identifiers.zod.ts
export type Iso497 = Assert, z.infer< typeof M113.SystemIdentifierSchema > >>;
export type Iso498 = Assert, z.infer< typeof M113.SnakeCaseIdentifierSchema > >>;
-export type Iso499 = Assert, z.infer< typeof M113.EventNameSchema > >>;
export type Iso862 = Assert, z.infer< typeof M113.MetadataItemNameSchema > >>;
// shared/mapping.zod.ts
@@ -1682,7 +1681,7 @@ describe('ADR-0122 type-alias convention', () => {
// this title and the section header above the pin list — are now asserted
// against the recomputed count below, so neither can go stale without a red
// test naming it.
- it('still declares all 836 isomorphic pins', () => {
+ it('still declares all 835 isomorphic pins', () => {
// The truth of each pin is proved by tsc, not here — an `Assert>`
// that stops holding is a compile error with the alias named. What tsc
// cannot notice is a pin that was DELETED: removing the assertion removes
@@ -2050,9 +2049,17 @@ describe('ADR-0122 type-alias convention', () => {
// than an `XParsed` — the exact reasoning of its #8211 sibling `Iso838`
// one entry up. Its id is `Iso865`, the next free one — ids are claims
// about pins, not positions.
+ //
+ // 836 -> 835 is #13613's ADR-0049 retirement of `EventNameSchema`
+ // (shared/identifiers.zod.ts): its pin `Iso499` left with the schema —
+ // the alias no longer exists, so there is nothing to be isomorphic. The
+ // three schemas that bound it keep their fields as plain `z.string()`;
+ // the platform-checked event vocabulary is the closed `DataEventType` /
+ // `BulkDataEventType` enums. -1 removed; the Iso number stays vacant
+ // (ids are claims about pins, not positions).
const self = readFileSync(fileURLToPath(import.meta.url), 'utf8');
const pins = self.match(/^export type Iso\d+ = Assert