Skip to content

Commit 5b5a5c3

Browse files
os-justinclaude
andauthored
feat(types)!: mirror eight ObjectViewSchema keys (three by spec reference) and retire viewTabBar (objectui#7779) (#7922)
`ObjectViewSchema`'s TypeScript face declared ten keys its Zod mirror never did (objectui#7279's `UnmirroredDeclared` reading), so a document authoring any of them passed the validator unexamined through `BaseSchema`'s passthrough. Maintainer ruling B (2026-09-06): liveness first, then mirror-or-retire per key. - objectql.zod.ts: `navigation` / `searchableFields` / `filterableFields` are `SpecListViewSchema.shape.*` by reference (identity-pinned); `allowCreateView` / `viewActions` are the sibling `ViewSwitcherSchema` slots by reference (the renderer forwards both verbatim into the `view-switcher` node); `defaultViewType` (the declaration's seven-value union) / `defaultListView` / `showViewSwitcher` are local literals after a reader census on `plugin-view/src/ObjectView.tsx`; `viewTabBar` is a `retirementTombstone()` (zero reads — the tab-bar config is `ViewTabBar`'s `config` prop from the host, never a node key) - objectql.ts: `viewTabBar?: never` with the RETIRED docblock (the #7322 shape) - `listViews` stays unmirrored on the ruling's own fallback clause, with the measurement pinned against the spec: the declaration's `NamedListView` (about 52 members, seven read) and the spec's strict `ObjectListViewSchema` (requires `columns`, refuses `options`, tuple filters and `default`) are incompatible, and the spec value refuses the named views the docs teach - zod-mirror-parity: the entry shrinks to `listViews`; `ObjectViewSchema` joins `SPEC_DERIVED_PAIRS` by a real code reference; header figures 14 / 96 -> 14 / 87, split 1 / 2 + 13 / 94 -> 2 / 3 + 12 / 84, with the history sentences - object-view-spec-parity: `TS_ONLY_BACKLOG` shrinks to `listViews` + `onNavigate`; the audit figures move 11 / 13 -> 20 / 22 with the arithmetic - new pin object-view-unmirrored-keys-7779: reads derived off disk with controls, membership, spec/sibling identity, accept-and-survive plus wrong-typed refusal at the key per mirrored key, the tombstone on both faces, the `listViews` measurement, neighbour and passthrough controls, the two doc tables - docs: the plugin-view README and plugin-view.mdx import tables no longer teach `viewTabBar` as a node key - changeset: `@object-ui/types` minor, "Breaking for authored metadata" Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0adb329 commit 5b5a5c3

8 files changed

Lines changed: 898 additions & 70 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**Breaking for authored metadata:** `ObjectViewSchema.viewTabBar` is RETIRED
6+
(objectui#7779, maintainer ruling B of 2026-09-06; ADR-0049 enforce-or-remove).
7+
An `object-view` node that authors `viewTabBar` no longer validates: the parse
8+
fails loudly on the `viewTabBar` path with the explanation in the message, and
9+
the TS member is a `?: never` tombstone so the same document is refused at
10+
compile time. Nothing ever read the key off the node — the tab-bar UX config
11+
(`ViewTabBarConfig`, still exported) is the `config` prop of the `ViewTabBar`
12+
component, composed by the host, not authored metadata. Remove the key.
13+
14+
In the same change, eight `ObjectViewSchema` keys the TypeScript interface
15+
declared and the Zod mirror never did are now ENFORCED. Until now a document
16+
authoring any of them passed the validator unexamined through `BaseSchema`'s
17+
passthrough while the published type invited the author to write it — declared
18+
but not enforced (objectui#7279's `UnmirroredDeclared` reading). Each key now
19+
admits exactly what the declaration promises and refuses a wrong-typed value at
20+
its own path; a correctly typed document is untouched.
21+
22+
| key | disposition | how |
23+
| --- | --- | --- |
24+
| `navigation` | mirrored | the spec's `ListViewSchema.navigation` slot by reference (`NavigationConfigSchema`, optional) |
25+
| `searchableFields` | mirrored | the spec's `ListViewSchema.searchableFields` slot by reference (`string[]`, optional) |
26+
| `filterableFields` | mirrored | the spec's `ListViewSchema.filterableFields` slot by reference (`string[]`, optional; the spec marks it a legacy shorthand for `userFilters.fields`) |
27+
| `allowCreateView` | mirrored | the sibling `ViewSwitcherSchema.allowCreateView` slot by reference — the renderer forwards the value verbatim into the `view-switcher` node it composes |
28+
| `viewActions` | mirrored | the sibling `ViewSwitcherSchema.viewActions` slot by reference, for the same reason |
29+
| `defaultViewType` | mirrored | local literal: the declaration's seven-value union (`grid`, `kanban`, `gallery`, `calendar`, `timeline`, `gantt`, `map`) — read as `schema.defaultViewType \|\| 'grid'` |
30+
| `defaultListView` | mirrored | local literal: `string` — read as `namedListViews?.[schema.defaultListView]` |
31+
| `showViewSwitcher` | mirrored | local literal: `boolean` — read as `schema.showViewSwitcher === true` |
32+
| `viewTabBar` | RETIRED | `?: never` + `retirementTombstone()` — zero reads on the node |
33+
| `listViews` | unchanged (still unmirrored) | see below |
34+
35+
**What was measured.** Every reading was taken on the `object-view` node
36+
renderer (`packages/plugin-view/src/ObjectView.tsx`, registered by
37+
`plugin-view/src/index.tsx`) with `schema.objectName` / `schema.layout` as the
38+
positive controls of the same `schema.KEY` query, so each zero is a reading;
39+
the repo-wide census of `viewTabBar` finds the key in no source file outside
40+
`@object-ui/types` (two doc tables listed it as authorable and are corrected
41+
here). The spec side was read through the installed pin
42+
(`@objectstack/spec@17.2.0`, `ui` entry, 117 exported object schemas walked;
43+
control keys `objectName` / `columns` / `navigation` / `listViews` hit): the
44+
three spec-modelled keys are optional slots on `ListViewSchema` and
45+
`ObjectListViewSchema`; the six local keys have no spec slot anywhere.
46+
47+
**`listViews` stays unmirrored, on the ruling's own fallback clause.** The
48+
declaration's value is the local `NamedListView` (about 52 members, of which the
49+
renderer reads seven — `label`, `type`, `columns`, `filter`, `sort`, `options`,
50+
`data`); the spec's `ViewSchema.listViews` is a record of the STRICT
51+
`ObjectListViewSchema`, which requires `columns` and refuses `options`, ObjectQL
52+
tuple filters and `default` — that is, it refuses the named views this package's
53+
own README and `content/docs/api/schema-reference.md` teach (`{ label: 'All
54+
Users' }` fails at `columns`; `filter: [["owner", "=", "..."]]` fails at
55+
`filter.0`). Mirroring the spec value would lose documented behaviour; mirroring
56+
the local value would enforce roughly 45 unread members into the contract — the
57+
very thing ruling B refused for the six local keys. The key therefore stays in
58+
the parity ledger with that measurement, pinned, until the maintainer decides its
59+
value type. It is not papered over with `z.any()`.
60+
61+
**Who is affected:** an author who wrote `viewTabBar` on an `object-view` node
62+
(remove it), or who wrote a wrong-typed value for one of the eight keys — e.g.
63+
`defaultViewType: 'tree'` (host-composition-only, objectui#5321),
64+
`navigation: 'page'` (write `navigation: { mode: 'page' }`),
65+
`searchableFields: 'name'` (write an array), `viewActions: 'share'` (write
66+
`[{ type: 'share' }]`). Such documents used to pass validation and render with
67+
the key ignored; they now fail at the key with the reason.
68+
69+
**Who is NOT affected:** every correctly typed document, and every document
70+
that never wrote these keys — `absent` stays valid on all nine. No renderer
71+
changed. The parity ledger (`zod-mirror-parity.test.ts`) records the move:
72+
`UnmirroredDeclared` 14 entries / 96 keys to 14 / 87, the `ObjectViewSchema`
73+
entry re-derived into the SPEC-DERIVED half because the mirror now references
74+
the spec in code.
75+
76+
Graded `minor`, not `patch`: this narrows the accepted input set, which is
77+
breaking for any author who wrote the tolerated spellings. It is not `major`
78+
per this repo's fixed-group convention (objectui's own breaking changes ship as
79+
`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
80+
mechanically enforced by `scripts/check-changeset-no-major.mjs`).

‎content/docs/plugins/plugin-view.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,10 @@ under your own key" — it ships **no schema types**. The authored
573573

574574
| Import from `@object-ui/types` | What it types |
575575
| --- | --- |
576-
| `ObjectViewSchema` | the whole `type: 'object-view'` node — `objectName` (required), `title`, `description`, `layout`, `defaultViewType`, `listViews`, `defaultListView`, `navigation`, `table`, `form`, `searchableFields`, `filterableFields`, the `show*` flags, `operations`, `onNavigate`, `viewTabBar`, `viewActions` |
576+
| `ObjectViewSchema` | the whole `type: 'object-view'` node — `objectName` (required), `title`, `description`, `layout`, `defaultViewType`, `listViews`, `defaultListView`, `navigation`, `table`, `form`, `searchableFields`, `filterableFields`, the `show*` flags, `operations`, `onNavigate`, `allowCreateView`, `viewActions` (`viewTabBar` is retired — objectui#7779 — and refused by name) |
577577
| `NamedListView` | one entry of `listViews` |
578578
| `ViewNavigationConfig` | `navigation` — row/item click behaviour |
579-
| `ViewTabBarConfig` | `viewTabBar` — tab-bar UX (inline add, overflow, indicators) |
579+
| `ViewTabBarConfig` | the `config` prop of `ViewTabBar` — tab-bar UX (inline add, overflow, indicators), composed by the host; not an `object-view` node key |
580580

581581
```typescript
582582
import type { ObjectViewSchema } from '@object-ui/types';

‎packages/plugin-view/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,10 +639,10 @@ re-exporting, so import it from there:
639639

640640
| Import from `@object-ui/types` | What it types |
641641
| --- | --- |
642-
| `ObjectViewSchema` | the whole `type: 'object-view'` node — `objectName` (required), `title`, `description`, `layout`, `defaultViewType`, `listViews`, `defaultListView`, `navigation`, `table`, `form`, `searchableFields`, `filterableFields`, `show*`, `operations`, `onNavigate`, `viewTabBar`, `viewActions` |
642+
| `ObjectViewSchema` | the whole `type: 'object-view'` node — `objectName` (required), `title`, `description`, `layout`, `defaultViewType`, `listViews`, `defaultListView`, `navigation`, `table`, `form`, `searchableFields`, `filterableFields`, `show*`, `operations`, `onNavigate`, `allowCreateView`, `viewActions` (`viewTabBar` is retired — objectui#7779 — and refused by name) |
643643
| `NamedListView` | one entry of `listViews` |
644644
| `ViewNavigationConfig` | `navigation` — row/item click behaviour |
645-
| `ViewTabBarConfig` | `viewTabBar` — tab-bar UX (inline add, overflow, indicators) |
645+
| `ViewTabBarConfig` | the `config` prop of `ViewTabBar` — tab-bar UX (inline add, overflow, indicators), composed by the host; not an `object-view` node key |
646646

647647
```typescript
648648
import type { ObjectViewSchema } from '@object-ui/types';

‎packages/types/src/__tests__/object-view-spec-parity.test.ts‎

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,20 @@ const tsOwnKeys = Object.keys(TS_OWN_KEYS);
238238
* interface only, which is the drift this guard exists to stop.
239239
*/
240240
const TS_ONLY_BACKLOG = new Set<string>([
241-
'defaultViewType',
241+
// objectui#7779 (maintainer ruling B, 2026-09-06) closed nine of the ten gaps
242+
// this set held: `navigation` / `searchableFields` / `filterableFields` are the
243+
// spec's `ListViewSchema` slots by reference, `allowCreateView` / `viewActions`
244+
// the sibling `ViewSwitcherSchema` slots by reference, `defaultViewType` /
245+
// `defaultListView` / `showViewSwitcher` local literals after a reader census,
246+
// and `viewTabBar` is a tombstone on BOTH faces (declared, refused by name) —
247+
// so none of the nine is TS-only any more. The one that stays:
248+
//
249+
// `listViews` — its VALUE type is undecided (the declaration's `NamedListView`
250+
// vs the spec's strict `ObjectListViewSchema`, which refuses the named views
251+
// this package's docs teach); it stays in the parity ledger with that
252+
// measurement (`zod-mirror-parity.test.ts`, `UnmirroredDeclared`) until the
253+
// maintainer decides. ⛔ Not closed with `z.any()`.
242254
'listViews',
243-
'defaultListView',
244-
'navigation',
245-
'searchableFields',
246-
'filterableFields',
247-
'showViewSwitcher',
248-
'viewTabBar',
249-
'allowCreateView',
250-
'viewActions',
251255
// Not a zod gap: a function, so it CANNOT be declared in a JSON protocol
252256
// schema. Recorded here rather than exempted silently.
253257
'onNavigate',
@@ -268,9 +272,9 @@ const SPEC_COUNTERPART: Record<string, { schema: unknown; key: string; note: str
268272
description: { schema: SpecListViewSchema, key: 'description', note: 'same name, same i18n type difference' },
269273
layout: { schema: SpecNavigationConfigSchema, key: 'mode', note: 'spec is a superset; fold into `navigation`, do not keep a parallel three-value enum' },
270274
defaultViewType: { schema: SpecListViewSchema, key: 'type', note: 'spec is a superset (adds chart, tree); ListViewSchema already imports this enum by reference' },
271-
navigation: { schema: SpecListViewSchema, key: 'navigation', note: 'objectui ViewNavigationConfig is a hand-copied duplicate — re-export by reference' },
272-
searchableFields: { schema: SpecListViewSchema, key: 'searchableFields', note: 'identical string[] — re-export by reference' },
273-
filterableFields: { schema: SpecListViewSchema, key: 'filterableFields', note: 'identical; inherits the spec\'s "legacy shorthand for userFilters.fields" deprecation' },
275+
navigation: { schema: SpecListViewSchema, key: 'navigation', note: 'DONE (objectui#7779): the zod mirror is this very slot by reference (`SpecListViewSchema.shape.navigation`); the TS face is the spec\'s `NavigationConfig` (objectui#4588)' },
276+
searchableFields: { schema: SpecListViewSchema, key: 'searchableFields', note: 'DONE (objectui#7779): the zod mirror is this slot by reference' },
277+
filterableFields: { schema: SpecListViewSchema, key: 'filterableFields', note: 'DONE (objectui#7779): the zod mirror is this slot by reference, and so inherits the spec\'s "legacy shorthand for userFilters.fields" deprecation' },
274278
showSearch: { schema: SpecUserActionsConfigSchema, key: 'search', note: 'scope A step 3, same fold' },
275279
showFilters: { schema: SpecUserActionsConfigSchema, key: 'filter', note: 'scope A step 3, same fold' },
276280
showSort: { schema: SpecUserActionsConfigSchema, key: 'sort', note: 'scope A step 3, same fold' },
@@ -304,7 +308,12 @@ const SANCTIONED_LOCAL = new Set<string>([
304308
'operations',
305309
// View-management chrome. No spec counterpart —
306310
// `UserActionsConfigSchema.buttons` is a string[] of action ids, a different
307-
// shape.
311+
// shape. `allowCreateView` / `viewActions` are READ (forwarded verbatim into
312+
// the `view-switcher` node) and mirrored by reference to that sibling's slots
313+
// (objectui#7779); `viewTabBar` is RETIRED by the same card — a `?: never` /
314+
// `retirementTombstone()` twin, still DECLARED on both faces so an authored
315+
// value is refused by name, which is why it stays in this set: the spec models
316+
// no such key, and the tombstone is objectui-only surface.
308317
'allowCreateView',
309318
'viewActions',
310319
'viewTabBar',
@@ -388,14 +397,17 @@ describe('ObjectViewSchema declared-surface consistency (#2890 scope B)', () =>
388397
).toEqual([...TS_ONLY_BACKLOG].sort());
389398
});
390399

391-
it('reproduces the 2026-07 audit\'s declared-surface figures', () => {
400+
it('reproduces the 2026-07 audit\'s declared-surface figures, moved by objectui#7779', () => {
392401
// Kept executable so "we closed the gap" is a test edit, not a claim.
393402
//
394-
// The audit reported "declared in zod: 13". Reproduced here as 11
395-
// non-envelope keys plus the 2 narrowed envelope keys above — the same 13
396-
// keys, with the arithmetic shown rather than asserted.
397-
expect(ouiZodKeys.filter((k) => !ENVELOPE.has(k))).toHaveLength(11);
398-
expect(ouiDeclaredKeys).toHaveLength(13);
403+
// The audit reported "declared in zod: 13" — reproduced then as 11
404+
// non-envelope keys plus the 2 narrowed envelope keys above. objectui#7779
405+
// added NINE members to the zod shape (eight mirrored keys plus the
406+
// `viewTabBar` tombstone, which is a shape member so it can refuse by name):
407+
// 11 + 9 = 20 non-envelope keys, 20 + 2 = 22 declared. The TS figure below
408+
// did not move — every one of the nine was already declared there.
409+
expect(ouiZodKeys.filter((k) => !ENVELOPE.has(k))).toHaveLength(20);
410+
expect(ouiDeclaredKeys).toHaveLength(22);
399411
// The interface's own surface beyond the envelope. The audit counted 25
400412
// declared fields including the 3 whose names the envelope also owns
401413
// (`type`, `description`, `className`); 22 is that figure with those three

0 commit comments

Comments
 (0)