diff --git a/.changeset/view-name-grammar-per-body-spelling.md b/.changeset/view-name-grammar-per-body-spelling.md new file mode 100644 index 0000000000..d55db24462 --- /dev/null +++ b/.changeset/view-name-grammar-per-body-spelling.md @@ -0,0 +1,37 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): state the `view` name grammar per body spelling in the view module header (#13134) + +`ViewMetadataSchema` is a union over three persisted `view` body spellings, and +they do not share one `name` grammar. Nothing said so — the rule was +reconstructible only by reading three schema factories: + +| body spelling | `name` is declared as | flat (undotted) name | +|:---|:---|:---| +| standalone ViewItem record | `ViewItemNameSchema` — `QUALIFIED_ITEM_NAME_PATTERN`, dot REQUIRED | rejected, located at `["name"]` | +| flattened runtime overlay | `z.string().optional()` — no grammar | accepted | +| `defineView` container | `z.string().optional()` — no grammar | accepted, and normally IS flat | + +Which grammar applies is decided by the body's shape, which an author never +names explicitly, so neither failure direction is discoverable from the key +being written: reading `ViewItemNameSchema` alone suggests the dot is mandatory +everywhere (it is not — a container's own name is the bare object key under +ADR-0017 §3.2's dual-read, and an overlay's name is stamped by the write path), +while reading a flat-named overlay or container row suggests flat is fine +generally (it is not — the same name on a standalone ViewItem record is +refused). + +**No schema change.** This is documentation: a family-level JSDoc block on +`ui/view.zod.ts`, three pointer comments at the declaration sites, and the +regenerated reference page. Every accept/reject decision is byte-for-byte what +it was — all three spellings were already internally consistent and the +flat-named rows already parse. + +Side effect worth naming for readers of the generated reference: `ui/view.zod.ts` +had no module description, so `content/docs/references/ui/view.mdx` and the two +skill reference indexes opened with the doc comment attached to an unrelated +`HttpRequest` re-export. They now open with the module's own description. The +`HttpRequest` note is unchanged in the source, where it documents that +re-export. diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 74d3e853dc..83a4576a68 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -5,8 +5,51 @@ description: View protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -HTTP Method Enum & HTTP Request Schema -Migrated to [shared/http.zod.ts](/docs/references/shared/http). Re-exported here for backward compatibility. +View protocol schemas — the `view` metadata type and its three persisted body spellings. + +Covers the authoring surfaces (`defineView`, `defineViewItem`), the wire +doors Studio and the REST layer write through, and +`ViewMetadataSchema` — the union every persisted `view` body is judged +by. + +## Name grammar depends on the body spelling + +`ViewMetadataSchema` is a union over three persisted body shapes, and +they do not share one `name` grammar. Which grammar applies is decided by the +shape of the body — something an author never names explicitly — so neither +failure direction below is discoverable from the key being written: + +| body spelling | recognised by | `name` is declared as | flat (undotted) name | +|:---|:---|:---|:---| +| standalone **ViewItem record** | a nested `config` | `ViewItemNameSchema` — `QUALIFIED_ITEM_NAME_PATTERN`, dot REQUIRED | **rejected**, located at `["name"]` | +| flattened runtime **overlay** | an inline view config; no `config`, no container slot | `z.string().optional()` — no grammar at all | accepted | +| `defineView` **container** | a container slot (`list` / `form` / `listViews` / `formViews`) | `z.string().optional()` on `ViewSchema` — no grammar at all | accepted, and normally IS flat | + +The two permissive rows are deliberate, not gaps left to tighten later: + +- A container's own name is the **bare object key**. ADR-0017 §3.2's + dual-read loader registers the aggregated container under `` and + each expanded item under `.`, so an object-scoped + container is named `crm_lead` — a name with no dot to carry. +- An overlay's name is **stamped by the write path**, not authored: + `normalizeViewMetadata` puts it on every view body at the single write + chokepoint, and a personalization PUT inherits the identity of the entry it + shadows. + +So both of the readings an author naturally forms are wrong: + +- *"the dot is mandatory on every view row"* — read off + `ViewItemNameSchema` alone. It is not: overlay and container rows + accept flat names, and rows in this repo legitimately use them + (`case_grid`, `cases`, the container row `crm_lead`). Those rows are + correct as written, not defects awaiting a dotted rewrite. +- *"flat names are fine generally"* — read off one of those flat-named rows. + It is not: put the same name on a standalone ViewItem record and it is + refused, on the one field the flat-named row told you to fill. + +This describes what the three shapes already do; it widens and narrows +nothing. The one item-name grammar itself lives in +`shared/identifiers.zod.ts` — grammar changes belong there, not here. **Source:** `packages/spec/src/ui/view.zod.ts` diff --git a/packages/spec/src/conversions/view-spelling-walk.test.ts b/packages/spec/src/conversions/view-spelling-walk.test.ts index 9e694656ca..54f625f2b4 100644 --- a/packages/spec/src/conversions/view-spelling-walk.test.ts +++ b/packages/spec/src/conversions/view-spelling-walk.test.ts @@ -426,7 +426,14 @@ describe('the walker recognizes shapes rather than guessing at them', () => { }); it('returns the SAME row reference when nothing converts (copy-on-write)', () => { - const row = { name: 'clean', object: 'crm_lead', viewKind: 'list', config: { type: 'grid', columns: ['name'] } }; + // The name is DOTTED because this is the file's only record-shaped fixture + // that reads as a template. The conversion chain never looks at `name` — + // measured: flat and dotted both return the same reference with zero + // notices — so the dot changes nothing this case proves. It exists so a + // reader copying this row does not inherit a body `ViewMetadataSchema` + // would reject: a record's `name` is the one spelling where the dot is + // required (`ui/view.zod.ts`'s header block states the three-way rule). + const row = { name: 'crm_lead.clean', object: 'crm_lead', viewKind: 'list', config: { type: 'grid', columns: ['name'] } }; expect(applyConversionsToStoredItem('view', row)).toBe(row); }); }); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 1a8d75ea1f..f4e7345f63 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1,5 +1,53 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. +/** + * View protocol schemas — the `view` metadata type and its three persisted body spellings. + * + * Covers the authoring surfaces (`defineView`, `defineViewItem`), the wire + * doors Studio and the REST layer write through, and + * {@link ViewMetadataSchema} — the union every persisted `view` body is judged + * by. + * + * ## Name grammar depends on the body spelling + * + * {@link ViewMetadataSchema} is a union over three persisted body shapes, and + * they do not share one `name` grammar. Which grammar applies is decided by the + * shape of the body — something an author never names explicitly — so neither + * failure direction below is discoverable from the key being written: + * + * | body spelling | recognised by | `name` is declared as | flat (undotted) name | + * |:---|:---|:---|:---| + * | standalone **ViewItem record** | a nested `config` | {@link ViewItemNameSchema} — `QUALIFIED_ITEM_NAME_PATTERN`, dot REQUIRED | **rejected**, located at `["name"]` | + * | flattened runtime **overlay** | an inline view config; no `config`, no container slot | `z.string().optional()` — no grammar at all | accepted | + * | `defineView` **container** | a container slot (`list` / `form` / `listViews` / `formViews`) | `z.string().optional()` on {@link ViewSchema} — no grammar at all | accepted, and normally IS flat | + * + * The two permissive rows are deliberate, not gaps left to tighten later: + * + * - A container's own name is the **bare object key**. ADR-0017 §3.2's + * dual-read loader registers the aggregated container under `` and + * each expanded item under `.`, so an object-scoped + * container is named `crm_lead` — a name with no dot to carry. + * - An overlay's name is **stamped by the write path**, not authored: + * `normalizeViewMetadata` puts it on every view body at the single write + * chokepoint, and a personalization PUT inherits the identity of the entry it + * shadows. + * + * So both of the readings an author naturally forms are wrong: + * + * - *"the dot is mandatory on every view row"* — read off + * {@link ViewItemNameSchema} alone. It is not: overlay and container rows + * accept flat names, and rows in this repo legitimately use them + * (`case_grid`, `cases`, the container row `crm_lead`). Those rows are + * correct as written, not defects awaiting a dotted rewrite. + * - *"flat names are fine generally"* — read off one of those flat-named rows. + * It is not: put the same name on a standalone ViewItem record and it is + * refused, on the one field the flat-named row told you to fill. + * + * This describes what the three shapes already do; it widens and narrows + * nothing. The one item-name grammar itself lives in + * `shared/identifiers.zod.ts` — grammar changes belong there, not here. + */ + import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; @@ -3106,6 +3154,10 @@ export const ViewSchema = lazySchema(() => strictObject({ // supplies these — declared for the same reason `translation` needed them // (#4001 batch 5): undeclared, they were stripped, and the metadata door // quietly discarded an item's own name on the way through. + // No grammar, and that is the ADR-0017 §3.2 dual-read talking: the + // container is registered under the BARE object key, so its own name is + // `crm_lead` — flat by construction, where a standalone ViewItem record + // must be `crm_lead.`. Stated once in this module's header block. name: z.string().optional().describe('Item name — supplied by the metadata door; for an object-scoped container it is the object name.'), label: I18nLabelSchema.optional().describe('Human-readable label shown in metadata lists.'), object: z.string().optional().describe('Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`.'), @@ -3238,6 +3290,10 @@ export const ViewKindSchema = z */ function viewItemBaseShape() { return { + // The dot is REQUIRED here and NOWHERE ELSE in the `view` family — the + // overlay and container spellings declare an ungrammared `name`. The + // three-way rule, and both directions it is misread in, are stated once in + // this module's header block. name: ViewItemNameSchema, object: z .string() @@ -3628,6 +3684,10 @@ const INLINE_VIEW_KIND_REQUIRED = */ function flattenedViewOverlayFields() { return { + // No grammar, deliberately: the write path stamps this name rather than an + // author writing it, so a flat overlay name is legal here while the SAME + // string is refused by `viewItemBaseShape()`'s `ViewItemNameSchema`. The + // per-spelling rule is stated once in this module's header block. name: z.string().optional().describe('Save name / qualified view id (stamped by the write path).'), object: z .string({ error: (issue) => (issue.input === undefined ? INLINE_VIEW_OBJECT_REQUIRED : undefined) }) diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index e17f5bc607..8a2050ea9f 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -50,7 +50,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol -- `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema +- `node_modules/@objectstack/spec/src/ui/view.zod.ts` — View protocol schemas — the `view` metadata type and its three persisted body spellings. ## How to read these diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 9e68460a29..08694f7664 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -17,7 +17,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/dataset.zod.ts` — Analytics Dataset — the one semantic layer (ADR-0021). - `node_modules/@objectstack/spec/src/ui/page.zod.ts` — Page Region Schema - `node_modules/@objectstack/spec/src/ui/report.zod.ts` — Report Type Enum -- `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema +- `node_modules/@objectstack/spec/src/ui/view.zod.ts` — View protocol schemas — the `view` metadata type and its three persisted body spellings. - `node_modules/@objectstack/spec/src/ui/widget.zod.ts` — Exports: FieldWidgetPropsSchema ## Transitive dependencies