From a4064439acc3e210c3cd4c74790824d5fec8c468 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 08:06:33 +0000 Subject: [PATCH 1/5] feat(spec): retire the six branded identifier schemas of shared/branded-types.zod.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0049 enforce-or-remove, maintainer ruling 2026-09-01 (director batch C, verbatim approval: retire; binding weighed and not adopted). No schema in either repo ever composed a brand, so the promised compile-time safety was unobtainable, while the surfaces the brands were named for are validated by inline regexes or bare SnakeCaseIdentifierSchema — those five real validators are the contract of record and are untouched here. Removes the module whole (6 schemas + 12 type exports), registers the six shared/* defs under RETIRED_DEFS_BY_MAJOR 18 plus the D3 semantic entry branded-identifier-schemas-retired, deletes the six manifest keys behind the #4725 gate, and regenerates api-surface / declaration-map / export-origins / reference docs. SnakeCaseIdentifierSchema and SystemIdentifierSchema stay published and unchanged. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 --- content/docs/references/index.mdx | 9 +- .../docs/references/shared/branded-types.mdx | 94 ------------ content/docs/references/shared/index.mdx | 1 - content/docs/references/shared/meta.json | 1 - packages/spec/api-surface/shared.json | 18 --- packages/spec/declaration-map/shared.json | 14 +- packages/spec/export-origins/shared.json | 18 --- .../spec/json-schema.manifest/shared.json | 8 +- packages/spec/scripts/lib/schema-section.ts | 2 +- .../retired-defs/18.shared__AppName.ts | 12 ++ .../retired-defs/18.shared__FieldName.ts | 13 ++ .../retired-defs/18.shared__FlowName.ts | 13 ++ .../retired-defs/18.shared__ObjectName.ts | 16 +++ .../retired-defs/18.shared__RoleName.ts | 13 ++ .../retired-defs/18.shared__ViewName.ts | 12 ++ .../18.branded-identifier-schemas-retired.ts | 50 +++++++ packages/spec/src/migrations/registry.ts | 113 +++++++++++++++ .../spec/src/shared/branded-types.test.ts | 134 ------------------ packages/spec/src/shared/branded-types.zod.ts | 115 --------------- packages/spec/src/shared/index.ts | 6 +- 20 files changed, 254 insertions(+), 408 deletions(-) delete mode 100644 content/docs/references/shared/branded-types.mdx create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__AppName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__FieldName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__FlowName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__ObjectName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__RoleName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__ViewName.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.branded-identifier-schemas-retired.ts delete mode 100644 packages/spec/src/shared/branded-types.test.ts delete mode 100644 packages/spec/src/shared/branded-types.zod.ts diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 713b5ab4c6..8e9a843b7e 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 — 1592 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 | 26 | 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** | **1592** | 14 protocol modules | --- @@ -286,13 +286,12 @@ 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, 26 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` | 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/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/api-surface/shared.json b/packages/spec/api-surface/shared.json index 2767daa95e..9182275560 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)", @@ -31,12 +28,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 +50,6 @@ "MutationEvent (type)", "MutationEventEnum (const)", "NormalizeStackInputOptions (interface)", - "ObjectName (type)", - "ObjectNameParsed (type)", - "ObjectNameSchema (const)", "ObjectStackRawIssue (type)", "P (const)", "PLURAL_TO_SINGULAR (const)", @@ -76,9 +64,6 @@ "RateLimitConfigParsed (type)", "RateLimitConfigSchema (const)", "ResilientFetchOptions (interface)", - "RoleName (type)", - "RoleNameParsed (type)", - "RoleNameSchema (const)", "SINGULAR_TO_PLURAL (const)", "SchemaDiffEntry (interface)", "SchemaDiffEntryKind (type)", @@ -97,9 +82,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..3e20e9a79c 100644 --- a/packages/spec/declaration-map/shared.json +++ b/packages/spec/declaration-map/shared.json @@ -2,8 +2,6 @@ "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", @@ -21,10 +19,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 +30,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 +38,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 +48,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..91b51e8761 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)", @@ -31,12 +28,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 +50,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 +64,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 +82,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..865c2fc656 100644 --- a/packages/spec/json-schema.manifest/shared.json +++ b/packages/spec/json-schema.manifest/shared.json @@ -2,7 +2,6 @@ "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", @@ -12,8 +11,6 @@ "shared/ExpressionInput", "shared/ExpressionMeta", "shared/FieldMapping", - "shared/FieldName", - "shared/FlowName", "shared/HttpMethod", "shared/HttpMethodSubset", "shared/HttpRequest", @@ -21,18 +18,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/scripts/lib/schema-section.ts b/packages/spec/scripts/lib/schema-section.ts index 4117ff4421..fe95bce777 100644 --- a/packages/spec/scripts/lib/schema-section.ts +++ b/packages/spec/scripts/lib/schema-section.ts @@ -562,7 +562,7 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio // // ONE LINE, and it is `formatType`'s rendering, not a second one. That is // what every property row on the page already prints for the same node, so - // `ObjectName`'s own section and a property typed `ObjectName` cannot + // `SystemIdentifier`'s own section and a property typed `SystemIdentifier` cannot // disagree about what it is. It is also why the line does NOT spell // `pattern` / `minLength` / `minimum`: this renderer has never printed a // constraint in any position, and inventing constraint prose here alone 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__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/registry.ts b/packages/spec/src/migrations/registry.ts index d755c8224f..3b452c2edc 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 ' @@ -9843,6 +9889,73 @@ 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', + // #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/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'; From 928a373ad479d6621b8abe782b5254aae80514ad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 08:24:37 +0000 Subject: [PATCH 2/5] =?UTF-8?q?feat(spec):=20retire=20EventNameSchema=20?= =?UTF-8?q?=E2=80=94=20the=20closed=20event=20enums=20are=20the=20only=20e?= =?UTF-8?q?vent-name=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0049 enforce-or-remove, maintainer ruling 2026-09-01 (director batch C, verbatim approval: retire). EventNameSchema presented itself as the platform's event-name grammar while nothing that runs consumed its three binding schemas (EventTypeDefinitionSchema.name, EventSchema.name, EventMessageSchema.eventName); the vocabulary the platform actually checks is the closed literal enums DataEventType / BulkDataEventType, which never referenced it. Removes the schema and its EventName type, widens the three binding fields to plain z.string() with describes that point at the enums, registers shared/EventName under RETIRED_DEFS_BY_MAJOR 18 plus the D3 semantic entry event-name-schema-retired, deletes the manifest key behind the #4725 gate, drops the Iso499 isomorphism pin with the schema (836 -> 835, ledgered in the pin file), and regenerates baselines, reference docs and skill refs. The kernel events fixture that pinned the retired grammar's reject branch is repinned to the widening (the refused spellings now parse). WebSocketEventSchema.channel stays a deliberate z.string(); the enums are byte-for-byte untouched. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 --- content/docs/references/api/websocket.mdx | 4 +- content/docs/references/index.mdx | 10 ++-- content/docs/references/kernel/events-bus.mdx | 2 +- .../docs/references/kernel/events-core.mdx | 4 +- content/docs/references/kernel/events-dlq.mdx | 4 +- .../docs/references/shared/identifiers.mdx | 15 +---- packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md | 2 +- packages/spec/api-surface/shared.json | 2 - packages/spec/declaration-map/shared.json | 2 - packages/spec/export-origins/shared.json | 2 - .../spec/json-schema.manifest/shared.json | 1 - packages/spec/src/api/websocket.zod.ts | 7 ++- packages/spec/src/kernel/events.test.ts | 38 ++++++------ packages/spec/src/kernel/events/core.zod.ts | 11 +++- .../retired-defs/18.shared__EventName.ts | 16 +++++ .../semantic/18.event-name-schema-retired.ts | 47 +++++++++++++++ packages/spec/src/migrations/registry.ts | 57 ++++++++++++++++++ packages/spec/src/shared/identifiers.test.ts | 58 ------------------- packages/spec/src/shared/identifiers.zod.ts | 40 +++---------- .../src/type-alias-convention.pin.test.ts | 15 +++-- skills/objectstack-ai/references/_index.md | 2 +- skills/objectstack-api/references/_index.md | 2 +- .../references/_index.md | 2 +- skills/objectstack-data/references/_index.md | 2 +- skills/objectstack-i18n/references/_index.md | 2 +- .../objectstack-platform/references/_index.md | 2 +- skills/objectstack-query/references/_index.md | 2 +- skills/objectstack-ui/references/_index.md | 2 +- 28 files changed, 192 insertions(+), 161 deletions(-) create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.shared__EventName.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.event-name-schema-retired.ts 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 8e9a843b7e..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 — 1592 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) | 7 | 26 | 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** | **199** | **1592** | 14 protocol modules | +| **Total** | **199** | **1591** | 14 protocol modules | --- @@ -286,7 +286,7 @@ Permission sets, row-level security, sharing rules, tenancy posture. ## Shared Protocol -**Source:** `packages/spec/src/shared/` · **Import:** `@objectstack/spec/shared` · **7 pages, 26 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. @@ -295,7 +295,7 @@ Primitives used across every protocol — identifiers, HTTP, expressions, error | [`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/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/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 9182275560..b1272f7333 100644 --- a/packages/spec/api-surface/shared.json +++ b/packages/spec/api-surface/shared.json @@ -12,8 +12,6 @@ "CronExpressionInputSchema (const)", "EXTERNAL_ERROR_CODES (const)", "EXTERNAL_ERROR_HTTP_STATUS (const)", - "EventName (type)", - "EventNameSchema (const)", "Expression (type)", "ExpressionDialect (type)", "ExpressionInput (type)", diff --git a/packages/spec/declaration-map/shared.json b/packages/spec/declaration-map/shared.json index 3e20e9a79c..1f58d35aa8 100644 --- a/packages/spec/declaration-map/shared.json +++ b/packages/spec/declaration-map/shared.json @@ -8,8 +8,6 @@ "CorsConfigSchema": "shared/CorsConfig", "CronExpressionInput": "shared/CronExpressionInput", "CronExpressionInputSchema": "shared/CronExpressionInput", - "EventName": "shared/EventName", - "EventNameSchema": "shared/EventName", "Expression": "shared/Expression", "ExpressionDialect": "shared/ExpressionDialect", "ExpressionInput": "shared/ExpressionInput", diff --git a/packages/spec/export-origins/shared.json b/packages/spec/export-origins/shared.json index 91b51e8761..322504cfb7 100644 --- a/packages/spec/export-origins/shared.json +++ b/packages/spec/export-origins/shared.json @@ -12,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)", diff --git a/packages/spec/json-schema.manifest/shared.json b/packages/spec/json-schema.manifest/shared.json index 865c2fc656..15eb80fe54 100644 --- a/packages/spec/json-schema.manifest/shared.json +++ b/packages/spec/json-schema.manifest/shared.json @@ -5,7 +5,6 @@ "shared/BaseMetadataRecord", "shared/CorsConfig", "shared/CronExpressionInput", - "shared/EventName", "shared/Expression", "shared/ExpressionDialect", "shared/ExpressionInput", 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__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/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 3b452c2edc..9682d3cfed 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6233,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", @@ -9899,6 +9942,20 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // 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 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/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 Date: Tue, 1 Sep 2026 08:24:37 +0000 Subject: [PATCH 3/5] chore: changeset for the #13612/#13613 identifier-schema retirements Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 --- ...ier-schemas-declared-unenforced-retired.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .changeset/identifier-schemas-declared-unenforced-retired.md 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. From a54beaa7a0941aa253db2c4910c934470c729d04 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 08:57:24 +0000 Subject: [PATCH 4/5] docs(spec): correct the two hand-kept counts the branded-types removal moves llms.txt counts *.zod.ts modules per domain (shared 13 -> 12, total 207 -> 206) and quick-reference.mdx declares how many reference pages content/docs/references/shared/ publishes (8 -> 7); both moved when shared/branded-types.zod.ts left with its six schemas. Caught by check:llms-txt and check:quick-reference-counts. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 --- content/docs/getting-started/quick-reference.mdx | 2 +- packages/spec/llms.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 | From 881b685f5c753767701032bcc12a488b575b9c5e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 09:20:30 +0000 Subject: [PATCH 5/5] revert(spec): restore schema-section.ts's historical comment example The ObjectName-to-SystemIdentifier rename in the :565 comment was decorative (zero behavior, forced by no gate) and its only measured effect was to co-edit the generator that the #11705 pure-regeneration exception trusts, which kept the eight regenerated skills/*/references/_index.md files GOVERNED. The retired name stays in the comment as a tolerable historical example; check:skill-refs, check:docs and check:generated pass unchanged against the unmodified generator. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 --- packages/spec/scripts/lib/schema-section.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/scripts/lib/schema-section.ts b/packages/spec/scripts/lib/schema-section.ts index fe95bce777..4117ff4421 100644 --- a/packages/spec/scripts/lib/schema-section.ts +++ b/packages/spec/scripts/lib/schema-section.ts @@ -562,7 +562,7 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio // // ONE LINE, and it is `formatType`'s rendering, not a second one. That is // what every property row on the page already prints for the same node, so - // `SystemIdentifier`'s own section and a property typed `SystemIdentifier` cannot + // `ObjectName`'s own section and a property typed `ObjectName` cannot // disagree about what it is. It is also why the line does NOT spell // `pattern` / `minLength` / `minimum`: this renderer has never printed a // constraint in any position, and inventing constraint prose here alone