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
37 changes: 37 additions & 0 deletions .changeset/view-name-grammar-per-body-spelling.md
Original file line numberDiff line numberDiff line change
@@ -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.
47 changes: 45 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 `<object>` and
each expanded item under `<object>.<viewKey>`, 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.

<Callout type="info">
**Source:** `packages/spec/src/ui/view.zod.ts`
Expand Down
9 changes: 8 additions & 1 deletion packages/spec/src/conversions/view-spelling-walk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
});
60 changes: 60 additions & 0 deletions packages/spec/src/ui/view.zod.ts
Original file line numberDiff line numberDiff line change
@@ -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 `<object>` and
* each expanded item under `<object>.<viewKey>`, 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';
Expand DownExpand Up@@ -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.<viewKey>`. 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=`.'),
Expand DownExpand Up@@ -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()
Expand DownExpand Up@@ -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) })
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Loading