Skip to content

Commit 85ea12f

Browse files
feat(spec)!: a view no longer declares itself personal — scope 'personal' and sharing.type retire (#29447)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9FHuKppFm22FnTZqw5cbJzG)_ Fixes #29037 Clause-②: no (narrowing) Metadata-protocol: narrowed ## 维护者速读 - 改了什么:视图不能再自称"个人视图"。`scope: 'personal'` 和列表视图的 `sharing.type` 都被拒收并给出改法;控制台列表工具栏不再显示"个人视图 / 共享视图"标签,只保留"由谁锁定"。 - 为什么改:这两个声明从来没有限制过谁能看到视图,标成个人的视图其实所有能读这个对象的人都看得到,标签在说假话(#28971 裁决 C:个人视图将来是用户自有记录,不复用这两个键)。 - 风险与回滚:BREAKING 的 spec minor(只收窄)。已存的行和组装产物在读取时自动转换,旧版本写入的默认值 `collaborative` 照收并丢弃,所以现有视图不用改就能加载;回滚就是 revert 本 PR。 - 先看:`packages/spec/src/ui/view.zod.ts`(两处退役与改法文案)和 `packages/spec/src/conversions/registry.ts` 的 `personalViewDeclarationsRemoved` 转换。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --project repo src/ui/view-personal-declarations-retirement.test.ts`,看 `personal` 两处被拒、`collaborative` 被丢弃、转换后的行能通过。 - 需要你做的:看 open questions 里 `collaborative` 残留是照收丢弃还是一起拒收(默认按 #12840 惯例照收丢弃)。 ## What changes Per ADR-0049 enforce-or-remove and ADR-0087, the two inert `personal` declarations retire. - **`view.scope: 'personal'`** is refused by `enumWithRetiredValues` with a prescription at every door that reads a view item's `scope`: `ViewItemSchema`, the wire schema, the `ViewMetadataSchema` record and overlay arms, and `AssembledViewArtifactSchema`. `package` and `shared` are unchanged. - **`view.list.sharing.type`** retires as a whole key with a `retiredKey` tombstone, not just its `personal` value. Once `personal` goes, `collaborative` is the only value left, and the only reader was the console badge. A measured `git grep` over packages/ and ui/ finds no server, runtime or service reader. `lockedBy` stays. - The emitted default `type: 'collaborative'`, which a released toolchain wrote into every parsed `sharing` block, is accepted and stripped through `acceptRetiredDefaultResidue` (house rule #12840). `type: 'personal'` is refused. - **D2 conversion** `view-personal-declarations-removed` (identifier `personalViewDeclarationsRemoved`, `retiredFromLoadPath: true`, `retiredAfter: '18.2.0'`) serves stored-row rehydration, assembled artifacts and `os migrate meta`. It strips `scope: 'personal'` from `views` / `viewItems` rows, and `sharing.type` from list payloads, list view item configs, flattened list overlays and `objects[].listViews.*`. **Route:** it deletes the declaration and does not invent `shared`, because nothing a reader sees changes. - **D3 entry** `ui-view-personal-declarations-retired` carries the part a migration cannot decide (keep the view as everyone-visible, delete it, or wait for user-owned personal views). One ADR-0087 marker sits in the changeset. There is also a `RETIRED_KEYS_BY_MAJOR` entry `ui/ViewSharing:type` and a STEP18 rationale fragment. - The docblocks that called per-user views a "parked direction" now point at #28971 letter C and the ADR notes in PR #29321. - **Console (in surface, the target-2 reading):** - `ui/packages/plugin-list/src/ListView.tsx`: the personal/shared badge is gone, and a view with `sharing.lockedBy` still shows "Locked by …" (`data-testid="sharing-locked-by"`). - `ui/packages/core/src/utils/normalize-list-view.ts`: drops the legacy `sharing: { visibility, enabled }` pair instead of folding it onto the retired `type`. - `list.sharingPersonal` / `list.sharingCollaborative` are removed from all 10 locales. - **Docs:** `content/docs/ui/audience-based-interfaces.mdx` now says per-user list preferences instead of personal views. The liveness row `view.json#list.sharing` is re-cited to the lock reader. - **Regenerated, riding this PR:** `authorable-surface/ui.json`, `authorable-defaults/ui.json`, and `content/docs/references/{api/protocol,data/object,ui/view}.mdx`. ## Forced test edits These are existing tests that pinned the retired values, and each one changes only its fixture: - `packages/spec/src/ui/view.test.ts`: the `ViewSharingSchema` describe was rewritten (an empty block parses to `{}`, and the lock is accepted). Three list-view literals change from `sharing.type` to `sharing.lockedBy`. - `packages/spec/src/ui/view-item-owner-hidden-retirement.test.ts`: the bystander row's `scope: 'personal'` changes to `'shared'`. The owner/hidden conversion fixture in `conversions/registry.ts` gets the same edit. - `packages/spec/src/ui/view-overlay-viewkind-arm.test.ts`: `{ sharing: { lockedBy: 'admin' } }`. - `packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts`: `{ sharing: { lockedBy: 'admin' } }`. - `ui/packages/plugin-list/src/__tests__/ListView.test.tsx`: the sharing tests were rewritten around `sharing-locked-by`. - `ui/packages/plugin-list/src/__tests__/ListViewBlock.inertKeys-13074.test.tsx` and `ListViewBlock.specInputs-13047.test.tsx`: they mount `{ sharing: { lockedBy: 'admin' } }` and assert `sharing-locked-by`. - `ui/packages/core/src/utils/__tests__/normalize-list-view.test.ts`: the legacy pair is dropped, and no `type` is written. - `ui/packages/plugin-view/src/__tests__/ObjectView.namedViewProtocolKeys-8980.test.tsx`: `lockedBy` values. - `ui/packages/types/src/__tests__/p1-spec-alignment.test.ts`: `{ lockedBy }` only. - `ui/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: the `list-view.sharing` reason prose. New test file: `packages/spec/src/ui/view-personal-declarations-retirement.test.ts`. It covers every door, `defineStack` 422, the type-level pins, the D2 conversion and the D3 entry, plus a tree-scoped absence walk. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmTHDhPHuivpyvN8gfF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91f6a0b commit 85ea12f

39 files changed

Lines changed: 975 additions & 292 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@object-ui/plugin-list': minor
4+
'@object-ui/core': minor
5+
'@object-ui/i18n': minor
6+
---
7+
8+
feat(spec)!: a view no longer declares itself personal — `scope: 'personal'` and `sharing.type` are retired, because nothing ever restricted a view to one user
9+
10+
Clause-②: no (narrowing)
11+
12+
Metadata-protocol: narrowed
13+
14+
Skills-docs: none — no file under `skills/**` teaches either declaration (a `git grep` of `skills/` finds no `scope: 'personal'` and no list-view `sharing.type`; its one `sharing` block is the public form link, which this does not touch); `content/docs/ui/audience-based-interfaces.mdx` now says data mode offers per-user list preferences, not personal views.
15+
16+
<!-- adr-0087: registered view-personal-declarations-removed, ui-view-personal-declarations-retired -->
17+
18+
**BREAKING** accept-set narrowing on the view surface (`@objectstack/spec`), ADR-0049 enforce-or-remove.
19+
20+
A view item saved with `scope: 'personal'`, or a list view saved with `sharing: { type: 'personal' }`, was accepted and served, and every user who could read the object still saw it: both switcher read paths list every view bound to the object, and nothing read either declaration as a visibility rule. The console list toolbar, the one reader of `sharing.type`, then labelled the view "Personal view". Personal named views are ruled to be a user-owned record, not view metadata, and they do not reuse either key.
21+
22+
- **FROM → TO.** `{ name, object, viewKind, scope: 'personal', … }` → `{ name, object, viewKind, … }` (omit `scope`, or write `shared` for a runtime-authored view). `list: { …, sharing: { type, lockedBy } }` (and `listViews.<key>`, an object's `listViews.<key>`, a list view item's `config`) → `list: { …, sharing: { lockedBy } }`.
23+
- **The one-line fix.** Delete `scope: 'personal'` and delete `sharing.type`, whatever value it holds. `os migrate meta --from 17` lists the edits for existing sources, and `--write` applies them. A stored `sys_metadata` row and an assembled artifact are converted when they are read (the `view-personal-declarations-removed` conversion), so a view saved with either declaration keeps loading and nothing a reader sees changes.
24+
- **What the parse says.** "`view.scope: 'personal'` was removed in @objectstack/spec 18.3.0 (ADR-0049 enforce-or-remove) — nothing ever restricted a `personal` view item to one user: …" at every door that reads a view item's `scope`, and "`view.list.sharing.type` was removed in @objectstack/spec 18.3.0 (ADR-0049 enforce-or-remove) — nothing enforced it: …" at every list-view door and at `defineStack` as `STACK_SCHEMA_INVALID` / 422. TypeScript rejects both at the authoring site.
25+
- **Residue.** `type: 'collaborative'`, the default a released toolchain wrote into every parsed `sharing` block, is accepted and stripped, so a view built before this release parses without an edit; `personal` is refused.
26+
- **Unchanged.** `scope: 'package'` and `scope: 'shared'`; `sharing.lockedBy`; a form view's public-link `sharing` block.
27+
- **Console.** The list toolbar no longer labels any view personal or shared; a view with `sharing.lockedBy` still shows "Locked by …". `normalizeListViewSchema` drops the legacy `sharing: { visibility, enabled }` pair instead of folding it onto the retired `type`, and the `list.sharingPersonal` / `list.sharingCollaborative` strings are gone from every locale.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
18771877
| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
18781878
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
18791879
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
1880-
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
1880+
| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration |
18811881
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
18821882
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
18831883
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
@@ -1956,7 +1956,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
19561956
| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
19571957
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
19581958
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
1959-
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
1959+
| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration |
19601960
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
19611961
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
19621962
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ const result = ApiMethod.parse(data);
395395
| **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
396396
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
397397
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
398-
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
398+
| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration |
399399
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
400400
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
401401
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |

0 commit comments

Comments
 (0)