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
44 changes: 44 additions & 0 deletions .changeset/form-view-option-default-narrowed.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@objectstack/spec': minor
---

feat(spec): narrow the per-option `default` key OUT of the form-view options vocabulary — the object-field face keeps it enforced (#12868)

**BREAKING** accept-set narrowing on the published FormView vocabulary, shipped
as `minor` under the repo's launch-window convention for breaking changes.

<!-- adr-0087: registered form-view-option-default-removed -->

`SelectOptionSchema` serves two surfaces, and only one of them reads the
per-option `default` key:

- **Object-field options** (`Field.select.options`): ENFORCED and UNTOUCHED —
`applyFieldDefaults` falls back to the option marked `default: true` when the
field declares no `defaultValue`, `defaultValue` wins when both are declared,
and the alias rows (`isDefault`/`selected` → `default`) stay.
- **Form-view options** (`FormFieldSchema.options` inside a FormView): the same
key parsed clean and nothing read it — the engine's insert-path fallback
consults the OBJECT definition's options, never a form view's, and no form
renderer seeds a value from it. The maintainer-ruled disposition (2026-08-28,
disposition 甲) narrows the key out of this face only. The ruled census
measured ZERO occurrences of `default` (and the alias spellings
`isDefault`/`selected`) inside form-view options across the tree, the example
apps and the published `*.form.ts` corpus, with the instrument's positive
control hitting the enforced object-field usages.

FROM → TO, and the one-line fix:

- FROM: `{ field: 'status', type: 'select', options: [{ label: 'Open', value: 'open', default: true }] }`
inside a form view's `sections[].fields[]` (or nested `fields`) — parsed
clean, did nothing.
- TO: delete the key from the form-view option. Declare the pre-selected choice
on the OBJECT definition instead — field-level `defaultValue`, or
`default: true` on that field's own `options` entry (both enforced there).
- The new `FormSelectOptionSchema` (an Omit-derivation of `SelectOptionSchema`
minus exactly `default`) refuses the key with a tombstone prescription
carrying this mapping; `isDefault`/`selected` get guidance pointing at the
object definition instead of a rename toward a key the shape refuses.
- The protocol-18 conversion `form-view-option-default-removed` strips the key
from stored sources (pure lossless delete — it never had an effect on this
surface to lose). Run `os migrate meta --from 17` to list the mechanical
edits for existing sources; apply them by hand.
10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
title: Protocol Reference
description: Every schema published by @objectstack/spec — 1604 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1605 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand DownExpand Up@@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with
| [Shared Protocol](/docs/references/shared) | 8 | 32 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **201** | **1604** | 14 protocol modules |
| [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **201** | **1605** | 14 protocol modules |

---

Expand DownExpand Up@@ -367,7 +367,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a

## UI Protocol

**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 152 schemas**
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 153 schemas**

Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer.

Expand All@@ -388,7 +388,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay
| [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` |
| [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` |
| [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` |
| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` |
| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` |

---

Expand Down
27 changes: 22 additions & 5 deletions content/docs/references/ui/view.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,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, FormFieldPublicPickerSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, 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, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormFieldPublicPicker, FormSection, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormFieldPublicPickerSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, 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, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormFieldPublicPicker, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';

// Validate data
const result = AddRecordConfigSchema.parse(data);
Expand DownExpand Up@@ -137,7 +137,7 @@ Column footer summary configuration
| :--- | :--- | :--- | :--- |
| **field** | `string` | ✅ | Field name (snake_case) |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | Field type (auto-infers widget if omitted) |
| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields |
| **options** | `{ label: string; value: string; color?: string; visibleWhen?: string \| object }[]` | optional | Options for select/multiselect/radio/checkboxes fields (per-option `default` is not accepted here — declare the pre-selected choice on the object definition) |
| **reference** | `string` | optional | Target object name for lookup/master_detail fields |
| **publicPicker** | `{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }` | optional | Opt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form. |
| **maxLength** | `integer` | optional | Maximum character length (positive integer; for text/textarea/email/url/phone) |
Expand DownExpand Up@@ -219,12 +219,13 @@ Column footer summary configuration

### Nested Shape: `FormField.options[number]`

Form-view select option — the object-field option shape minus the per-option `default` key (declare the pre-selected choice on the object definition: field-level `defaultValue`, or `default: true` on that field's own `options` entry).

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | ✅ | Display label (human-readable, any case allowed) |
| **value** | `string` | ✅ | Stored value (lowercase machine identifier) |
| **color** | `string` | optional | Color code for badges/charts |
| **default** | `boolean` | optional | Is default option |
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user` — wider than field-level visibleWhen, which has no `current_user`. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions` |

### Nested Shape: `FormField.publicPicker`
Expand DownExpand Up@@ -299,7 +300,7 @@ View filter rule
| :--- | :--- | :--- | :--- |
| **field** | `string` | ✅ | Field name (snake_case) |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | Field type (auto-infers widget if omitted) |
| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields |
| **options** | `{ label: string; value: string; color?: string; visibleWhen?: string \| object }[]` | optional | Options for select/multiselect/radio/checkboxes fields (per-option `default` is not accepted here — declare the pre-selected choice on the object definition) |
| **reference** | `string` | optional | Target object name for lookup/master_detail fields |
| **publicPicker** | `{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }` | optional | Opt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form. |
| **maxLength** | `integer` | optional | Maximum character length (positive integer; for text/textarea/email/url/phone) |
Expand DownExpand Up@@ -328,6 +329,22 @@ View filter rule
| **fields** | `{ field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … }[]` | optional | Sub-fields for composite/repeater/record types |


---

## FormSelectOption

Form-view select option — the object-field option shape minus the per-option `default` key (declare the pre-selected choice on the object definition: field-level `defaultValue`, or `default: true` on that field's own `options` entry).

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | ✅ | Display label (human-readable, any case allowed) |
| **value** | `string` | ✅ | Stored value (lowercase machine identifier) |
| **color** | `string` | optional | Color code for badges/charts |
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user` — wider than field-level visibleWhen, which has no `current_user`. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions` |


---

## FormView
Expand Down
16 changes: 8 additions & 8 deletions docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ regenerate.
| Measure | Value |
|---|---|
| Triaged directories | 5 |
| Object sites in them | 436 |
| Object sites in them | 437 |
| Still-open (strip) sites | 124 |
| Files carrying at least one | 22 |

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

| Dir | Sites | strict | passthrough | catchall | strip |
|---|---|---|---|---|---|
| `ui/` | 168 | 156 | 5 | 0 | 7 |
| `ui/` | 169 | 157 | 5 | 0 | 7 |
| `data/` | 156 | 74 | 1 | 0 | 81 |
| `automation/` | 65 | 42 | 0 | 0 | 23 |
| `security/` | 20 | 7 | 0 | 0 | 13 |
| `studio/` | 27 | 27 | 0 | 0 | 0 |
| **total** | **436** | **306** | **6** | **0** | **124** |
| **total** | **437** | **307** | **6** | **0** | **124** |

## File-level triage — site counts

Expand All@@ -74,9 +74,9 @@ classify and is not listed (it becomes reportable the day it grows its first sit
| `report.zod.ts` | 3 |
| `responsive.zod.ts` | 1 |
| `sharing.zod.ts` | 1 |
| `view.zod.ts` | 57 |
| `view.zod.ts` | 58 |
| `widget.zod.ts` | 1 |
| **total** | **168** |
| **total** | **169** |

### `data/` — sites

Expand DownExpand Up@@ -155,15 +155,15 @@ over it is here.

### `ui/` — open

**7 strip of 168**, in 4 file(s).
**7 strip of 169**, in 4 file(s).

| File | Strip | Sites |
|---|---|---|
| `action-params.zod.ts` | 1 | 1 |
| `app.zod.ts` | 1 | 18 |
| `view.zod.ts` | 4 | 57 |
| `view.zod.ts` | 4 | 58 |
| `widget.zod.ts` | 1 | 1 |
| **total** | **7** | **168** |
| **total** | **7** | **169** |

| Bucket | Sites |
|---|---|
Expand Down
3 changes: 3 additions & 0 deletions packages/spec/api-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,6 +160,9 @@
"FormSection (type)",
"FormSectionParsed (type)",
"FormSectionSchema (const)",
"FormSelectOption (type)",
"FormSelectOptionParsed (type)",
"FormSelectOptionSchema (const)",
"FormView (type)",
"FormViewParsed (type)",
"FormViewSchema (const)",
Expand Down
4 changes: 4 additions & 0 deletions packages/spec/authorable-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -447,6 +447,10 @@
"ui/FormSection:pane",
"ui/FormSection:visibleOn",
"ui/FormSection:visibleWhen",
"ui/FormSelectOption:color",
"ui/FormSelectOption:label",
"ui/FormSelectOption:value",
"ui/FormSelectOption:visibleWhen",
"ui/FormView:allowSkip",
"ui/FormView:aria [RETIRED]",
"ui/FormView:buttons",
Expand Down
3 changes: 3 additions & 0 deletions packages/spec/export-origins/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,6 +160,9 @@
"FormSection": "src/ui/view.zod.ts#FormSection (type)",
"FormSectionParsed": "src/ui/view.zod.ts#FormSectionParsed (type)",
"FormSectionSchema": "src/ui/view.zod.ts#FormSectionSchema (const)",
"FormSelectOption": "src/ui/view.zod.ts#FormSelectOption (type)",
"FormSelectOptionParsed": "src/ui/view.zod.ts#FormSelectOptionParsed (type)",
"FormSelectOptionSchema": "src/ui/view.zod.ts#FormSelectOptionSchema (const)",
"FormView": "src/ui/view.zod.ts#FormView (type)",
"FormViewParsed": "src/ui/view.zod.ts#FormViewParsed (type)",
"FormViewSchema": "src/ui/view.zod.ts#FormViewSchema (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/json-schema.manifest/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@
"ui/FormField",
"ui/FormFieldPublicPicker",
"ui/FormSection",
"ui/FormSelectOption",
"ui/FormView",
"ui/GalleryConfig",
"ui/GanttConfig",
Expand Down
Loading
Loading