Skip to content

Commit 085ca6b

Browse files
docs(adr): ADR-0005 appendix (c) — the persisted document is the parsed body once every round-trip key is declared (#20777)
Fixes #20457 Part of #20051 (stage (iii) of ruling 甲 there; stage (iv), the storage switch, stays on that card). Clause-②: no ## What this changes One file, `docs/adr/0005-metadata-customization-overlay.md`: - **Appendix (c)**, the persisted-document bullet of "Addendum — 2026-05-16 (c)", now states the ruled end state: the persisted document is the parsed body (`parsed.data`) once every round-trip key is declared. The bullet itself says the rule is ruled but not yet in force. - **A dated note directly under that bullet** ("Amended (2026-09-30)"), written the way ADR-0053's 2026-09-30 in-place amendment is written: 1. the provenance: ruling 甲 on #20051, comment 5856781584, maintainer 「开始总监决裁」; 2. the 2026-05-16 bullet, quoted verbatim and marked superseded; 3. what "every round-trip key is declared" means for `view`: the record `VIEW_CONSOLE_ROUND_TRIP_KEYS` and its closure test; 4. the stage (ii) symbol that declares each key, on each member: `ViewItemWireSchema` and `VIEW_METADATA_MEMBERS.listOverlay`, via `viewSwitcherRowStateFields`, `viewItemWireFields`, `listOverlayRoundTripFields`, `viewItemBaseShape` and `flattenedViewOverlayFields`; 5. where the census's other finds map instead (`objectName` to `object`, and the others); 6. the interim state; 7. what stage (iv) does. - **Status line**: one new `Amended (2026-09-30, …)` entry. Prime Directive #13 asks for an amended status line when a recorded decision is reversed, and every earlier decision-changing amendment of this ADR is on that line. This is the one line outside the bullet-plus-note surface the claim names. Same file. ⛔ No persistence code, no GUARD pins, nothing under `packages/`. No `scripts/adr-anchors/` file either: no gate asked for one (all green below). The ADR's new code citations are symbol anchors that `check-adr-symbol-anchors` resolves. ## Declared is not yet enforced: how the text handles that On `main` the runtime still stores `request.item`. The verbatim-save pins are still there (`protocol-meta.test.ts` "preserves Studio-only auxiliary fields verbatim (not stripped)", `protocol.graft-folded-form-sections.test.ts` "GUARD: Studio-only round-trip keys still survive the save"). So the ADR states the decision and the status next to each other: - the amended bullet ends with **"Ruled, not yet in force"**; - note item 5 says that today a key no member declares is still stored, so "declared" does not yet mean "the only keys stored"; - note item 6 lists what stage (iv) does: store the parsed body, flip the GUARD pins, judge `ViewItemWireSchema`'s top-level `options`, and first measure the production count of stored rows that carry undeclared top-level keys. This follows the PM's working assumption. It also matches the repo convention: ADR-0053's 2026-09-30 amendment carries an "Interim state" item in the same way. ## Evidence (head `c8ee5833`) - Gates were derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, with no paths: the same 19 as the dispatch. Reconciled with `--ran`: "19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)". - All 19 exit 0 on `c8ee5833`: `check-adr-links` (+ `--self-test`), `check-adr-symbol-anchors` (+ `--self-test`), `check-ci-filter-parity`, `check-closing-keyword-parity` (+ `--self-test`), `check-comment-mask-corpus`, `check:doc-formula-expressions`, `check:adr-anchors`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:driver-memory-census`, `check:gitlink-declared`, `check:nul-bytes`, `check:pm-governed-merges`, `check:pm-prior-rulings`, `check:refd-timer-probe`, `check:watch-hint-literal`. - `check-adr-symbol-anchors` printed: "2156 anchors across 140 records resolve — 320 symbol (294 declaration, 26 literal), 1809 file-level, 27 cross-repo, 6 exempt, 9 continuation". - `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` were unbuilt in the fresh worktree). That run measured nothing. I built both through the verify lock (a turbo cache hit, 4 of 4) and reran it: exit 0. - **Ablation** (a one-off; no permanent test file). With the change committed, I used `scripts/ablation-replace.mjs` to break one new anchor, renaming `#listOverlayRoundTripFields` to `#listOverlayRoundTripFieldz` (anchor count 1 to 0, blob `a893abb2` to `50270267`). - The gate then printed "❌ check-adr-symbol-anchors: 1 finding(s)" and "[unresolved-symbol] … `#listOverlayRoundTripFieldz`" at the note's line. - I restored the file with `git checkout HEAD --`. Its blob was back to `a893abb2` and `git diff HEAD` was empty. - So the gate reads the new anchors; it does not pass over them. - No changeset: `docs/adr/**` publishes nothing (`skip-changeset`). ## Acceptance notes These are observations, not filed. Their carrier is stage (iv) on #20051. 1. **The verbatim-save pin population is wider than the two the ruling read.** Three tests assert the verbatim save by name: - `packages/objectql/src/protocol-meta.test.ts`: "preserves Studio-only auxiliary fields verbatim (not stripped)"; - `packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts`: "GUARD: Studio-only round-trip keys still survive the save"; - `packages/metadata-protocol/src/protocol.graft-normalized-operators.test.ts`: "keeps Studio-only auxiliary fields a `parsed.data` swap would strip". At least one more depends on the verbatim save: `protocol.runtime-authoring-gate.test.ts`, "the door persists the authored body verbatim". Two of the pins need re-judging, not just flipping. The graft-folded GUARD writes `isPinned` / `isDefault` / `sortOrder` onto a **form** overlay, and `VIEW_CONSOLE_ROUND_TRIP_KEYS` records that the console never writes them there. The protocol-meta pin writes `objectName`, which the census maps to `object`. 2. **Per-type scope of the switch.** `resolveOverlaySchema` validates every type with a registered schema, and each one is stored as sent today. The ruling's end state names "a saved view", and the round-trip census covered `view` only. The amended bullet states the rule with its condition ("once every round-trip key is declared") and claims a satisfied condition for `view` only. Whether the stage (iv) switch covers `dashboard` and the other validated types is stage (iv)'s question. 3. **Code comments that cite this decision.** The `resolveOverlaySchema` docblock and the `saveMetaItem` comment in `packages/metadata-protocol/src/protocol.ts`, and the pins, cite "ADR-0005 §Validation", a heading this ADR does not have. They describe today's behaviour, which stays true until stage (iv). No `scripts/adr-anchors/` entry ties `protocol.ts` to ADR-0005. Stage (iv) implements the decision in code, so that is where the anchor belongs (Prime Directive #13). ## 维护者速读(草稿) **改了什么**:只改了 ADR-0005 这一份文档。 - 附录 (c) 原来写的是:保存视图时原样存请求体,不存解析结果。现在改成裁决甲的终态:所有要往返的键都在契约里声明之后,存的是解析后的结果。 - 这一条下面紧跟一段 2026-09-30 的修订注记,写了四件事:出处(#20051 的裁决甲);原文一字不改地引用,标为"已被取代";逐键列出声明这些键的 spec 符号;现状说明。 - 状态行加了一条"已修订"。 **为什么改**:这是裁决甲的第 (iii) 步。第 (ii) 步已经把控制台会写回、再读回的键都声明进了 spec:置顶、排序、可见性分组、默认视图、列状态,以及设置覆盖标记。所以附录 (c) 现在可以照实写。 **风险与代价(含回滚)**: - 纯文档改动。不动运行时代码,不动测试,不发布任何包。 - 唯一的风险是读者把新文字当成"运行时已经这样做了"。所以条目本身写明"已裁定、尚未生效";注记也写明:目前仍然原样存请求体,存储切换是第 (iv) 步。 - 回滚:revert 这一个 commit 即可。 **席位意见**: **你要做的**:审阅后在本 PR 上给 APPROVED。这是 Tier H 治理面 `docs/adr/**`,由席位落地。存储切换本身(第 iv 步)仍挂在 #20051 上,不在本 PR 里。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 274e162 commit 085ca6b

1 file changed

Lines changed: 71 additions & 7 deletions

File tree

‎docs/adr/0005-metadata-customization-overlay.md‎

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> **v5.0 update (2026):** Throughout this document, the term *project* has been renamed to *environment* (no aliases; CLI flags, URL paths, schemas, env vars all hard-renamed). See [ADR-0006 v4 — the v5.0 rename and its no-alias decision](./0006-project-environment-split.v4.md#the-v50-rename-and-its-no-alias-decision) for the rationale. The body below is preserved verbatim for historical context.
44
55

6-
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged)
6+
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged) · **Amended** (2026-09-30, #20051 ruling 甲 — in "Addendum — 2026-05-16 (c)", which code cites as appendix (c), the persisted document is the **parsed body** once every round-trip key is declared, reversing the verbatim `request.item` save; a stored `view` row's round-trip keys are declared, and the storage switch itself is the ruling's last stage, not yet landed — see the amendment note under that addendum's persisted-document bullet)
77
**Deciders**: ObjectStack Protocol Architects
88
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (Package as first-class citizen), [ADR-0004](./0004-cloud-multi-kernel.md) (Cloud + per-project kernels)
99
**Amended by**: [ADR-0006 v4](./0006-project-environment-split.v4.md) (drops `sys_project` entirely), [ADR-0008](./0008-metadata-repository-and-change-log.md) (re-expresses overlay as `LayeredRepository`; subsequently drops `project`/`branch` from `MetaRef`), [ADR-0029 D9](./0029-kernel-object-ownership-and-platform-objects-decomposition.md#amendment-2026-08-09-6853-a-tenant-overlay-of-an-object-is-its-own-contributor-layer-not-a-second-own) (for `object` only: the overlay is a registry contributor LAYER over the packaged owner, resolved as `base = overlay ?? own`, instead of a destructive in-place overwrite)
@@ -334,12 +334,76 @@ Implementation (`packages/metadata-protocol/src/protocol.ts`):
334334
carrying `path/message/code` for each Zod issue. REST layer
335335
(`packages/rest/src/rest-server.ts`) already propagates `status`
336336
and `code` to the response.
337-
- The persisted document is the **original** `request.item`, NOT
338-
`parsed.data`. Studio attaches auxiliary fields (`isPinned`,
339-
`isDefault`, `sortOrder`, `objectName`, …) that aren't in the canonical
340-
schema; storing `parsed.data` would silently strip them on every save.
341-
This trades strict-stripping against forward compatibility for Studio
342-
extensions — the canonical fields are still type-checked.
337+
- The persisted document is the **parsed body** (`parsed.data`) once every
338+
round-trip key is declared. A key a client writes onto a stored row and
339+
reads back after a reload is declared on the type's schema, with its
340+
meaning, so the parse keeps it; a key nothing declares is not stored. The
341+
canonical fields are type-checked, and the stored row is what the contract
342+
says it is. ⚠️ **Ruled, not yet in force:** the storage switch is the
343+
ruling's last stage and has not landed, so today `saveMetaItem` still
344+
stores the request body (`request.item`). The note below records what is
345+
declared and what remains.
346+
347+
> **Amended (2026-09-30) — the persisted document is the parsed body once
348+
> every round-trip key is declared, and a stored `view` row's round-trip
349+
> keys now are.** Provenance: ruling 甲 on #20051 — maintainer
350+
> 「开始总监决裁」, 2026-09-27, director batch #227 item 2, recorded on the
351+
> card as comment 5856781584 — which keeps the persistence half of ruling A
352+
> (comment 5824043998, item 2) as the end state: 「a saved view is the
353+
> parsed body, every key the console reads back is declared on the
354+
> contract, and ADR-0005 appendix (c) is amended to say so」. Code cites
355+
> this addendum as "ADR-0005 appendix (c)". Why: stored as sent, the Studio
356+
> keys lived in the store and nowhere in the contract, and nothing failed
357+
> while the parse dropped them unread. State the console needs after a
358+
> reload is declared, not kept alive by a store-the-raw-request exception.
359+
>
360+
> 1. **Superseded.** The bullet above read, as written on 2026-05-16:
361+
> "The persisted document is the **original** `request.item`, NOT
362+
> `parsed.data`. Studio attaches auxiliary fields (`isPinned`,
363+
> `isDefault`, `sortOrder`, `objectName`, …) that aren't in the
364+
> canonical schema; storing `parsed.data` would silently strip them on
365+
> every save. This trades strict-stripping against forward compatibility
366+
> for Studio extensions — the canonical fields are still type-checked."
367+
> 2. **What "every round-trip key is declared" means for `view`** (stage
368+
> (ii) of the ruling, #20456, PR #20474). It means one record:
369+
> `packages/spec/src/ui/view.zod.ts#VIEW_CONSOLE_ROUND_TRIP_KEYS`, each key the console writes onto a stored `view` row and reads back, mapped to the `#VIEW_METADATA_MEMBERS` branches it is declared on.
370+
> `packages/spec/src/ui/view-console-round-trip-keys.test.ts` holds the
371+
> record equal to the census of objectui's console at the
372+
> `.objectui-sha` pin, and holds each (key, member) pair declared,
373+
> described and kept by a parse. A new round-trip key joins the record,
374+
> declared on its members, in the change that adds the console write.
375+
> 3. **The declarations, key by key** — a reading at `origin/main`
376+
> `c9c182ed`; the record, not this list, is the authority. Two members
377+
> carry them: `viewItem`, published as `packages/spec/src/ui/view.zod.ts#ViewItemWireSchema`, and `listOverlay`, published as `packages/spec/src/ui/view.zod.ts#VIEW_METADATA_MEMBERS.listOverlay`.
378+
> - `isPinned`, `sortOrder`, `visibility`: one declaration, `packages/spec/src/ui/view.zod.ts#viewSwitcherRowStateFields`, spread into the two members by `#viewItemWireFields` and `#listOverlayRoundTripFields`.
379+
> - `isDefault`: `packages/spec/src/ui/view.zod.ts#viewItemBaseShape` on `viewItem`, `#flattenedViewOverlayFields` on `listOverlay`.
380+
> - `columnState`: `packages/spec/src/ui/view.zod.ts#viewItemWireFields` on `viewItem`, `#flattenedViewOverlayFields` on `listOverlay`.
381+
> - `_isOverride`: `packages/spec/src/ui/view.zod.ts#listOverlayRoundTripFields`, on `listOverlay` only, the one row shape the console's settings-overlay marker is written on.
382+
>
383+
> No form-overlay row carries these keys: the switcher lists list-family
384+
> views only.
385+
> 4. **The census's other finds map to a spelling already declared, not to
386+
> a new key.** `objectName`, named in the superseded bullet, maps to
387+
> `object`; a top-level `id` maps to `name`; a bare-array
388+
> `exportOptions` maps to the object form the parse lifts it to.
389+
> `filter[].id` / `sort[].id` are removed before the parse by `packages/spec/src/ui/view.zod.ts#stripViewConsoleDecorations` (`#VIEW_CONSOLE_ROW_DECORATIONS`).
390+
> `_draft` / `_diagnostics` are `packages/spec/src/kernel/metadata-read-decorations.ts#METADATA_READ_DECORATIONS`, stamped on the read and never stored.
391+
> The record's docblock gives the reason for each.
392+
> 5. **Interim state: the storage switch is stage (iv) of #20051, and it
393+
> has not landed.** Until it does, `packages/metadata-protocol/src/protocol.ts#saveMetaItem` stores the request body, with the normalizations it grafts back from the parse, and the pins that hold that verbatim save stay green.
394+
> One of them is `packages/objectql/src/protocol-meta.test.ts`
395+
> "preserves Studio-only auxiliary fields verbatim (not stripped)", the
396+
> 2026-05-16 "unknown extras preserved" case listed below. So today a
397+
> key no member declares is still stored: "declared" does not yet mean
398+
> "the only keys stored".
399+
> 6. **What stage (iv) does, on #20051 itself.** Persistence stores the
400+
> parsed body; the GUARD pins flip to assert the declared round-trip
401+
> instead of the verbatim save; the top-level `options` of
402+
> `ViewItemWireSchema` is judged in the same step; and the switch lands
403+
> only once the count of stored `view` rows carrying undeclared top-level
404+
> keys has been measured against production `sys_metadata`. Made before
405+
> its prerequisites, the switch turns 「stored and unread」 into 「200,
406+
> then the user's pinned / sort / visibility state silently dropped」.
343407
344408
Sample 422 response:
345409

0 commit comments

Comments
 (0)