From 685d54bd7115c904821ede1837c9fe17d98344ef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 10:16:58 +0000 Subject: [PATCH 1/2] docs(spec): make the SystemIdentifierSchema docblock name its real bound surfaces The docblock claimed eleven consuming surfaces. The per-surface census (its os-dev-report comment, measured on origin/main @ e2debee6) measured exactly ONE of the eleven as validated by this schema: select option values (SelectOptionSchema.value). Eight go to SnakeCaseIdentifierSchema or an inline flat regex that forbids dots outright; event keys went to the sibling EventNameSchema, since retired unbound. Rewrite the prose to name the whole bound list as it stands today (SelectOptionSchema.value plus the three object-storage keys the old prose never claimed, recorded as bound-but-unauthored), to point each unbound surface at the schema that really validates it, and to state that these are different accept sets rather than looser spellings of one grammar. Drop the Event keys row of the naming table and the 'for events' example, both of which asserted a binding that no longer exists. Comment lines only: no regex change, no accept-set change, no .describe() change. The storage-owned length-ceiling note is carried through unchanged. --- .../system-identifier-docblock-truth.md | 46 ++++++++++ packages/spec/src/shared/identifiers.zod.ts | 85 ++++++++++++++----- 2 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 .changeset/system-identifier-docblock-truth.md diff --git a/.changeset/system-identifier-docblock-truth.md b/.changeset/system-identifier-docblock-truth.md new file mode 100644 index 0000000000..7184605b6f --- /dev/null +++ b/.changeset/system-identifier-docblock-truth.md @@ -0,0 +1,46 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): make the `SystemIdentifierSchema` docblock name the surfaces it actually validates (#13621) + +Prose truth restoration. No regex change, no accept-set change, no `.describe()` +change — the diff is comment lines only. + +The docblock claimed eleven consuming surfaces ("Applies to all metadata that +acts as a machine identifier": object names, field names, role names, permission +set names, action/trigger names, event keys, app IDs, menu/page IDs, select +option values, workflow names, webhook names). The per-surface census on #12245 — +its `os-dev-report` comment is the measurement of record, taken on `origin/main` +@ `e2debee6` — measured **exactly one** of those eleven as validated by this +schema: select option values (`SelectOptionSchema.value`). Eight are validated by +`SnakeCaseIdentifierSchema` or by an inline flat regex that forbids dots outright, +and event keys went to the sibling `EventNameSchema`, since retired unbound +(#13613). The docblock ships into `@objectstack/spec`'s `.d.ts`, so the false map +was reaching consumers' editor tooltips and — the reason the card was filed — +every AI generator that reads it as authority on where the grammar applies. + +The rewritten docblock states, for today's tree: + +- The **whole** bound list, as a table: `SelectOptionSchema.value` + (`data/field.zod.ts`, the one surface with a real authored population, reused by + the form-view option list via `SelectOptionSchema.shape`), plus three + object-storage keys the old prose never claimed — `LifecyclePolicyRuleSchema.id`, + `BucketConfigSchema.name`, `ObjectStorageConfigSchema.name` — recorded as bound + in declaration with nothing authoring them ("nothing to census", not "censused + clean"). +- Where the ten unbound surfaces are **actually** validated, so a reader who came + here for the object-name rule leaves with the right file: the inline + `/^[a-z_][a-z0-9_]*$/` sites, `SnakeCaseIdentifierSchema`, + `MetadataItemNameSchema`, and — for event keys — the closed `DataEventType` / + `BulkDataEventType` enums, which are not a grammar at all. +- That these are **different accept sets**, not looser spellings of one another, + with the measured `SystemIdentifierSchema` vs `MetadataItemNameSchema` delta + named (`a.`, `a..b`, `a.1b`, `a._b`). +- That the dot this grammar accepts is unexercised on its one live surface: 0 of + 1218 authored select option values contain one. + +The `Event keys | dot.notation` row of the naming-convention table and the +`'order.created' (for events)` example both asserted a binding that no longer +exists; both now state what is true. The storage-owned length-ceiling note +(#12144) is carried through unchanged. diff --git a/packages/spec/src/shared/identifiers.zod.ts b/packages/spec/src/shared/identifiers.zod.ts index 08409d7087..707a054cbe 100644 --- a/packages/spec/src/shared/identifiers.zod.ts +++ b/packages/spec/src/shared/identifiers.zod.ts @@ -5,33 +5,70 @@ import { z } from 'zod'; /** * System Identifier Schema * - * Universal naming convention for all machine identifiers (API Names) in ObjectStack. - * Enforces lowercase with underscores or dots to ensure: - * - Cross-platform compatibility (case-insensitive filesystems) - * - URL-friendliness (no encoding needed) - * - Database consistency (no collation issues) - * - Security (no case-sensitivity bugs in permission checks) - * - * **Applies to all metadata that acts as a machine identifier:** - * - Object names (tables/collections) - * - Field names - * - Role names - * - Permission set names - * - Action/trigger names - * - Event keys - * - App IDs - * - Menu/page IDs - * - Select option values - * - Workflow names - * - Webhook names - * + * A lowercase machine-identifier grammar: starts with a letter, then letters, + * digits, underscores or dots. It is one of several identifier grammars in + * this repo, not the universal one — see the binding list below before + * reaching for it. Where it *is* bound, the shape buys the usual four + * properties: cross-platform safety (case-insensitive filesystems), + * URL-friendliness (no encoding needed), database consistency (no collation + * issues), and no case-sensitivity bugs in permission checks. + * + * **Bound surfaces — this is the whole list.** + * An earlier revision of this docblock claimed eleven consuming surfaces. The + * per-surface census on #12245 — its `os-dev-report` comment is the + * measurement of record, taken on `origin/main` @ `e2debee6` — measured + * **exactly one** of those eleven as validated by this schema; the other ten + * are validated by something else (next block). What composes this schema is: + * + * | Bound key | Declared at | Authored population | + * |------|---------|---------| + * | Select option `value` | `SelectOptionSchema.value` (`data/field.zod.ts`) | 1218 authored values censused | + * | Lifecycle rule `id` | `LifecyclePolicyRuleSchema.id` (`system/object-storage.zod.ts`) | none — nothing authors it | + * | Bucket `name` | `BucketConfigSchema.name` (`system/object-storage.zod.ts`) | none | + * | Storage config `name` | `ObjectStorageConfigSchema.name` (`system/object-storage.zod.ts`) | none | + * + * Select option values are the only surface with a real authored population. + * The option shape is reused by the form-view option list + * (`FormSelectOptionSchema`, `ui/view.zod.ts`, derived from + * `SelectOptionSchema.shape`), so both option lists carry this grammar. The + * three object-storage keys are bound in declaration only: the census found no + * corpus to measure for them and reports them as "nothing to census", never as + * measured clean. + * + * **NOT bound here — where those names are actually validated.** A generator + * that consults this docblock to learn what validates a name needs the real + * answer. None of these is a looser or stricter spelling of this grammar; + * they are different accept sets, so substituting one for another changes + * what is refused: + * + * - Object names, field names, workflow names — inline + * `/^[a-z_][a-z0-9_]*$/` at `data/object.zod.ts`, `data/field.zod.ts`, + * `automation/flow.zod.ts`. Dots forbidden; a leading `_` allowed. + * - Role (position) names, permission set names, action/trigger names, app + * IDs, menu/page IDs, webhook names — {@link SnakeCaseIdentifierSchema}. + * Dots forbidden. + * - Metadata item names (the `sys_metadata` / `/api/v1/meta` addressing + * identity) — {@link MetadataItemNameSchema}. Dots *allowed*, but as + * qualifiers between anchored segments, so it refuses the empty-, + * digit-initial and underscore-initial segments this schema accepts + * (`a.`, `a..b`, `a.1b`, `a._b`). Enforced at the metadata publish door. + * - Event keys — no author-facing grammar at all. The event vocabulary is the + * closed literal enums `DataEventType` / `BulkDataEventType` + * (`api/events.zod.ts`); the sibling `EventNameSchema` that the census found + * holding this claim was retired unbound under ADR-0049 (#13613 — tombstone + * at the foot of this file). The four branded aliases that wrapped *this* + * schema went the same way, also unbound (#13612). + * * **Naming Convention Summary:** * | Type | Pattern | Example | * |------|---------|---------| * | Machine ID | snake_case | `crm_account`, `btn_submit`, `role_admin` | - * | Event keys | dot.notation | `user.login`, `order.created` | * | Labels | Any case | `Client Account`, `Submit Form` | * + * The dot this grammar accepts is unexercised on the one live surface: 0 of + * the 1218 authored select option values contain one (#12245). Recorded as the + * measurement it is — dots are accepted, not a convention to write in. + * * **Length ceiling — storage-owned, deliberately not declared here (#12144).** * The identifier schemas in this file declare a floor and a grammar but no * `.max()`: the enforced ceiling on an identifier is the `maxLength` of the @@ -50,9 +87,11 @@ import { z } from 'zod'; * - 'account' * - 'crm_account' * - 'user_profile' - * - 'order.created' (for events) * - 'api_v2_endpoint' - * + * - 'order.created' (the grammar accepts a dot; no bound surface authors one + * today — see the note above, and never read this row as the event-name + * contract, which is a closed enum elsewhere) + * * @example Invalid identifiers (will be rejected) * - 'Account' (uppercase) * - 'CrmAccount' (camelCase) From e5222b5d5f813766c0bc687cd645673eff6c3169 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 11:03:51 +0000 Subject: [PATCH 2/2] docs(changeset): correct the reach claim to what was measured The first draft of this changeset said the docblock ships into @objectstack/spec's .d.ts and so reached consumers' editor tooltips. Measured after the build: it does not -- no emitted .d.ts or .d.cts carries any docblock from identifiers.zod.ts. The corrected note states the real audience (readers of this source, human and agent), which is the audience the card was filed for. --- .changeset/system-identifier-docblock-truth.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.changeset/system-identifier-docblock-truth.md b/.changeset/system-identifier-docblock-truth.md index 7184605b6f..cff9ce6fb7 100644 --- a/.changeset/system-identifier-docblock-truth.md +++ b/.changeset/system-identifier-docblock-truth.md @@ -16,9 +16,12 @@ its `os-dev-report` comment is the measurement of record, taken on `origin/main` schema: select option values (`SelectOptionSchema.value`). Eight are validated by `SnakeCaseIdentifierSchema` or by an inline flat regex that forbids dots outright, and event keys went to the sibling `EventNameSchema`, since retired unbound -(#13613). The docblock ships into `@objectstack/spec`'s `.d.ts`, so the false map -was reaching consumers' editor tooltips and — the reason the card was filed — -every AI generator that reads it as authority on where the grammar applies. +(#13613). The reach is in-repo, measured: tsup's declaration emit does not carry +this file's docblocks into the published `.d.ts`, so no consumer tooltip changes +— the readers being corrected are the ones working in this source, which is the +reason the card was filed: an AI generator reads this docblock as authority on +where the grammar applies, and a docblock governing one surface while claiming +eleven is a false map of the contract. The rewritten docblock states, for today's tree: