Commit e967cbd
Fixes #20456
Clause-②: yes (narrowing)
Stage (ii) of ruling 甲 on #20051 (`5856781584`), the **spec end** of
this `Seam:` card: every key objectui's console writes onto a stored
`view` row and reads back is now declared, with its meaning, on the wire
member that judges that row, so a parse of the row keeps it. The census
below is the measurement the declarations follow. ⛔ Nothing about what
is persisted changes: `saveMetaItem` still stores the request body, and
the three GUARD pins are green and untouched (evidence under
Verification). The objectui end ("objectui aligns its reads to the
declared spellings") is the seat's follow-up card, filed at ACCEPT with
`Blocked-by:` this PR; its items are listed at the end.
## Census: what the console writes onto a stored `view` row and reads
back
Measured against objectui at the `.objectui-sha` pin
`dd3f7e1be3561d63267d7162f3fc0ac52e72834d`, fetched into an isolated
clone. Spec side read on this branch, merged with `origin/main`
`e956924e`.
**Method, two legs.**
1. **Reader leg (syntax walk).** A TypeScript compiler-API walk (syntax
only) of the console's stored-view read seams:
`data-objectstack/src/index.ts` (`listViews`, `listViewOverrides`,
`getView`, `updateView`, `updateViewConfig`, `createView`,
`mergeViewPatch`, `narrowPersonalizationOverlay`,
`isPersonalizationOverlayRow`, `viewItemObjectName`, `unwrapViewDraft`
and the three key constants), `app-shell/src/utils/viewIdentity.ts`,
`app-shell/src/views/ObjectView.tsx` (the view-row functions, the
saved-view load, the tab builder and sort, the switcher handlers),
`plugin-view/src/ViewTabBar.tsx`,
`plugin-view/src/config/view-config-utils.ts` (filter / sort row
read-back), `app-shell/src/providers/MetadataProvider.tsx`,
`InterfaceListPage.tsx`, `apps/console` `FormPage.tsx` and
`PublicFormsPage.tsx`, `ResourceEditPage.tsx`, and the export-options
readers. It collects every non-call property read, string-keyed element
access, destructured key, `in` test and key-list literal: **154 distinct
keys**. The spec's own view vocabulary (**280 keys**, walked off the Zod
defs of the four `ViewMetadataSchema` members) was subtracted, leaving
105, each classified by hand (most are props, locals and client
objects).
2. **Writer leg (parse diff).** Each console write body, built from its
writer site, run through `ViewMetadataSchema` and diffed, parse output
against input: what the parse drops, rewrites or adds.
**Controls.** LIT: `isPinned`, a known key, is found at 11 sites; an
injected fixture key `zzLitControlKey` is found at its 1 site. DARK: a
fabricated key `pinnedAtEpoch` is found at 0 sites. Zero readings in the
table are readings, not a dead scan.
### Round-trip keys: declared by this PR, or declared before it
Parse columns are before and after this PR. `viewItem` is a ViewItem
record row (`{ name, object, viewKind, config }`); `listOverlay` is a
flattened list row.
| key | written by (objectui at the pin) | read back at | parse before |
parse after | meaning |
|:--|:--|:--|:--|:--|:--|
| `isPinned` | pin toggle `ObjectView.tsx:2120` via `updateView` merge
`index.ts:2993`; config save carries it `ObjectView.tsx:1102`, `:1161` |
`ObjectView.tsx:910`, `:3398`; `ViewTabBar.tsx:330` (pinned group) |
kept on `viewItem`, **dropped on `listOverlay`** | kept on both | pinned
in the view switcher; the row has no per-user scope |
| `sortOrder` | drag-reorder `ObjectView.tsx:966`
(`reorderViewPatches`); config save `:1102` | `ObjectView.tsx:913`,
`:1925`-`:1934` (tab order) | kept on `viewItem`, **dropped on
`listOverlay`** | kept on both | position among the object's saved
views, 0-based over saved views only; `order` stays the authored default
|
| `visibility` | no control sets it; config save carries a stored value
`ObjectView.tsx:1102`; a saved view's toolbar save writes the whole tab
`:1085` | `ObjectView.tsx:912`, `:3399`; `ViewTabBar.tsx:336` (group
order), `:385` (private divider), `:442` (lock icon) | **dropped on
both** | kept on both | switcher grouping only (`private` / `team` /
`organization` / `public`), **not** access control |
| `_isOverride` | `updateViewConfig` `index.ts:5340`, on the row it
writes for a toolbar change to a code-defined view |
`isPersonalizationOverlayRow` `index.ts:2778`, used by `listViews`
`:5448` and `narrowPersonalizationOverlay` `:2895` | **dropped on
`listOverlay`** | kept | marks the row as that view's settings overlay,
not a saved view of its own |
| `isDefault` | set-default `ObjectView.tsx:946`
(`setDefaultViewPatches`) | `ObjectView.tsx:911`, `:3397`;
`MetadataProvider.tsx:420`; `index.ts:5470` | kept (declared; no meaning
on the overlay) | kept, meaning added | the object's default view in the
switcher |
| `columnState` | toolbar `ObjectView.tsx:2803`, `:3112`; config save
`:1102` | `ObjectView.tsx:2786`; `index.ts:2832` (overlay-owned keys) |
kept (declared by #9933) | kept | column order and widths; the
"per-user" wording was corrected |
These six are the new export **`VIEW_CONSOLE_ROUND_TRIP_KEYS`**
(`@objectstack/spec/ui`), each mapped to the members its rows use. That
record is the spec symbol stage (iii)'s ADR-0005 appendix (c) note can
cite.
### Found by the census and mapped to an existing declared spelling (no
new key)
| key | where | declared spelling | why no new declaration |
|:--|:--|:--|:--|
| `objectName` (also `object_name`) | stamped on rows read
`ObjectView.tsx:1802`; written back by a saved view's toolbar save
`:1085`; read `index.ts:2698`, `ResourceEditPage.tsx:933` | `object`
(declared, required on both overlays) | every reader already falls back
to `object`; a second spelling of one field is what this contract
refuses |
| top-level `id` / `_id` | written by the same tab spread; read only by
`viewRowId` `viewIdentity.ts:95`, after `name` | `name` | the write path
stamps `name` on every row, so `id` is never consulted for a stored row
|
| `filter[].id` / `sort[].id` | builder rows `view-config-utils.ts:145`,
`:159`, `:319` | `VIEW_CONSOLE_ROW_DECORATIONS`, removed before the
parse by `stripViewConsoleDecorations` | read back as `item.id \|\|
crypto.randomUUID()` (`:159`, `:319`): a row without one gets a fresh
id, so a parsed row loses nothing the console shows |
| `exportOptions` as a bare array | a stored legacy value, read
`ObjectView.tsx:2839` | the object form `{ formats, … }`, which the
parse already lifts the array to | the export menu reads
`exportOptions.formats` (`ObjectGrid.tsx:3888`); only
`ListView.tsx:1783` folds the array itself |
### Found, and not stored-row round-trip keys
- `_draft` (`index.ts:5455`): a read decoration. The read path stamps
it, `saveMetaItem` strips it before anything else
(`stripReadDecorations`), and it is never stored.
- **Read, never written by the console:** `showSearch` / `showFilters` /
`showSort` (`ObjectView.tsx:907`-`:909`, `:3139`-`:3141`), `allowExport`
(`:2838`-`:2839`), `created_at` (`:1936`, the saved-view sort
tie-break), `updatedAt` / `updated_at`, `viewType` and an `item.spec`
envelope (`PublicFormsPage.tsx:142`, `:150`, `:163`;
`FormPage.tsx:1462`). None is declared on any view member. A stored row
carries one only if an author wrote it through the save door, where the
parse strips it and the save stores it. Stage (iv) would drop them from
such rows, so they belong in stage (iv)'s production census, and the
objectui card decides their reads (declared spellings exist for three:
`userActions.search` / `.sort` / `.filter`).
### Production `sys_metadata`
**NOT MEASURED.** This container holds no connection to any deployed
environment: no `OS_DATABASE_URL`, `TURSO_*` or `PG*` variable is set
(an environment grep answers empty), and nothing here reaches a customer
store. The count of stored views carrying undeclared top-level keys,
which stage (iv) needs, has to be taken by a seat with production
access. The census above names what to count: rows carrying
`objectName`, a top-level `id`, a legacy `exportOptions` array, any of
the read-only keys listed just above, and any `visibility` outside the
four groups or `_isOverride` other than `true` (now refused on re-save).
## What changes in `packages/spec`
- `viewSwitcherRowStateFields()` declares `isPinned`, `sortOrder` and
`visibility` once, each with `.describe()` meaning, spread into the
ViewItem wire member (`viewItemWireFields()`) and the flattened list
overlay. The form overlay gets none of them: the switcher lists
list-family views only, and no console write puts them on a form row.
- `listOverlayRoundTripFields()` adds `_isOverride: true` on the
flattened list overlay. The overlay's existing `isDefault` gains its
meaning.
- `VIEW_CONSOLE_ROUND_TRIP_KEYS` is exported (api-surface /
export-origins regenerated with the tools).
- The authoring door (`ViewItemSchema`) names `visibility` in its
refusal guidance, and says it is not access control.
- `columnState`'s declared meanings no longer call it per-user state: a
stored view row is environment metadata (ADR-0017 as amended).
## Verification
All at `2530b598` (this branch merged with `origin/main` `e956924e`)
unless noted. The only later commit, `a47aeb5d`, rewrites one code
comment in `view.zod.ts`. The derived gate union runs at that head, and
its result is in the report. Head `fc5a47d0` then merges `origin/main`
`75b21692` through `scripts/pm/os-regen-merge.sh` (api-surface and
export-origins regenerated on the merged tree; `check:generated` green)
and adds the changeset's narrowing arm. The changeset gates at that head
are reported on #20456.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **570 files, 16730 passed, 1 todo**, exit 0.
- `pnpm --filter @objectstack/spec exec vitest run --project repo
--maxWorkers=2`: **38 files, 690 passed**, exit 0.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- **The three GUARD pins, unchanged and green** (dependency closure
`@objectstack/objectql^...` built first, exit 0): `metadata-protocol`
`protocol.graft-folded-form-sections.test.ts` (holds "GUARD: Studio-only
round-trip keys still survive the save") and
`protocol.graft-normalized-operators.test.ts` (holds "keeps Studio-only
auxiliary fields a `parsed.data` swap would strip"): 2 files, 42 passed.
`objectql` `protocol-meta.test.ts` (holds "preserves Studio-only
auxiliary fields verbatim"): 95 passed.
- `check:generated`: the first run named exactly 3 stale artifacts
(api-surface, export-origins, reference docs), and they were regenerated
with their `gen:` commands. `check:authorable-surface` was green: no
authorable key moved.
- **Ablation** (on committed `d19cbad8`, via
`scripts/ablation-replace.mjs`): the `visibility` declaration renamed
away (anchor hit 1 time, blob `d2aacf7a` became `3dd7dd8a`). The closure
pin went red, 6 of 34: the declared-member and parse-keeps pairs for
`viewItem` and `listOverlay`, and both typed-refusal cases. The restore
is proven: blob back to `d2aacf7a` equal to HEAD, `git diff HEAD` empty.
The test reads the spec source directly (a relative import), so no
`dist/` leg applies.
- **Reverse type check** against the rebuilt `dist/ui/index.d.ts` from a
scratch consumer: `visibility: 'everyone'` on a `ViewItemWire` fails
with TS2322 (exit 2); without that line, `visibility: 'team'` and
`VIEW_CONSOLE_ROUND_TRIP_KEYS._isOverride` typecheck (exit 0).
## Acceptance notes
- **Ill-typed values are now refused.** A declared key is typed, so a
`view` save carrying a non-boolean `isPinned`, a non-integer
`sortOrder`, a `visibility` outside the four groups, or an `_isOverride`
other than `true` is refused (422) where it used to be stripped and
stored. The console writes none of those. Following the seat's answer on
#20456 (comment `5874463510`), the changeset declares this: the `yes
(narrowing)` arm, a **BREAKING** line naming the refused class with its
remedy (correct the value or delete the key), and the ADR-0087
disposition `not-required (no-migration-prescription)`. The level stays
`minor`.
- **`visibility` is a naming trap.** `private` gets a lock icon in the
switcher (`ViewTabBar.tsx:442`) and restricts nobody. It is declared as
the ruling orders, with an honest meaning, and the authoring door's
guidance says so.
- The authoring-door refusal texts for `isPinned` / `sortOrder` /
`columnState` (`VIEW_ITEM_SURFACE`, `ListViewShapeSchema` guidance)
still say "per-user". Noted, not changed here: they are refusal prose,
not the wire.
- **For stage (iv).** (a) The metadata-protocol GUARD fixture is a FORM
overlay carrying `isPinned` / `sortOrder`. No console write puts those
on a form row, so when the pins flip, that fixture moves to a list
overlay or drops the two keys. (b) The parse adds `type: 'grid'` to a
column-less list patch, and form sections gain `collapsible` /
`collapsed` / `columns` defaults: stored parsed, those defaults land in
rows. (c) `_isOverride` must survive, or a stored toolbar overlay comes
back as a saved view with its merge un-narrowed; this PR is what makes
it survive.
## objectui end (the seat's follow-up card, `Blocked-by:` this PR)
Align the console's reads to the declared spellings:
- read the bound object from `object`, and stop stamping and writing
back `objectName` (`ObjectView.tsx:1802`, `index.ts:2698`,
`ResourceEditPage.tsx:933`);
- stop writing the tab's `id` into a saved view's row
(`buildPersistedViewBody`, `ObjectView.tsx:1085`);
- `listViews`' flatten of a ViewItem record (`index.ts:5466`-`:5472`)
carries only `name` / `label` / `isDefault` / `_draft`. The record's
declared row state (`isPinned`, `sortOrder`, `visibility`,
`columnState`) is written there but not surfaced by that reader;
- retire the bare-array `exportOptions` fold (`ListView.tsx:1783`) once
rows are stored parsed;
- decide the read-only keys: `showSearch` / `showFilters` / `showSort` →
`userActions.*`, and `allowExport`, which has no declared spelling.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4b2d904 commit e967cbd
9 files changed
Lines changed: 385 additions & 33 deletions
File tree
- .changeset
- content/docs/references/ui
- packages/spec
- api-surface
- export-origins
- src
- system
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2256 | 2256 | | |
2257 | 2257 | | |
2258 | 2258 | | |
2259 | | - | |
2260 | | - | |
2261 | | - | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
| 2262 | + | |
2262 | 2263 | | |
2263 | 2264 | | |
2264 | 2265 | | |
| |||
2327 | 2328 | | |
2328 | 2329 | | |
2329 | 2330 | | |
2330 | | - | |
2331 | | - | |
| 2331 | + | |
| 2332 | + | |
2332 | 2333 | | |
2333 | 2334 | | |
2334 | 2335 | | |
| |||
2356 | 2357 | | |
2357 | 2358 | | |
2358 | 2359 | | |
2359 | | - | |
2360 | | - | |
2361 | | - | |
| 2360 | + | |
| 2361 | + | |
| 2362 | + | |
| 2363 | + | |
2362 | 2364 | | |
2363 | 2365 | | |
2364 | 2366 | | |
| |||
2402 | 2404 | | |
2403 | 2405 | | |
2404 | 2406 | | |
2405 | | - | |
2406 | | - | |
| 2407 | + | |
| 2408 | + | |
2407 | 2409 | | |
2408 | 2410 | | |
2409 | 2411 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
419 | 419 | | |
420 | 420 | | |
421 | 421 | | |
| 422 | + | |
422 | 423 | | |
423 | 424 | | |
424 | 425 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
| 408 | + | |
408 | 409 | | |
409 | 410 | | |
410 | 411 | | |
| |||
Lines changed: 3 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
304 | 304 | | |
305 | 305 | | |
306 | 306 | | |
307 | | - | |
| 307 | + | |
308 | 308 | | |
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
| 314 | + | |
315 | 315 | | |
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
319 | 319 | | |
320 | 320 | | |
321 | | - | |
| 321 | + | |
322 | 322 | | |
323 | 323 | | |
324 | 324 | | |
| |||
0 commit comments