diff --git a/docs/adr/0005-metadata-customization-overlay.md b/docs/adr/0005-metadata-customization-overlay.md index a893abb2778..62fe9587495 100644 --- a/docs/adr/0005-metadata-customization-overlay.md +++ b/docs/adr/0005-metadata-customization-overlay.md @@ -3,7 +3,7 @@ > **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. -**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) +**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, the ruling's last stage, is in force for `view` since PR #20868 (`9905e61ca2`) — see the amendment notes under that addendum's persisted-document bullet) · **Amended** (2026-09-30, #20051 ruling B — the parsed body a `view` stores is the parsed value of every key its request body carried, not the whole parse output: undeclared keys dropped, schema defaults not materialised, a moved key stored under its canonical spelling; every other type still stores its request body — see the second amendment note under that bullet) **Deciders**: ObjectStack Protocol Architects **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) **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,15 +334,21 @@ Implementation (`packages/metadata-protocol/src/protocol.ts`): carrying `path/message/code` for each Zod issue. REST layer (`packages/rest/src/rest-server.ts`) already propagates `status` and `code` to the response. -- The persisted document is the **parsed body** (`parsed.data`) once every - round-trip key is declared. A key a client writes onto a stored row and - reads back after a reload is declared on the type's schema, with its - meaning, so the parse keeps it; a key nothing declares is not stored. The - canonical fields are type-checked, and the stored row is what the contract - says it is. ⚠️ **Ruled, not yet in force:** the storage switch is the - ruling's last stage and has not landed, so today `saveMetaItem` still - stores the request body (`request.item`). The note below records what is - declared and what remains. +- The persisted document of a `view` is the parsed value of every key its + request body carried (`projectStorableViewBody`), not the whole parse + output: a key nothing declares is dropped, a declared key keeps its parsed, + normalised value, a moved key is stored under its canonical spelling + (`groups` → `sections`, `visibleOn` → `visibleWhen`), and a schema default + the author did not write is not stored — the rule + [ADR-0087's addendum 2026-08-01b](./0087-metadata-protocol-upgrade-contract.md#addendum-2026-08-01b--flows-reach-the-finish-line-too-4454) + records for a flow's `storable`. The stored row re-parses to exactly what + the save accepted. A key a client writes onto a stored `view` row and reads + back after a reload is declared on the type's schema, with its meaning, so + the parse keeps it. The canonical fields are type-checked, and the stored + row is what the contract says it is. Every other type still stores its + request body (`request.item`). In force since PR #20868 (`9905e61ca2`); + rows stored before it are not migrated. The notes below record what is + declared and what a `view` save stores. > **Amended (2026-09-30) — the persisted document is the parsed body once > every round-trip key is declared, and a stored `view` row's round-trip @@ -389,6 +395,13 @@ Implementation (`packages/metadata-protocol/src/protocol.ts`): > `filter[].id` / `sort[].id` are removed before the parse by `packages/spec/src/ui/view.zod.ts#stripViewConsoleDecorations` (`#VIEW_CONSOLE_ROW_DECORATIONS`). > `_draft` / `_diagnostics` are `packages/spec/src/kernel/metadata-read-decorations.ts#METADATA_READ_DECORATIONS`, stamped on the read and never stored. > The record's docblock gives the reason for each. + > + > ⚠️ **Historical — superseded.** Items 5 and 6 record the interim state and + > the plan as they stood at stage (iii), and are retained verbatim. The + > storage switch landed in PR #20868 (`9905e61ca2`) under ruling B, without + > the production count item 6 names, which the maintainer waived. The next + > note carries the present tense. + > > 5. **Interim state: the storage switch is stage (iv) of #20051, and it > 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. > One of them is `packages/objectql/src/protocol-meta.test.ts` @@ -405,6 +418,70 @@ Implementation (`packages/metadata-protocol/src/protocol.ts`): > its prerequisites, the switch turns 「stored and unread」 into 「200, > then the user's pinned / sort / visibility state silently dropped」. + > **Amended (2026-09-30) — the storage switch is in force for `view`: a + > saved view stores the parsed value of every key its request body carried, + > not the whole parse output.** Provenance: ruling B on #20051 — maintainer + > 「同意 批次 #256」, 2026-09-30, director batch #256 item 3, Q2 = B, + > recorded on the card as comment 5910106571 — which rules that "a saved + > view stores the keys the author wrote, normalised by the parse: undeclared + > keys dropped, schema defaults NOT materialised". The maintainer's + > 「20051 不考虑现有的数据」 (comment 5909085121) waived the stored-row count + > the first note's item 6 made a precondition. Stage (iv) of ruling 甲 + > landed under ruling B in PR #20868, merged as `9905e61ca2`. Why B rather + > than the whole parse output: views and flows keep one rule, a stored row + > carries no schema default the author did not write, so it never pins the + > day's default or records a choice the author never made. + > + > 1. **Superseded.** Written at stage (iii), the bullet above read: "The + > persisted document is the **parsed body** (`parsed.data`) once every + > round-trip key is declared. A key a client writes onto a stored row and + > reads back after a reload is declared on the type's schema, with its + > meaning, so the parse keeps it; a key nothing declares is not stored. + > The canonical fields are type-checked, and the stored row is what the + > contract says it is. ⚠️ **Ruled, not yet in force:** the storage switch + > is the ruling's last stage and has not landed, so today `saveMetaItem` + > still stores the request body (`request.item`). The note below records + > what is declared and what remains." The Status line's ruling 甲 entry + > ended "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". + > 2. **What a `view` save stores.** For `view` only, `packages/metadata-protocol/src/protocol.ts#saveMetaItem` stores what `packages/metadata-protocol/src/protocol.ts#projectStorableViewBody` builds from the request body and its parse: + > - a key the body carried and the parse kept is stored with its parsed + > value, so a legacy filter `operator` spelling is stored normalised + > and a bare-array `exportOptions` in its object form; + > - a key the body carried and the parse did not keep is dropped: an + > undeclared key (`objectName`, a top-level `id`), a console + > decoration (`sort[].id`), or the old spelling of a moved key; + > - a key the parse added is stored only when a re-parse of the body + > does not reproduce it: that is a moved key, stored under its + > canonical spelling (`groups` → `sections`, `visibleOn` → + > `visibleWhen`). A key the re-parse reproduces is a schema default, + > and it is not stored. + > + > The projection re-parses the body it builds until that re-parse equals + > the save's parse, so the stored row re-parses to exactly what the save + > accepted. If no such body is found, the whole parse output is stored + > and a warning is logged once per view: nothing the author wrote is + > lost, only that row's defaults are pinned. + > 3. **The two grafts no longer run on a `view` save.** `packages/metadata-protocol/src/protocol.ts#graftNormalizedOperators` and `packages/metadata-protocol/src/protocol.ts#graftFoldedFormSections` are special cases of the projection. + > Every other type still stores its request body, with the + > normalizations `saveMetaItem` applied to it before the switch. + > 4. **A `view` item record's top-level `options` is refused by name** (Q3 + > = A of the same ruling), the judgement the first note's item 6 left + > open: `422 INVALID_METADATA` at `options`, with the prescription to + > write `config.KIND` (`packages/spec/src/ui/view.zod.ts#VIEW_ITEM_OPTIONS_REFUSED`; the ADR-0087 semantic entry `view-item-options-bag-refused`). + > The flattened list overlay keeps its legacy `options` bag, judged key + > by key. + > 5. **The pins.** `packages/metadata-protocol/src/protocol.project-storable-view-body.test.ts` + > holds the projection's rules and its fail-safe. The stage (iv) blocks + > of `packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts` + > hold the save: the console's toolbar saves store no `type` default, an + > undeclared key is dropped, the persisted body parses to exactly what + > the save parsed, and a page is stored as sent. The 2026-05-16 "unknown + > extras preserved" case listed below is re-judged in + > `packages/objectql/src/protocol-meta.test.ts`: the declared round-trip + > keys are stored and `objectName` is dropped. + Sample 422 response: ```json