Repository navigation
feat(spec): declare the console's round-trip keys on the stored view wire (#20456) - #20474
Conversation
…wire The stored view overlay's `.strip()` dropped the keys objectui's console writes onto a stored `view` row and reads back: `isPinned` / `sortOrder` on the flattened list overlay, `visibility` on both the list overlay and the ViewItem record, and the settings-overlay marker `_isOverride`. `saveMetaItem` stores the request body verbatim, so they lived in the store and nowhere in the contract. - `viewSwitcherRowStateFields()` declares `isPinned`, `sortOrder` and `visibility` once, with their meaning, for the ViewItem wire member and the flattened list overlay. - The list overlay also declares `_isOverride: true`, and its existing `isDefault` gains its meaning. - `VIEW_CONSOLE_ROUND_TRIP_KEYS` records the census: each round-trip key and the members whose rows carry it. - The authoring door names `visibility` in its refusal guidance. What is persisted does not change: the save still stores the request body. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…nd-trip keys Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…the changeset - api-surface / export-origins record the new `VIEW_CONSOLE_ROUND_TRIP_KEYS` export; the view reference page carries the declared meanings. - A stored `view` row has no per-user scope, so the column-layout descriptions no longer call it per-user state. - Changeset: `@objectstack/spec` minor, with the Clause-② declaration. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…ored-overlay-round-trip-keys
…rewritten comment Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 126d18056f072483519e730039e9b3632cb102f6 && git checkout 126d18056f072483519e730039e9b3632cb102f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e028591857980ae69b9f9badb380dfa61367e62 fc5a47d08d0c842125d9ccc97c91b7e858647d4b && git checkout -B drift-repro 8e028591857980ae69b9f9badb380dfa61367e62 && git merge --no-ff fc5a47d08d0c842125d9ccc97c91b7e858647d4b
node scripts/docs-audit/affected-docs.mjs --json 8e028591857980ae69b9f9badb380dfa61367e62
|
…ored-overlay-round-trip-keys
…tree The os-regen driver kept one side of both ui.json artifacts in the merge of origin/main; regenerated from the merged source they carry main's new component-props exports and this branch's VIEW_CONSOLE_ROUND_TRIP_KEYS. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…s (narrowing), BREAKING line, ADR-0087 disposition Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20456 (body + all 4 comments: claim ① Derived judgmentsAccept-set changes (each named).
Public surface. (a) The census. Method (a compiler-API syntax walk of the named readers, 154 keys, minus the 280-key spec vocabulary, hand-classified; a parse diff of each console write body) with lit controls (b) The six declarations. Each typed right (above), each with a true (c) Persistence untouched. Other edits. The "per-user" drop on the Pin. Merge. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ing origin/main os-regen-merge.sh step 3: the merge driver deferred this generated path (both sides changed it) and kept main's side; pnpm --filter @objectstack/spec gen:schema && gen:docs re-derives it on the merged tree, carrying forward both this branch's timeZone describe fix and main's #20474 round-trip-key docs. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…persist as real instants (objectstack-ai#20495) Fixes objectstack-ai#20466 Clause-②: no ## What changed `packages/spec/src/ui/view.zod.ts`, the `GanttConfig` `timeZone` member's `.describe()`: replaced only the false clause "persisted data stays real instants" with the card's true wording — a datetime value is still written as the real instant, and a date value as the calendar day it was dropped on in this zone's calendar (`YYYY-MM-DD`). Nothing else in the describe moved. Verified against the spec's own storage rule (`packages/core/src/utils/temporal-storage-form.ts`, ADR-0053): `datetime` stores canonical UTC ISO text; `date` stores a timezone-naive `YYYY-MM-DD` calendar day. The new sentence is true of the contract, not of one renderer. No mention of objectui's DST shim defect (objectstack-ai/objectui#10866 slice 5) — that stays a renderer bug, per triage. Regenerated the reference docs that carried the old sentence verbatim (`content/docs/references/ui/view.mdx` ×3, `component.mdx` ×1) via `pnpm --filter @objectstack/spec gen:schema && gen:docs` — never by hand. Grepped the exact phrase across the whole repo; the only other copies were those four generated doc occurrences (plus the JSON-Schema manifest, which only ratchets schema names, not prose). No hand-written copy found, no test pins the old text. ## Verification record - `pnpm --filter @objectstack/spec build` — green (36/36 declared `.d.ts`, 130 declaration files, 394/394 relative references resolved). - `pnpm --filter @objectstack/spec check:generated` — 15/15 generated artifacts green (only `check:docs` was stale before `gen:docs`, now current). - `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` + scripts + test-layer) — green. - `pnpm --filter @objectstack/spec test` — 572 test files, 16789 tests passed, 1 todo (unrelated pre-existing todo). - Targeted first: `view.test.ts` + `view-gantt-tree-config-closed-15469.test.ts` — 502 tests passed. No test pins the old describe text; none needed updating. - `packages/spec`'s own build closure (`pnpm --filter '@objectstack/spec^...' build`) is EMPTY — nothing in this repo depends on `@objectstack/spec` as a workspace package the way `turbo`'s graph tracks it here, so ① has nothing to build. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 102 gate commands from the diff (4 files, +25/-5, 30 changed lines). Ran all 102: - **99/102 green** on the real command (exit 0), including `check:doc-authoring` (no tracker number in the describe), `check:nul-bytes`, `check:merge-driver`, `check:changeset-no-major`, `check:empty-changeset`, `check:issue-citations`/`check:closing-keyword-parity` (the `Fixes objectstack-ai#20466` line), and every `packages/spec` `check:*` gate the diff touches. - **3/102 NOT MEASURED** — `PREREQUISITE NOT MET` (exit 3, not a finding): `check:skill-examples` (client SDK surface needs `@objectstack/client-react`+`@objectstack/client` built), `check:dual-build-cjs-loads` (needs ~80 packages' `dist/`, effectively a whole-repo build) and `check:lean-entry-closure` (needs `@objectstack/objectql` built). None is in `packages/spec`'s own build closure (which is empty); building them locally would mean rebuilding most of the monorepo for a single-clause prose fix. Deferred to CI's full build, which measures all three. - 3 of the original 6 exit-3s (`check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift`) needed only `@objectstack/formula` + `@objectstack/lint` built — built those two directly and reran: all green. - Merged `origin/main` (view.zod.ts carried seat 4's landed objectstack-ai#20474 `view-console-round-trip-keys` change past my region) via `bash scripts/pm/os-regen-merge.sh`: the driver deferred `content/docs/references/ui/view.mdx` (both sides changed it), regenerated with `gen:schema && gen:docs` on the merged tree, verified `objectstack-ai#20474`'s symbols and prose survived byte-for-byte (`git grep` against `origin/main`), then committed the regeneration as its own commit per the runbook. `git diff origin/main HEAD -- packages/spec/src/ui/view.zod.ts` shows exactly the one-clause change, nothing else. - Reverified no further origin/main commits touch this file surface after the merge (`git log HEAD..origin/main -- <the 3 touched paths>` — empty). ## Acceptance notes - objectui's `@object-ui/types` zod mirror (`stripImportedDefaults(SpecGanttConfigSchema).shape.timeZone`) is a spec-derived reuse of this describe. Per triage's direction, objectui is not edited here — its mirror picks up the corrected text at its next spec bump. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…stored view rows by the spec's declared spellings and drops the reads nothing writes (objectui#11013) (objectstack-ai#11209) Fixes objectstack-ai#11013 Clause-②: yes ## Context - This is the objectui end of ruling 甲 on objectstack-ai/objectstack#20051 (record `5856781584`, batch objectstack-ai#227 item 2), stage ii: "…and objectui aligns its reads to the declared spellings". The spec end is objectstack-ai/objectstack#20474. It is installed here as `@objectstack/spec@17.5.0`, which exports `VIEW_CONSOLE_ROUND_TRIP_KEYS` from `@objectstack/spec/ui`. - Claim `5908989184` (seat `domain:ui#1`, session `session_0122Knsowci76D2rBWReCzzZ`). - Cross-seat note `5909173910` corrects `5908394824`. objectstack-ai/objectstack#20051 stage (iv) no longer waits on this card. It is dispatched there under the maintainer's instruction on that card: 「20051 不考虑现有的数据」. At objectstack's pin, the console's own save path already writes `object` and only the declared row-state keys. So the reads aligned here matter for rows written earlier. No consumer-side fallback for legacy rows was added beyond what the card names. ## What changed: one commit per work item 1. **Declared spellings for the bound object and the row identity** (`08a159633`) - `viewItemObjectName` (data-objectstack) reads `data.object ?? object`. The `objectName` leg is gone. - The object page no longer stamps `objectName: sv.objectName || sv.object || objectName` onto the rows `listViews()` returns. No reader consumed that stamp; the handlers act on the route's object name. A saved view's whole-body toolbar save wrote the stamp back into the stored row. - The Studio `view-ref` picker now reads `object` (`viewRefCatalog`). It used to read `objectName ?? object ?? object_name`. - The Studio view preview reads `object` (`resolveObjectName`). It used to read `objectName` on the body and on the draft. This site was not in the card's census. It is fixed in place as the same defect class and the same gate family, and no open PR touches the file. - `viewRowId` reads `name` only. A top-level `id` / `_id` is no identity. - `filter[].id` / `sort[].id` and `exportOptions` needed no change; see the measurements below. 2. **Drop the reads nothing writes; `userActions` for the toolbar policy** (`af24d4e7c`) - Both relays of a view into the `list-view` node stop reading nine keys off the view: `allowExport`, `wrapHeaders`, `clickIntoRecordDetails`, `addRecordViaForm`, `addDeleteRecordsInline`, `collapseAllByDefault`, `fieldTextColor`, `prefixField`, and the `editRecordsInline` spelling of `inlineEdit`. The two relays are plugin-view's `renderListView` composition and the object page's relay over it. - The object-view node's own values still reach `ListView`. That node read is the objectui#5097 host-composition read, and it is untouched. - A view's `allowExport: false` no longer withholds its `exportOptions`. - `NamedListView.allowExport` becomes a `?: never` tombstone. Ruling A on objectui#7924 kept it declared only because both relays read it. - Two sites now read the toolbar policy as `userActions.search` / `.sort` / `.filter`. They used to read the bare `showSearch` / `showSort` / `showFilters` flags off the view. The sites are the `object-view` node the object page builds and plugin-view's non-grid `generateViewSchema` route. - Census and pin updates: the relay census (`ObjectView.relayRungCensus-7559.test.ts`) declares each dropped rung's absence with its evidence kind; the objectui#7779 census re-points its `allowExport` pins; the objectui#10694 panel probe edits `resizable` instead of `wrapHeaders`. 3. **`listViews()` record flatten** (`67048225b`) - The flattened row now carries the record's `object`. It also carries the spec's round-trip keys declared on the `viewItem` member: `isDefault`, `isPinned`, `sortOrder`, `visibility`, `columnState`. The set is read off `VIEW_CONSOLE_ROUND_TRIP_KEYS`, not retyped. - app-shell's `VIEW_ROW_STATE_KEYS` for the view-config save is derived from the same record. - A reload round trip per key is pinned against a stub metadata store. Also in this PR: - The changeset (`bcd3c6f67`) states each dropped key's effect in user terms. - `c1d3b684a` adds dated notes to two pending objectui#7924 changeset entries. Both said "`allowExport` stays declared", which this change makes false; `pnpm check:changeset-claims` surfaced them. - `b2df69071` is a lint tidy with no behaviour change. ## PM mechanism assumptions, measured 1. **"Every `objectName` reader already falls back to `object`."** This is **partially falsified**. `viewItemObjectName` did read `object` before `objectName`. The other two sites read `objectName` FIRST, with `object` as the fallback: the object-page stamp (`sv.objectName || sv.object`) and the picker (`objectName ?? object ?? object_name`). A fourth reader, `ViewPreview`'s `resolveObjectName`, read it third and fifth. - Where rows carrying only `objectName` would come from: no door accepts one. `ViewMetadataSchema` refuses a flattened overlay bound by `objectName` alone, and also a ViewItem record bound by it alone. The same bodies bound by `object` parse; that is the lit control. The test is `viewItemObjectName.declaredSpelling-11013.test.ts`. - Every adapter writer stamps `object`: `createView`, `updateViewConfig` and `updateView`, all measured in the same file. - A row carrying both spellings answers by `object` at every site, as before. 2. **"No producer for the nine keys."** **Confirmed: zero producers.** - Stored rows: no console write path puts any of them on a view. The config save narrows `config` to the spec's `ListViewSchema` keys plus the declared row-state keys. The create path goes through the spec-driven inspector. The pin toggle and reorder write `isPinned` / `isDefault` / `sortOrder`. The settings overlay writes `rowHeight` / `sort` / `hiddenFields` / `columnState` / `inlineEdit`. - Census command, non-test sources of `packages/*/src`, `apps/**` and `examples/**`: `git grep` for the key as a property. The only hits are the readers, the type declarations, and two React props handed to `ListView` directly: `StudioDesignSurface`'s `addDeleteRecordsInline: true`, and `InterfaceListPage`'s list-view node `allowExport: false`. Neither is a view row or an authored view. - Lit control on the same search: `isPinned` finds the pin toggle's write. - Authored views in the sibling objectstack checkout (`examples/**`, `apps/**`): 0 files for each of the nine. The control `inlineEdit` finds 9 files. - The spec's strict `ListViewSchema` refuses all nine with `unrecognized_keys`; the control `exportOptions` is accepted. - Nothing is raised on objectstack-ai/objectstack#20051. 3. **"The round-trip keys come back only through the `loadViewOverrides` override merge."** This is **partially falsified**. - Confirmed: the flatten kept only `config` + `name` / `label` / `isDefault` / `_draft`. - The override merge restores `isPinned` / `visibility` / `columnState` onto the TAB. It does so for records that `MetadataProvider` lists into `objectDef.listViews`, which is where `loadViewOverrides` takes its ids. - The switcher's sort reads `sortOrder` off the `savedViews` row only, so `sortOrder` never came back through the merge. A reordered record lost its place on reload, wherever the per-browser `viewOrder:` cache did not mask it. - Pinned per key in `listViews.roundTripKeys-11013.test.ts`. 4. **"No reader keys state by `filter[].id` / `sort[].id` across a reload."** **Confirmed.** - The search ran over non-test sources of plugin-view, plugin-list, the components `custom/` builders and renderers, and app-shell views. It looked for `(filter|sort|condition|item|…).id` and for `localStorage` / `sessionStorage`. - The ids are React keys and in-session edit targets in `filter-builder.tsx` / `sort-builder.tsx`. `view-config-utils.ts` mints one when a row has none. - No persisted state is keyed by them. `exportOptions` needed no change on this end: the only reader outside `ListView`, `ObjectGrid`, reads `exportOptions.formats`. `ListView.tsx`'s bare-array tolerance is untouched. The card retires it after rows are stored parsed, and the file is in PR objectui#10278's area. ## Acceptance notes - **`NamedListView.allowExport` tombstone.** It follows from dropping the read. The alternative, keeping the type declared with no reader, is the declare-and-ignore shape ADR-0049 forbids. The seat's disposition on objectstack-ai/objectstack#20456 is also "no producer, no declared spelling". `NamedListView` no longer types `ObjectViewSchema.listViews` (objectui#7928), so the break is on an exported legacy type only. - **Toolbar reads go through `normalizeListViewSchema`.** This is the one fold the relays' own `userActions` rung already runs, so no new tolerance is added. A stored view that still carries a bare `show*` flag keeps its answer, which objectui#7924's changeset states as policy ("that fold stays"). Reading `userActions` raw would change those rows at these two sites only. - **`MetadataProvider.applyViewItem`** still flattens a record to `config` + identity. Its tab gets the round-trip keys back through the override merge. It was noted and not changed; the `listViews` comment says so. - **Not changed, noted only:** - `UnifiedViewConfig` (`types/src/designer.ts`) still declares `allowExport` / `addRecordViaForm` / `show*`. Nothing constructs that type. Carrier: none. - `ResourceEditPage`'s `sourceObjectName` falls back to `draft.objectName` for every metadata type. That is not a view-row read. - **Overlap with open PRs:** - PR objectui#11192 edits `data-objectstack/src/index.ts`, in its import block and filter translation, away from these hunks. - PR objectui#10278 edits `types/src/objectql.ts` at `ObjectKanbanSchema`, away from `NamedListView`. - **File surface beyond the claim's list**, each a consequence of the three items: - `types/src/objectql.ts` and its objectui#7779 census, for the tombstone - `ViewPreview.tsx`, the in-place fix above - the relay census and the objectui#10694 probe, plus comment refreshes in two plugin-view pins - two pending changesets, for the dated notes ## Verification All readings below are at head `b2df69071`. Exit codes were captured before any pipe. Heavy runs went through `os-verify-lock.sh`. **Build and type-check** - `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' run build`: exit 0. That closure holds 29 packages, including `types`, `data-objectstack` and `plugin-view`. The build ran at `c1d3b684a`; `b2df69071` changed no exported type. - `pnpm --filter PACKAGE run type-check`: exit 0 for all four packages. `types` and `plugin-view` were checked at `c1d3b684a` and are unchanged since. `data-objectstack` and `app-shell` were re-checked at `b2df69071`. - The new and edited tests are in each type-check population, counted with `--listFiles`: - app-shell `tsconfig.test.json`: 5 of 5 - types `tsconfig.test.json`: 1 of 1 (it holds the `allowExport` `@ts-expect-error` pin) - plugin-view `tsconfig.test.json`: 2 of 2 - data-objectstack `tsconfig.json`: 2 of 2 new tests, 74 test files in all **Tests** - `pnpm exec vitest run packages/data-objectstack/ packages/types/ packages/plugin-view/`: exit 0. `Test Files 421 passed (421)`, `Tests 8177 passed (8177)`. - `pnpm exec vitest run packages/app-shell/ --shard=N/4`, N = 1 to 4, each exit 0: - shard 1: 229 files passed, 2189 tests passed, 1 skipped - shard 2: 228 files passed and 1 skipped, 2074 tests passed and 8 skipped - shard 3: 228 files passed, 2743 tests passed - shard 4: 228 files passed, 2205 tests passed **Reverse verification** Each ablation was one-off: mutate, run the pin, restore. Every leg went through `ablation-replace.mjs` in WRAP mode, with the anchor required to hit exactly once and restore proven against HEAD's blob. After the run, `git diff HEAD` is empty. The predicted direction was red for every leg, and every leg went red: | leg | mutation | pin | result | | --- | --- | --- | --- | | A1 | `objectName` leg back in `viewItemObjectName` | `viewItemObjectName.declaredSpelling-11013` | 2 failed | | A2 | `id` / `_id` back in `viewRowId` | `ObjectView.setDefaultViewIdentity` | 3 failed | | A3 | `objectName ?? object ?? object_name` back in `viewRefCatalog` | `ResourceEditPage.pickerLoadFailure` | 1 failed | | A4 | `objectName` back in `resolveObjectName` | `ViewPreview.declaredObject-11013` | 2 failed | | A5 | `activeView?.wrapHeaders` back in plugin-view | `ObjectView.viewRowDeclaredReads-11013` | 2 failed | | A6 | `activeView?.showSearch` back on the object-view node | same | 1 failed | | A7 | the `allowExport` rung and gate back in app-shell | relay census, the same file, and the objectui#7779 census | 6 failed across 3 files | | A8 | the round-trip loop emptied in the flatten | `listViews.roundTripKeys-11013` | 5 failed | | A9 | `activeView?.showSearch` back on plugin-view's non-grid route | `ObjectView.viewRowDeclaredReads-11013` | 1 failed | A4's first attempt was a no-op. Its replacement contained the anchor, so the tool refused, the anchor count did not drop, and no test ran. It was redone with a non-overlapping replacement; the row above is the redone run. **Gates** Each exit 0: - `node scripts/check-changeset-presence.mjs`: "21 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)" - `pnpm check:control-bytes` - `check-changeset-no-major`, `check-changeset-fixed` - `check-changeset-overwrite`. It is report-only. It names the two dated-note edits, and their front matter is unchanged. - `pnpm check:new-line-citations`: "0 new citation(s)" - `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape` - `check:spec-symbols`, `check:metadata-write-doors`, `check:test-path-roots`, `check:pending-changeset-literals` - `check:icon-record-names`, `check:docs-route-closure` - `check:changeset-claims`. It is report-only; its two findings got the dated notes. **Lint, narrowed and proven** - Population: `eslint.config.js` lints `**/*.{ts,tsx}`. All 21 changed or added `.ts` / `.tsx` files are in it. - Count: `pnpm exec eslint --format json` over those 21 files reports 21 files and 0 errors, with inline config honoured as `pnpm lint` honours it. - Warnings per file against the merge base are equal, except two. `ResourceEditPage.tsx` gains one `react-refresh/only-export-components` hint, for the exported `viewRefCatalog`. `ViewPreview.tsx` loses one. - Invariance: the config sets no `parserOptions.project`, so linting is not type-aware. It loads no import plugin, and no rule in `eslint-rules/` reads another file. So this diff cannot move an untouched file's verdict. - The full `pnpm lint` is CI's run. **NOT MEASURED** - `check:doc-examples`. Reason: PREREQUISITE NOT MET. It needs the `app-shell`, `cli`, `plugin-ai`, `plugin-gantt`, `plugin-map`, `plugin-markdown` and `plugin-timeline` dists, which are outside the built closure. No doc example writes `allowExport` on a `NamedListView` or names another changed symbol. It is declared to CI. **Mergeability** - Main has moved to `846cec0ef`. Of this branch's files, only `data-objectstack/src/index.ts` changed there, by PR objectui#11192. - `git merge-tree` of HEAD against that main exits 0. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Warren Buffett <warren@objectstack.ai>
Fixes #20456
Clause-②: yes (narrowing)
Stage (ii) of ruling 甲 on #20051 (
5856781584), the spec end of thisSeam:card: every key objectui's console writes onto a storedviewrow 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:saveMetaItemstill 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 withBlocked-by:this PR; its items are listed at the end.Census: what the console writes onto a stored
viewrow and reads backMeasured against objectui at the
.objectui-shapindd3f7e1be3561d63267d7162f3fc0ac52e72834d, fetched into an isolated clone. Spec side read on this branch, merged withorigin/maine956924e.Method, two legs.
data-objectstack/src/index.ts(listViews,listViewOverrides,getView,updateView,updateViewConfig,createView,mergeViewPatch,narrowPersonalizationOverlay,isPersonalizationOverlayRow,viewItemObjectName,unwrapViewDraftand 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/consoleFormPage.tsxandPublicFormsPage.tsx,ResourceEditPage.tsx, and the export-options readers. It collects every non-call property read, string-keyed element access, destructured key,intest and key-list literal: 154 distinct keys. The spec's own view vocabulary (280 keys, walked off the Zod defs of the fourViewMetadataSchemamembers) was subtracted, leaving 105, each classified by hand (most are props, locals and client objects).ViewMetadataSchemaand 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 keyzzLitControlKeyis found at its 1 site. DARK: a fabricated keypinnedAtEpochis 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.
viewItemis a ViewItem record row ({ name, object, viewKind, config });listOverlayis a flattened list row.isPinnedObjectView.tsx:2120viaupdateViewmergeindex.ts:2993; config save carries itObjectView.tsx:1102,:1161ObjectView.tsx:910,:3398;ViewTabBar.tsx:330(pinned group)viewItem, dropped onlistOverlaysortOrderObjectView.tsx:966(reorderViewPatches); config save:1102ObjectView.tsx:913,:1925-:1934(tab order)viewItem, dropped onlistOverlayorderstays the authored defaultvisibilityObjectView.tsx:1102; a saved view's toolbar save writes the whole tab:1085ObjectView.tsx:912,:3399;ViewTabBar.tsx:336(group order),:385(private divider),:442(lock icon)private/team/organization/public), not access control_isOverrideupdateViewConfigindex.ts:5340, on the row it writes for a toolbar change to a code-defined viewisPersonalizationOverlayRowindex.ts:2778, used bylistViews:5448andnarrowPersonalizationOverlay:2895listOverlayisDefaultObjectView.tsx:946(setDefaultViewPatches)ObjectView.tsx:911,:3397;MetadataProvider.tsx:420;index.ts:5470columnStateObjectView.tsx:2803,:3112; config save:1102ObjectView.tsx:2786;index.ts:2832(overlay-owned keys)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)
objectName(alsoobject_name)ObjectView.tsx:1802; written back by a saved view's toolbar save:1085; readindex.ts:2698,ResourceEditPage.tsx:933object(declared, required on both overlays)object; a second spelling of one field is what this contract refusesid/_idviewRowIdviewIdentity.ts:95, afternamenamenameon every row, soidis never consulted for a stored rowfilter[].id/sort[].idview-config-utils.ts:145,:159,:319VIEW_CONSOLE_ROW_DECORATIONS, removed before the parse bystripViewConsoleDecorationsitem.id || crypto.randomUUID()(:159,:319): a row without one gets a fresh id, so a parsed row loses nothing the console showsexportOptionsas a bare arrayObjectView.tsx:2839{ formats, … }, which the parse already lifts the array toexportOptions.formats(ObjectGrid.tsx:3888); onlyListView.tsx:1783folds the array itselfFound, and not stored-row round-trip keys
_draft(index.ts:5455): a read decoration. The read path stamps it,saveMetaItemstrips it before anything else (stripReadDecorations), and it is never stored.showSearch/showFilters/showSort(ObjectView.tsx:907-:909,:3139-:3141),allowExport(:2838-:2839),created_at(:1936, the saved-view sort tie-break),updatedAt/updated_at,viewTypeand anitem.specenvelope (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_metadataNOT MEASURED. This container holds no connection to any deployed environment: no
OS_DATABASE_URL,TURSO_*orPG*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 carryingobjectName, a top-levelid, a legacyexportOptionsarray, any of the read-only keys listed just above, and anyvisibilityoutside the four groups or_isOverrideother thantrue(now refused on re-save).What changes in
packages/specviewSwitcherRowStateFields()declaresisPinned,sortOrderandvisibilityonce, 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: trueon the flattened list overlay. The overlay's existingisDefaultgains its meaning.VIEW_CONSOLE_ROUND_TRIP_KEYSis exported (api-surface / export-origins regenerated with the tools).ViewItemSchema) namesvisibilityin 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 withorigin/maine956924e) unless noted. The only later commit,a47aeb5d, rewrites one code comment inview.zod.ts. The derived gate union runs at that head, and its result is in the report. Headfc5a47d0then mergesorigin/main75b21692throughscripts/pm/os-regen-merge.sh(api-surface and export-origins regenerated on the merged tree;check:generatedgreen) 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.@objectstack/objectql^...built first, exit 0):metadata-protocolprotocol.graft-folded-form-sections.test.ts(holds "GUARD: Studio-only round-trip keys still survive the save") andprotocol.graft-normalized-operators.test.ts(holds "keeps Studio-only auxiliary fields aparsed.dataswap would strip"): 2 files, 42 passed.objectqlprotocol-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 theirgen:commands.check:authorable-surfacewas green: no authorable key moved.d19cbad8, viascripts/ablation-replace.mjs): thevisibilitydeclaration renamed away (anchor hit 1 time, blobd2aacf7abecame3dd7dd8a). The closure pin went red, 6 of 34: the declared-member and parse-keeps pairs forviewItemandlistOverlay, and both typed-refusal cases. The restore is proven: blob back tod2aacf7aequal to HEAD,git diff HEADempty. The test reads the spec source directly (a relative import), so nodist/leg applies.dist/ui/index.d.tsfrom a scratch consumer:visibility: 'everyone'on aViewItemWirefails with TS2322 (exit 2); without that line,visibility: 'team'andVIEW_CONSOLE_ROUND_TRIP_KEYS._isOverridetypecheck (exit 0).Acceptance notes
viewsave carrying a non-booleanisPinned, a non-integersortOrder, avisibilityoutside the four groups, or an_isOverrideother thantrueis refused (422) where it used to be stripped and stored. The console writes none of those. Following the seat's answer on spec(ui)+objectui: declare the console's round-trip keys on the stored view overlay (#20051 stage ii, ruling 甲) #20456 (comment5874463510), the changeset declares this: theyes (narrowing)arm, a BREAKING line naming the refused class with its remedy (correct the value or delete the key), and the ADR-0087 dispositionnot-required (no-migration-prescription). The level staysminor.visibilityis a naming trap.privategets 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.isPinned/sortOrder/columnState(VIEW_ITEM_SURFACE,ListViewShapeSchemaguidance) still say "per-user". Noted, not changed here: they are refusal prose, not the wire.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 addstype: 'grid'to a column-less list patch, and form sections gaincollapsible/collapsed/columnsdefaults: stored parsed, those defaults land in rows. (c)_isOverridemust 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:
object, and stop stamping and writing backobjectName(ObjectView.tsx:1802,index.ts:2698,ResourceEditPage.tsx:933);idinto a saved view's row (buildPersistedViewBody,ObjectView.tsx:1085);listViews' flatten of a ViewItem record (index.ts:5466-:5472) carries onlyname/label/isDefault/_draft. The record's declared row state (isPinned,sortOrder,visibility,columnState) is written there but not surfaced by that reader;exportOptionsfold (ListView.tsx:1783) once rows are stored parsed;showSearch/showFilters/showSort→userActions.*, andallowExport, which has no declared spelling.Generated by Claude Code