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>
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