Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/system-identifier-docblock-truth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
"@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 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:

- 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.
85 changes: 62 additions & 23 deletions packages/spec/src/shared/identifiers.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Loading