Skip to content

Commit d23dc08

Browse files
claude[bot]os-zhuangclaude
authored
feat(spec,lint,metadata-protocol): a page member on the view type enum — mount a published page on an object view (#13372)
* feat(spec,lint,metadata-protocol): `page` member on the view type enum (#13216) Adds a `page` member to `ListViewSchema.type` plus a `pageName` binding, so an object list view / tab can mount an already-published custom page. Rendering is delegated to the existing page renderer; no metadata type's authorization flags are touched. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry * chore(#13216): regenerate docs, api-surface, react-blocks contract and i18n bundles; derive the batch-closure accumulator from RuntimePendingDeclarations Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry * test(lint): move the two shipped-form predicate census pins for the new `page` surface block `view.form.ts`'s `page` section is gated by `visibleWhen: "data.type == 'page'"` exactly as its seven sibling surface blocks are, so the shipped-metadata-form corpus carries one more predicate (48 -> 49) and one more `==` literal comparison (41 -> 42). Both pins track the corpus, not an issue; the expectation moves in place with the reason recorded beside it. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry * chore(#13216): regenerate the os-regen artifacts from the merged tree Step 4 of scripts/pm/os-regen-merge.sh: the merge commit landed first (the pre-commit hook recorded the deferral), and this commit is the regeneration on top of it — schema, reference docs, liveness counts, api-surface and the react-blocks contract, all from the merged tree. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry --------- Co-authored-by: Claude <jack@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c09451b commit d23dc08

36 files changed

Lines changed: 1173 additions & 54 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": minor
4+
"@objectstack/metadata-protocol": patch
5+
---
6+
7+
feat(spec,lint,metadata-protocol): a `page` member on the `view` type enum — mount an already-published page on an object view (#13216)
8+
9+
A custom page created and published at runtime through the metadata API had no
10+
in-protocol way to reach an end user (#13100's evidence map). App navigation is
11+
closed to runtime content (`app.allowOrgOverride: false`), and the `view` `type`
12+
enum — on one of the five types the platform deliberately leaves open
13+
(`allowOrgOverride: true`, `allowRuntimeCreate: true`) — was closed over
14+
declarative row renderers, so a published page could not be mounted as an
15+
object's list view or tab.
16+
17+
Maintainer ruling 2026-08-29 (live director session, verbatim 「同意」), 方向 1:
18+
19+
> `view``type` 枚举新增 `page` 成员——对象的列表视图/标签页可挂载一个已发布页面。走平台**有意开着**的门(`view` 本就 `allowOrgOverride=true` + 运行时可创建),零新增授权面;设计要点:`page` 型 view 需声明 `pageName` 绑定,校验目标页面存在,渲染委托既有页面渲染器
20+
21+
**Zero new authorization surface, as the ruling's basis requires.** Nothing in
22+
this change touches a metadata type's `allowOrgOverride` / `allowRuntimeCreate`
23+
flags, adds a write door, or adds a read door. A `page` view is a `view` written
24+
through the door `view` already opens, and it holds a NAME — the page itself is
25+
still fetched through the page read path it already had, and still renders
26+
through the existing page renderer, so the page's own audience gate
27+
(`page.assignedProfiles`) rides along unchanged. Delegation is what preserves
28+
that: a second renderer is what would have introduced a second gate.
29+
30+
**The binding, refused in both directions at parse.** `ListViewSchema` gains
31+
`pageName`, declared with `SnakeCaseIdentifierSchema` — the same grammar
32+
`PageSchema.name` carries, so the accepted set is exactly the set of strings that
33+
could name a page. `checkListViewPageMount` then refuses:
34+
35+
- `type: 'page'` with no `pageName` — unlike every other view type there is no
36+
degraded rendering to fall back to, so the view would be blank;
37+
- `pageName` on any other view type — the accepted-and-ignored shape;
38+
- a non-empty `columns` beside a page mount — `columns` is the one required key
39+
on a list view, and the only truthful value for a page mount is `[]`.
40+
41+
The check is attached at all three list-view doors (`ListViewSchema`,
42+
`ObjectListViewSchema`, and the flattened runtime overlay behind
43+
`PUT /api/v1/meta/view`), with a pinned test that fails if any attachment is
44+
dropped.
45+
46+
**Existence of the target page** is answered where the collection is visible:
47+
`defineStack`'s `validateCrossReferences` refuses at build time (same
48+
`pageNames.size > 0` policy the two other page references in that function
49+
already use), and the new `@objectstack/lint` rule `view-page-unresolved`
50+
(`validateViewPageRefs`) resolves it on `os validate` / `os lint` / `os compile`
51+
**and** at the runtime publish gate. Advisory, not gating, for its nav twin's
52+
reason: with no curated cross-package page registry, "unresolved here" cannot be
53+
told apart from "provided by a package this stack cannot see".
54+
55+
Reaching the runtime publish gate needed the per-write snapshot to carry the
56+
`pages` collection (`RuntimeStackContext.pages`, threaded through
57+
`evaluateRuntimeAuthoringGate` and read off the live registry in
58+
`saveMetaItem`'s gate call). That is the one-key widening `RuntimeStackContext`
59+
documents, made when a rule that reads the collection crossed the wall — never
60+
in advance — and the false-positive channel it closes is measured both ways in
61+
`runtime-gate.view-page-refs.test.ts`. `pages` joined `NAME_KEYED_STACK_KEYS` in
62+
the same edit, because a collection that is both context-filled and
63+
write-targeted must have its finding paths name-keyed (#10064).
64+
65+
**Downstream note (not an accept-set narrowing).** No previously valid metadata
66+
becomes invalid: `pageName` is a new key and `page` a new enum member, so every
67+
refusal above can only fire on a document that could not be written before.
68+
What does change for a downstream schema author is composition: `ListViewSchema`
69+
now carries a refinement, and zod 4 refuses `.omit()` / key-overwriting
70+
`.extend()` on a refined object. The unrefined shape stays module-private
71+
(publishing it would mint a duplicate protocol def and a second full set of
72+
ratcheted authorable-surface keys), so a consumer that derived from
73+
`ListViewSchema` by omission should compose with `.safeExtend()` or narrow after
74+
parsing. `FormViewSchema` has had this property since its own refinement landed,
75+
so this is the established shape for view schemas rather than a new one.
76+
77+
**Deliberately out of scope**, per the same ruling: 方向 2 (registering app
78+
navigation at publish time) is deferred to its own design card — it would
79+
require reversing the `app.allowOrgOverride: false` authorization decision — and
80+
with it the known limitation the ruling accepts on the record, that a page
81+
belonging to no object still has no browse-to entry. `page` is also NOT added to
82+
`VisualizationTypeSchema`: the switcher offers alternative ways to draw the same
83+
rows, and a page draws none.

content/docs/references/api/metadata.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,7 @@ Metadata query with filtering, sorting, and pagination
791791
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
792792
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
793793
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
794-
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
794+
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| >; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
795795
| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused — mirror the value onto a stored text field and declare that. |
796796
| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules |
797797
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). |

content/docs/references/api/protocol.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,9 +1595,9 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
15951595
| **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
15961596
| **label** | `string \| Record<string, string>` | optional | Human-readable label shown in metadata lists. |
15971597
| **object** | `string` | optional | 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=`. |
1598-
| **list** | `{ name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }` | optional | |
1598+
| **list** | `{ name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| >; data?: object \| … +3 more; … }` | optional | |
15991599
| **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | |
1600-
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
1600+
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| >; data?: object \| … +3 more; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
16011601
| **formViews** | `Record<string, { type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views |
16021602
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. |
16031603
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
@@ -1614,7 +1614,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16141614
| :--- | :--- | :--- | :--- |
16151615
| **name** | `string` | optional | Internal view name (lowercase snake_case) |
16161616
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
1617-
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | |
1617+
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| >` | optional (default: `"grid"`) | |
16181618
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source configuration (defaults to "object" provider) |
16191619
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` || Fields to display as columns |
16201620
| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) |
@@ -1634,6 +1634,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16341634
| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration |
16351635
| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout |
16361636
| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record<string, any>` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
1637+
| **pageName** | `string` | optional | Published page this view mounts — required when `type: 'page'`, and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own `assignedProfiles` audience. |
16371638
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
16381639
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
16391640
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
@@ -1698,7 +1699,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16981699
| :--- | :--- | :--- | :--- |
16991700
| **name** | `string` | optional | Internal view name (lowercase snake_case) |
17001701
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
1701-
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | |
1702+
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| >` | optional (default: `"grid"`) | |
17021703
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source configuration (defaults to "object" provider) |
17031704
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` || Fields to display as columns |
17041705
| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) |
@@ -1718,6 +1719,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17181719
| **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration |
17191720
| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map configuration — applies when the view renders as a map layout |
17201721
| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record<string, any>` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
1722+
| **pageName** | `string` | optional | Published page this view mounts — required when `type: 'page'`, and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own `assignedProfiles` audience. |
17211723
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
17221724
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
17231725
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |

0 commit comments

Comments
 (0)