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