Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 87 additions & 10 deletions docs/adr/0005-metadata-customization-overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand Down
Loading