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/rare-jars-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'@objectstack/spec': major
---

**BREAKING (authoring gate tightens): `ViewItemSchema` is split into an authoring schema and a wire variant.**

`ViewItemSchema` used to carry two contracts at once — the authoring surface
`defineViewItem()` and Studio's view-create form parse, AND member 1 of the
`ViewMetadataSchema` union that `saveMetaItem` validates every persisted `view`
body against. Because the second role needs Studio's round-trip keys through,
the shape stayed open, and an authoring typo was silently dropped:

```ts
defineViewItem({ name: 'crm_lead.pipeline', object: 'crm_lead', viewKind: 'list', confg: { … } })
// before: parsed clean → a ViewItem with NO view configuration at all
// after: Unrecognized key(s) on this view item: `confg`. Did you mean `config`?
```

**What changed**

- `ViewItemSchema` is now strict on both arms. It is the authoring gate.
- `ViewItemWireSchema` (new export) is the `.strip()` wire variant and is
member 1 of `ViewMetadataSchema`. It **declares** `isPinned` and `sortOrder`,
the Studio switcher keys the console round-trips.
- `ViewFilterRuleSchema` and the `ListView.sort[]` entry are now strict too,
and `ListView.sort[]` rejects `direction` with a pointer to `order` (the two
spell the same tuple, and the wrong one reversed the sort silently).
- New exports: `ViewItemWireSchema`, `ViewItemWire`, `stripViewConsoleDecorations`,
`VIEW_CONSOLE_ROW_DECORATIONS`.

**Migration — authored metadata (`*.view.ts`, `defineViewItem`, published packages)**

| you wrote | on a … | now |
|:---|:---|:---|
| `confg:` / any undeclared key | view item | rejected, with the closest declared key suggested |
| `isPinned:` | view item | remove it — per-user Studio state, written by the console |
| `sortOrder:` | view item | use `order` for the authored default position |
| `id:` | filter rule / sort entry | remove it — a console row key, never authored |
| `direction: 'desc'` | sort entry | `order: 'desc'` |

**Nothing changes for the console/write path.** The `view` metadata write door
still accepts every body the platform itself writes: pinning a saved view, the
column-sort PUT and the filter-save PUT all parse exactly as before. The
console's row `id`s are removed by `stripViewConsoleDecorations` before
validation — the write-path mirror of `stripReadDecorations` — and
`saveMetaItem` still persists the original body verbatim, so those ids
round-trip to the renderer untouched. `id` was deliberately **not** declared:
it is a React list key, and declaring it would put a UI artifact on the
authorable surface.
71 changes: 69 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Migrated to [shared/http.zod.ts](/docs/references/shared/http). Re-exported here
## TypeScript Usage

```typescript
import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';

// Validate data
const result = AddRecordConfigSchema.parse(data);
Expand Down Expand Up @@ -817,6 +817,73 @@ This schema accepts one of the following structures:
---


---

## ViewItemWire

### Union Options

This schema accepts one of the following structures:

#### Option 1

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **viewKind** | `'list'` | ✅ | |
| **config** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }` | ✅ | List-family view configuration. |
| **name** | `string` | ✅ | Globally-unique view id, `<object>.<viewKey>`. |
| **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. |
| **label** | `string` | optional | Display label (supports i18n). |
| **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. |
| **order** | `integer` | optional | Sort order within the object's view switcher / left rail. |
| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). |
| **owner** | `string` | optional | Owner user id — set when `scope` is `personal`. |
| **hidden** | `boolean` | optional | Hidden from the switcher (per-user / per-org declutter). |
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **isPinned** | `boolean` | optional | Studio round-trip: view pinned in the switcher (per-user state, written by the console — not authored). |
| **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). |

---

#### Option 2

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **viewKind** | `'form'` | ✅ | |
| **config** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | ✅ | Form view configuration. |
| **name** | `string` | ✅ | Globally-unique view id, `<object>.<viewKey>`. |
| **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. |
| **label** | `string` | optional | Display label (supports i18n). |
| **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. |
| **order** | `integer` | optional | Sort order within the object's view switcher / left rail. |
| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). |
| **owner** | `string` | optional | Owner user id — set when `scope` is `personal`. |
| **hidden** | `boolean` | optional | Hidden from the switcher (per-user / per-org declutter). |
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **isPinned** | `boolean` | optional | Studio round-trip: view pinned in the switcher (per-user state, written by the console — not authored). |
| **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). |

---


---

## ViewKind
Expand Down
26 changes: 13 additions & 13 deletions docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ regenerate.
| Measure | Value |
|---|---|
| Triaged directories | 5 |
| Object sites in them | 474 |
| Still-open (strip) sites | 219 |
| Object sites in them | 476 |
| Still-open (strip) sites | 217 |
| Files carrying at least one | 34 |

Remaining strip sites by class:

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 15 |
| authorable — the ruling's forced scope | 11 |
| unresolved — needs a per-schema verdict | 33 |
| wire / open — out of forced scope | 104 |
| wire / open — out of forced scope | 106 |
| no door — no carrier, ADR-0049 territory | 36 |
| no gate — carrier live, no parse | 31 |

Expand All @@ -43,12 +43,12 @@ The `strict` column is the one the campaign schedules against; it counts both th

| Dir | Sites | strict | passthrough | catchall | strip |
|---|---|---|---|---|---|
| `ui/` | 190 | 112 | 5 | 0 | 73 |
| `ui/` | 192 | 116 | 5 | 0 | 71 |
| `data/` | 162 | 54 | 1 | 0 | 107 |
| `automation/` | 75 | 49 | 0 | 0 | 26 |
| `security/` | 20 | 7 | 0 | 0 | 13 |
| `studio/` | 27 | 27 | 0 | 0 | 0 |
| **total** | **474** | **249** | **6** | **0** | **219** |
| **total** | **476** | **253** | **6** | **0** | **217** |

## File-level triage — site counts

Expand Down Expand Up @@ -78,9 +78,9 @@ classify and is not listed (it becomes reportable the day it grows its first sit
| `sharing.zod.ts` | 1 |
| `theme.zod.ts` | 6 |
| `touch.zod.ts` | 7 |
| `view.zod.ts` | 51 |
| `view.zod.ts` | 53 |
| `widget.zod.ts` | 9 |
| **total** | **190** |
| **total** | **192** |

### `data/` — sites

Expand Down Expand Up @@ -160,7 +160,7 @@ over it is here.

### `ui/` — open

**73 strip of 190**, in 11 file(s).
**71 strip of 192**, in 11 file(s).

| File | Strip | Sites |
|---|---|---|
Expand All @@ -173,15 +173,15 @@ over it is here.
| `keyboard.zod.ts` | 4 | 4 |
| `offline.zod.ts` | 3 | 3 |
| `touch.zod.ts` | 7 | 7 |
| `view.zod.ts` | 5 | 51 |
| `view.zod.ts` | 3 | 53 |
| `widget.zod.ts` | 9 | 9 |
| **total** | **73** | **190** |
| **total** | **71** | **192** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 6 |
| authorable — the ruling's forced scope | 2 |
| unresolved — needs a per-schema verdict | 0 |
| wire / open — out of forced scope | 0 |
| wire / open — out of forced scope | 2 |
| no door — no carrier, ADR-0049 territory | 36 |
| no gate — carrier live, no parse | 31 |

Expand Down
Loading
Loading