Skip to content

feat(spec): declare the console's round-trip keys on the stored view wire (#20456) - #20474

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20456-stored-overlay-round-trip-keys
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20456-stored-overlay-round-trip-keys

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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 spec(ui)+objectui: declare the console's round-trip keys on the stored view overlay (#20051 stage ii, ruling 甲) #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

…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>
…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>
…rewritten comment

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/permissions/authorization.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/permissions/permission-metadata.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/permissions/permission-sets.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/permissions/positions.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/permissions/profiles.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/protocol/objectui/layout-dsl.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/ui/apps.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/ui/dashboards.mdx (via sortOrder (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/ui/pages.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/releases/v13.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/releases/v15.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))
  • content/docs/releases/v17/17-0.mdx (via isDefault (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS), sortOrder (symbol, a field of const object VIEW_CONSOLE_ROUND_TRIP_KEYS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8e028591857980ae69b9f9badb380dfa61367e62 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 126d18056f072483519e730039e9b3632cb102f6 — the merge of head fc5a47d08d0c842125d9ccc97c91b7e858647d4b into base 8e028591857980ae69b9f9badb380dfa61367e62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8e028591857980ae69b9f9badb380dfa61367e62 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fc5a47d08d0c842125d9ccc97c91b7e858647d4b
Local-runs: none

Inputs read: card #20456 (body + all 4 comments: claim 5872888895, dev reports 5874427709 / 5874664336, seat answers 5874463510), #20051 (body + all 13 comments, ruling 甲 5856781584 confirmed at 5856866699, seat 2 analysis 5853397704), PR #20474 (body, 9-file list, net diff vs base 75b21692), the head's check-runs, git show / git grep / git diff / git merge-tree against refs in objectstack, and objectui at the pin dd3f7e1b through the sibling checkout (git show at that sha).

① Derived judgments

Accept-set changes (each named).

  • ViewItemWireSchema (both arms) gains visibility: enum(private|team|organization|public).optional() — widens (the key is now kept by the parse) and narrows (any other value, formerly stripped, is refused). Right. The four groups are the console's own VISIBILITY_ORDER (ViewTabBar.tsx:336); the console reads it off stored rows (ObjectView.tsx:912, :3399; ViewTabBar.tsx:385, :442) and carries a stored value forward on a config save (VIEW_ROW_STATE_KEYS, :1102, :1161); no console control sets it.
  • VIEW_METADATA_MEMBERS.listOverlay gains isPinned: boolean, sortOrder: int, visibility (same enum) and _isOverride: literal(true) — widens (all four kept) and narrows (non-boolean / non-integer / off-enum / not-true refused at the key). Right. Writers at the pin: pin toggle { isPinned: pinned } boolean (ObjectView.tsx:2120), reorderViewPatches sortOrder: idx integer (:973), updateViewConfig stamps [VIEW_OVERLAY_MARKER]: true (index.ts:5340), and the reader tests === true (isPersonalizationOverlayRow, :2778) — so literal(true) is the honest type and the census's "zero console writes affected" holds.
  • flattenedViewOverlayFields isDefault gains a .describe() on both overlays (form included); ViewColumnStateSchema / viewItemWireFields().columnState describes rewritten; isPinned / sortOrder on the ViewItem wire keep their types with new describes. Text only, no accept-set change — Spec property liveness is success on the head. Right.
  • VIEW_ITEM_SURFACE.guidance.visibility — a named refusal on the authoring door; defineViewItem still refuses all four console keys by unrecognized_keys (closure pin). Right, and its "restricts nobody" is true at the pin (ViewTabBar.tsx:442 draws a lock icon and nothing else).
  • The form overlay is left off viewSwitcherRowStateFields(). Right: the switcher lists list-family views only and no console write puts the three keys on a form row; listViews never surfaces them from a ViewItem record either (index.ts:5466-5472 flattens to config + name/label/isDefault/_draft, the dev's own out-of-scope note).

Public surface. +1 export VIEW_CONSOLE_ROUND_TRIP_KEYS (api-surface/ui.json, export-origins/ui.json each +1; export * from './view.zod' in ui/index.ts). Its map — isDefault / isPinned / sortOrder / visibility / columnState → viewItem + listOverlay, _isOverride → listOverlay — matches what the head declares (viewItem: isDefault from viewItemBaseShape, the three from viewSwitcherRowStateFields, columnState from viewItemWireFields; listOverlay: isDefault / columnState from flattenedViewOverlayFields('list'), the rest from listOverlayRoundTripFields). Right, and it is the citeable symbol stage (iii) needs. The generated view.mdx (ViewItemWire list + form arms gain the visibility row, describes updated) was regenerated on the merge of 75b21692 — the diff against base is +1 per JSON, #20420's component-props rows kept; the list-overlay member is not documented in view.mdx, so no rows are owed there. Lint & Repo Gates (generated / api-surface) is success.

(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 isPinned ×11 and an injected zzLitControlKey ×1, dark pinnedAtEpoch ×0 — a reading, not a dead scan. Every cited site resolves at dd3f7e1b (checked: ObjectView.tsx 900-916 / 1085 / 1102 / 1161 / 1802 / 1925-1936 / 2120 / 2786 / 2803 / 2838-2839 / 3112 / 3139-3141 / 3397-3399; index.ts 2698 / 2778 / 2832 / 2895 / 2993 / 5340 / 5448 / 5455 / 5466-5472; view-config-utils.ts 145 / 159 / 319; ViewTabBar.tsx 330 / 336 / 385 / 442; viewIdentity.ts:95; ResourceEditPage.tsx:933; ObjectGrid.tsx:3888; ListView.tsx:1783 — in plugin-list, not plugin-view). The result — the base's parse dropped isPinned / sortOrder on the list overlay, visibility on both members, _isOverride on the list overlay — is supported by the base's schemas (viewItemWireFields had only isPinned / sortOrder / columnState; the list overlay at 5982-5994 composed none of the four; no visibility or _isOverride declaration existed) and by the base's own put.* pins in view-union-diagnostics.test.ts, which asserted the drop. Supported. One gap, in the read-only class only (see ③): eight objectui renderer flags the console reads off the active view and never writes are missing from the PR body's "read, never written by the console" list.

(b) The six declarations. Each typed right (above), each with a true .describe() meaning (pinned in the switcher / 0-based saved-view position with order the authored default / display grouping not access control / settings-overlay marker true only / default view in the switcher / column order+widths), and view-console-round-trip-keys.test.ts closes them: record equals CENSUS; each (key, member) pair declared + described and kept by both the member parse and the ViewMetadataSchema union; typed refusal at the key for all four narrowed keys; objectName / id pinned absent on every member with object / name present; authoring refusal by name. The three put.* output pins now keeping isPinned / sortOrder are the direct consequence; the reconciliation-ledger quotes and the view-metadata-schema.test.ts comment match the head's describes. Right.

(c) Persistence untouched. git diff 75b21692..fc5a47d0 -- packages/metadata-protocol packages/objectql is empty; the three GUARD pins are present unedited at the head (protocol.graft-folded-form-sections.test.ts:222, protocol.graft-normalized-operators.test.ts:126, protocol-meta.test.ts:586). Their green comes from the head's check-runs: all six Test Core shards success (1/6 17:11:10Z, 2/6 17:06:56Z, 3/6 17:12:02Z, 4/6 17:08:59Z, 5/6 17:15:17Z, 6/6 17:12:18Z) and the Test Core rollup success (17:15:36Z). The head's full check-run set at 17:15:39Z: 49 runs, 43 success, 6 skipped (Auto Label ×2, Check PR Size ×2, Console Pin Gate, Packed-tarball smoke opt-in), 0 failed, 0 un-concluded — Build Core, Build Docs, Check Changeset ×3, Lint & Repo Gates, Spec property liveness, Type Check ×5, Dogfood ×5, Temporal Conformance, Governed Surface Queue Guard and the claim/branch guards all success. Right.

Other edits. The "per-user" drop on the columnState describes is supported by ADR-0017's 2026-09-04 amendment (ADR-0131 D13: runtime-authored views are environment metadata; per-user view scoping is parked). The guidance texts still saying "per-user" (view.zod.ts:2483-2488, :5081-5084) are left and noted, as the PR says. The changeset's 422 INVALID_METADATA is the save door's real envelope (protocol.ts:2697). No prose claim in the PR body is false at the head; one list is incomplete (③).

Pin. .objectui-sha is dd3f7e1be356…2834d on main (8e028591), at the PR base 75b21692, at both fork points (fbeb56e4, e956924e) and at the head — the census was read at the pin main carries.

Merge. git merge-tree --write-tree origin/main fc5a47d0 (origin/main 8e028591): clean, tree 7eb7844d, exit 0.

② Semver level

.changeset/20456-view-console-round-trip-keys.md: '@objectstack/spec': minor, line-leading Clause-②: yes (narrowing), a **BREAKING** paragraph naming the refused class (non-boolean isPinned, non-integer sortOrder, off-enum visibility, _isOverride other than true) with the remedy, and the ADR-0087 marker not-required (no-migration-prescription) in the gate's comment form. Matches the diff: it widens (four newly kept keys, one new export → yes, at least minor) and narrows (ill-typed values the strip swallowed are refused → the narrowing arm, "a diff that widens one surface and narrows another", scripts/pm/clause2-line.mjs), shipped minor under the launch-window convention (check-changeset-no-major refuses major; precedent .changeset/17779). The ADR-0087 category is right: nothing authored moves, the only producer writes none of the refused values, and the remedy is not a FROM→TO rewrite — Check Changeset (runs check-empty-changeset, check-adr-0087-registration --base, check-changeset-no-major) is success ×3 on the head. The ruling's letter for stage (ii) (minor, yes) is unchanged; the arm is the seat's classification (5874463510 Q1 = B), which is a non-escalation call.
Clause-②: yes (narrowing) — line-leading in the changeset and on line 3 of the PR body.

③ Boundary flags

  • Q1 (Clause-② arm) → B, applied at the head. Right, per ②.
  • Q2 (census dispositions) → A, accepted. Judged right for stage (iv)'s purpose: objectName → object (every reader falls back to object: index.ts:2698, ObjectView.tsx:1802, ResourceEditPage.tsx:933, and object is already required at the door, so a row that would lose it today fails today); top-level id → name (viewRowId reads name first, :95; buildPersistedViewBody writes id = viewRowId(sv)); filter[].id / sort[].id stay VIEW_CONSOLE_ROW_DECORATIONS — their only consumers at the pin are the builders' item.id || crypto.randomUUID() (:159, :319); user-filter selections key by field through URL params (:2364), nothing keys by a row id across a reload, so a re-minted id loses nothing; the bare-array exportOptions lifts to { formats } at parse (view.zod.ts:2767, a .transform), which ObjectGrid.tsx:3888 reads and ListView.tsx:1783 merely tolerates. No user state lost at stage (iv).
  • Q3 (allowExport) → A. Right; it goes on the objectui card.
  • Dev deviations 1-3 answered by Q1-Q3 and ①; 4-5 (gate rounds) superseded by the head's check-runs; 6 (main moved) closed by the merge of 75b21692 at 82078f50 and the clean merge-tree above.
  • Deviation 7 — production sys_metadata NOT MEASURED (no deployed-environment connection in the container; reason given). Allowed by the card's acceptance; stage (iv) cannot land without the count. Escalated to the seat: a seat with production access takes it before stage (iv), counting what the PR names — plus the eight keys below.
  • Escalation — census read-only class under-counted (no declaration owed). At dd3f7e1b the console reads wrapHeaders, clickIntoRecordDetails, addRecordViaForm, addDeleteRecordsInline, collapseAllByDefault, fieldTextColor, prefixField (app-shell/ObjectView.tsx:2864-2870, plugin-view/ObjectView.tsx:2547-2553) and editRecordsInline (:2805) off the active view (activeView = { ...baseView, ...viewDraft }, :1972, built from stored rows), none is declared anywhere in packages/spec/src at the head, and no console surface writes one onto a stored row (the one literal, StudioDesignSurface.tsx:2709, is a render-time prop; objectui's own parity test files them as "renderer flags with no spec equivalent (yet)"). They are the allowExport class exactly: not round-trip keys, so VIEW_CONSOLE_ROUND_TRIP_KEYS is right without them, and this PR owes them nothing — but the PR body's "Read, never written by the console" list omits them, so stage (iv)'s prescribed production count and the objectui card's "decide the read-only keys" item are both eight keys short. The seat carries the eight onto the objectui card (Q3's disposition, read by read) and onto stage (iv)'s count list. Not a FAIL: the ruled deliverable — every key the console writes and reads back, declared with its meaning — is met and closed by the pin.
  • Out-of-scope findings: listViews record flatten (verified at index.ts:5466-5472) → objectui card; "per-user" guidance texts → 承接者:无, noted; visibility: 'private' lock icon → declared honestly. None blocks.

Implemented-by: claude/issue-20456-stored-overlay-round-trip-keys
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 17:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit e967cbd Sep 28, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20456-stored-overlay-round-trip-keys branch September 28, 2026 17:42
os-tesla pushed a commit that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(ui)+objectui: declare the console's round-trip keys on the stored view overlay (#20051 stage ii, ruling 甲)

2 participants