Skip to content

Commit fd43dc2

Browse files
fix(metadata-protocol): a .custom pair's published override of a page, app, dataset, doc or book is served from its row by every read (#29707)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8XgtQ7Gh7PURXHo26gxhWum)_ Fixes #29613 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`.custom` 配对包对基础包页面、应用、数据集、文档、文档书的已发布覆盖,所有读取都从覆盖自己的存储行提供,和仪表盘、报表一样;注册表裸槽位不再放一份穿着基础包信封的副本。`/me/apps` 改读协议的有效列表。 - 为什么改:这五种类型不接受环境覆盖,读取把配对包的覆盖行也当成被封存的覆盖拒掉,覆盖只能靠裸槽位副本提供。真实启动(有元数据服务)时按名读取返回基础包原件,列表却返回覆盖,分层读取和配对包的按包层从来看不到覆盖,违背 ADR-0146 D8。 - 风险与回滚:按名读取(真实启动)、分层读取的 effective、按包层会变化,全部朝 D8 方向,席位已批准;已经返回覆盖的读取不变。回滚即还原本 PR,无需迁移,重启即按新规则重新水合。 - 先看:`packages/metadata-protocol/src/protocol-lock-state.ts` 的 `declinesStoredRow`;`protocol-package-bindings.ts` 的两个类型清单;`plugin-hono-server` 的 `servedAppsOf`。 - 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run src/protocol-custom-override-bare-slot-envelope.test.ts`,预期 24 个用例全绿(其中五种类型各两种启动形态、重启前后)。 - 需要你做的:无,席位落地 ## What changes A `.custom` pair's published override of a base's `page`, `app`, `dataset`, `doc` or `book` is now served from its stored row by every read, the way a dashboard's or report's already is. The registry's bare slot no longer holds a copy of it wearing the base's envelope. These five types are sealed (ADR-0131 D6: `allowEnvironmentOverlay: false`). Every read asked `declinesStoredRow`, which declined their stored rows as overlays of managed content (`isUnservedSealedOverlay`), and that included the pair's declared override. The only place the override was served from was the bare slot. With a metadata service (every real boot), the by-name read answered the service's copy of the base's body before it reached the registry. - `packages/metadata-protocol/src/protocol-lock-state.ts`: `declinesStoredRow` admits, for these five types, the shipping package's declared override, judged by `isServedOverrideRowOf` on the row's own `package_id` and body against `packagedArtifactOwner`. This is the same admission flow got in #28412, and the same predicate the boot's sealed-overlay census already uses to leave the override out of its warning. Every other sealed row of the name stays declined. Every reader already passes the row it serves (by-name, both lists, layered, the per-package customizations read), so no reader is edited. - `packages/metadata-protocol/src/protocol-package-bindings.ts`: the closed list `SEALED_ROW_SERVED_OVERRIDE_TYPES` and `servesSealedOverrideRow`; the five types join `BORROWED_ENVELOPE_DECLINED_TYPES`, so the #22057 guard keeps their row out of the bare slot under #29600's condition (the row's package ships no artifact of the name while another package does). The #4624 two-shipper case keeps its bare entry. `mapping` (overridable and sealed) is left out: no card measured its reads. `action` and `flow` wait for ADR-0146 D9. - `packages/metadata-protocol/src/protocol.ts`: comments only. Three docblock sentences that said these five types still register in the bare slot are corrected. No code line changes. - `packages/plugins/plugin-hono-server/src/current-user-endpoints.ts`: `GET /me/apps` lists the protocol's served apps (`getMetaItems({ type: 'app' })`, what `GET /meta/app` serves) when a protocol service with that read is present. With no protocol service, today's registry path is unchanged. A protocol list read that throws is warned once and answered from the registry. File surface confirmed with the seat first. - `.changeset/29613-sealed-pair-override-row-served.md`: patch for both packages. ## Why ADR-0146 D8 item 2: every read that serves a base's name serves the `.custom` item that declares it. Item 3: the pair's per-package layer carries the overriding item, and the base's carries the base unchanged. Measured on main `744211fc` (table below), these five types broke both items, and on a real boot the by-name read and the list disagreed about the same name. The card's two hypotheses reconcile: the #29600 harness has no metadata service, so its by-name read fell to the bare slot (override); the #29649 real-boot reading has one, so its by-name read answered the shipped body. Both readings are reproduced below, one harness run each way. ## Tests **Before and after, per type.** Identical for `page`, `app`, `dataset`, `doc` and `book`. S = the shipped body, O = the pair's published override. Before = main `744211fc`, the #29600 harness, without and with a metadata service holding the code package's bodies. After = this branch, both shapes, and after a restart. | Reader | Before, no metadata service | Before, with one (a real boot) | After (both, and after a restart) | ADR-0146 D8 | |---|---|---|---|---| | `getMetaItem` (no package, BASE, PAIR) | O | **S** | O | item 2 | | `getMetaItems` (no package, BASE) | [O] | [O] | [O] | item 2 (unchanged) | | `getMetaItemLayered(BASE).effective` | **S** | **S** | O | item 2 | | per-package layer, PAIR | **[]** | **[]** | [O] | item 3 | | per-package layer, BASE | **[S, O]** | **[O]** | [S] | item 3 | | registry bare slot | O wearing BASE | O wearing BASE | none | | | `registry.getItem` (bare, BASE, PAIR) | O | O | S | registry-direct | | `registry.getAllApps` (app) | [S, O] | [S, O] | [S] | registry-direct | | `GET /me/apps` (app) | the registry's list | the registry's list | the protocol's served list: O | item 2 | Each moved reading is seat-approved as code aligning to D8 (no protocol change, so `Clause-②: no` and `Metadata-protocol: none`). New and extended pins: - `packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts`: a second top-level describe in #29600's shared harness file (the rule on main since #29633). Per type, with and without a metadata service, every reader in the table at once and after a restart over the same stores (10 tests). CONTROL per type: a package-less environment row of the base's name (pre-seal hatch residue) stays unserved: by-name and layered answer S (5 tests). The driver now takes optional shared stores and a module-level id counter so a restart works; the #29600 tests are untouched otherwise. 24/24 green. - `packages/metadata-protocol/src/protocol-package-bindings.sealed-override.test.ts` (new; the module had no test file): the two closed lists agree, plural spellings fold, `mapping`/`action`/`flow`/regime-O types are outside, the #4624 case keeps its bare entry. 20/20 green. - `packages/plugins/plugin-hono-server/src/current-user-endpoints-delegated-resolution.test.ts`: a `[#29613]` describe beside the file's other `/me/apps` pins. With a protocol service the served list is the source and the registry is not read; with none, or with a protocol that has no list read, the registry as before; a failing list read is warned and answered from the registry. 14/14 green. <details><summary>Ablation (both mechanism lines back to main's behaviour)</summary> The fix was committed first. `servesSealedOverrideRow` was mutated to return false (marker `ABLATION29613`) and the five types were removed from `BORROWED_ENVELOPE_DECLINED_TYPES` (marker `ABLATION29613B`), `grep -c` = 2 on disk, then `pnpm --filter @objectstack/metadata-protocol build`. ``` objectql protocol-custom-override-sealed-row-served (now folded into the shared file): × page/app/dataset/doc/book, with no metadata service: every reader serves the override, at once and after a restart × page/app/dataset/doc/book, with a metadata service (a real boot): every reader serves the override, at once and after a restart Tests 10 failed | 5 passed (15) <- the 5 CONTROL cases stay green, by design metadata-protocol protocol-package-bindings.sealed-override: Tests 6 failed | 14 passed (20) restore: git checkout HEAD -- protocol-package-bindings.ts; DIFF-HEAD-EMPTY=yes; rebuilt; ablation-dist-preflight --absent: tree clean against HEAD, ABSENT-EXIT=0 ``` The preflight's presence leg printed a refusal about an untracked path at that moment instead of a marker count. The 10 objectql reds read the protocol from `dist/`, which is the evidence the mutation reached it. `/me/apps` ablation (`ablation-replace.mjs`, `servedAppsOf(ctx)` replaced by `undefined`, source-imported): 2 failed | 2 passed (the two served-list cases red; the two registry cases green by design); restored, blob equals HEAD. </details> <details><summary>Package suites (on the branch before the merge of main)</summary> ``` @objectstack/objectql test Test Files 544 passed (544) Tests 10616 passed (10616) exit 0 @objectstack/metadata-protocol test Test Files 309 passed | 4 skipped (313) Tests 22282 passed | 26 skipped exit 0 @objectstack/plugin-hono-server test Test Files 39 passed (39) Tests 476 passed (476) exit 0 ``` After the merge of main (closure rebuilt): runtime `packages-export-base-unchanged-per-type.integration.test.ts`, `packages-customizations.test.ts`, `packages-ensure-customization.test.ts`: 56/56, exit 0. `typecheck` for metadata-protocol, objectql, plugin-hono-server: exit 0. </details> #29649's showcase-boot export pin: `pairServed` widened to all eight types (`f40dd0e68`); 8/8 green, ablation 5 red. ## Acceptance notes - Two shippers of one of these names: the admission judges the row against `packagedArtifactOwner` (the first package the artifact lookup answers), as flow's admission does. A pair override of the OTHER shipper would stay declined, and with the guard its bare entry goes too. Not measured; no shipped stack has two packages shipping one page, app, dataset, doc or book name. - `mapping` is overridable and sealed and has the same shape. Left out on purpose (no measurement). - #29649 landed first (`33ee9aa1`); its pin re-run on the merged head, green, and widened. - The base export (`packages/runtime/src/domains/packages.ts`) is not touched. With this PR the base's per-package layer lists S alone for these types, which is what #29649's filter assumes. ## Verification Local subset on head `90267e800` (after the merge of main), derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (8 paths), each exit captured before any pipe, then reconciled: ``` ✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). ``` - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: PREREQUISITE NOT MET (it reads every package's `dist/`, and only this change's closure is built here). CI's `Lint & Repo Gates` runs it. - `node scripts/check-plugin-teardown-shape.mjs --self-test` first exited 3 (its pinned fixture commit was outside the shallow clone); after `git fetch origin 621a487` it exits 0. - `pnpm lint`: exit 0. <details><summary>Recorded exit codes</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-engine-split-ratio.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:authz-resolver :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:durability-log-level :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:error-status-conformance :: exit 0 pnpm check:filter-alias-parity :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:route-envelope :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm lint :: exit 0 ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbXCAvuF6biLpPaEcJ5)_ <!-- signed: domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 10e4a77 commit fd43dc2

9 files changed

Lines changed: 476 additions & 32 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/plugin-hono-server': patch
4+
---
5+
6+
fix(metadata-protocol): a `.custom` pair's published override of a page, app, dataset, doc or book is served from its stored row by every read, and no longer sits in the registry under the base's envelope
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Pages, apps, datasets, docs and books take no environment overlay, so every read declined their stored rows as overlays of a package's item. That included the override a base's `<base>.custom` pair publishes under ADR-0146. The override was served only from a copy the registry kept under the item's bare name, wearing the base's envelope. On a server with a metadata service (every real boot), the by-name read answered the base's shipped item instead, while the lists answered the override.
12+
13+
The reads now serve the pair's declared override from its row, as they already did for dashboards and reports. Every other stored row of such a name is still not served. The registry no longer keeps the copy under the base's envelope. What moves, for each of the five types, all toward ADR-0146 D8:
14+
15+
- `GET /meta/TYPE/NAME` with no package, the base or the pair: the override on every server (item 2). Before, it served the base's shipped item whenever a metadata service was present.
16+
- The layered read naming the base, `effective`: the override (item 2). Before, the base's shipped item.
17+
- The per-package layer naming the pair: the override (item 3). Before, empty. So the pair's export (`GET /packages/PAIR/export`) now carries the override with its `overrides` block, as it does for a dashboard.
18+
- The per-package layer naming the base: the base's own item only (item 3). Before, it also listed the override, or listed only the override when a metadata service was present.
19+
- `GET /me/apps` (`@objectstack/plugin-hono-server`): it now lists the protocol's served apps (what `GET /meta/app` serves) when the protocol service is present, so the pair's override of an app is listed. With no protocol service it reads the registry as before.
20+
- A registry read that names the item directly answers the base's own item.
21+
22+
Reads that already served the override still do: the effective list (no package, the base). Nothing to migrate: a restart re-hydrates the registry under the new rule.

‎packages/metadata-protocol/src/protocol-lock-state.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { canonicalMetaType } from './protocol.js';
5050
import type { ItemLock } from './item-lock.js';
5151
import { isOriginGatedType } from './packaged-base-regime.js';
5252
import { isServedOverrideRowOf, type ServedOverrideRow } from './served-override.js';
53+
import { servesSealedOverrideRow } from './protocol-package-bindings.js';
5354
import { isCodeArtifactBody } from '@objectstack/metadata-core';
5455
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
5556
import {
@@ -147,7 +148,15 @@ export function declinesStoredRow(
147148
return !isServedOverrideRowOf(row, this.packagedArtifactOwner({ type: 'flow', name: name as string }));
148149
}
149150
if (typeof name !== 'string' || name === '') return false;
150-
return this.isDeclaredCodeDatasource(type, name) || this.isUnservedSealedOverlay(type, name);
151+
if (this.isDeclaredCodeDatasource(type, name)) return true;
152+
if (!this.isUnservedSealedOverlay(type, name)) return false;
153+
// [#29613, ADR-0146 D8 item 2] …except, for a page, app, dataset, doc or
154+
// book, the shipping package's declared override: judged as the flow's
155+
// above, on the row's own `package_id` and body, against the package
156+
// that ships the name. The reads then serve it from its row, as they
157+
// serve a dashboard's. Every other sealed row of the name stays declined.
158+
return !(servesSealedOverrideRow(type)
159+
&& isServedOverrideRowOf(row, this.packagedArtifactOwner({ type, name })));
151160
}
152161

153162
/** The body of `ObjectStackProtocolImplementation#isUnservedSealedOverlay`; the method's docblock there is its contract. */
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#29613] The two closed type lists the row-served override path rests on:
5+
* the sealed types whose declared `.custom` override the reads serve from its
6+
* row ({@link servesSealedOverrideRow}), and the types whose such row stays out
7+
* of the registry's bare slot ({@link declinesBorrowedEnvelope}). Every type of
8+
* the first is in the second, so no override loses its bare entry before its
9+
* row is served, and none keeps a bare entry wearing the base's envelope after.
10+
*
11+
* The readings themselves (every reader, per type, with and without a
12+
* metadata service) are pinned in `@objectstack/objectql`'s
13+
* `protocol-custom-override-sealed-row-served.test.ts`, over the real registry.
14+
*/
15+
import { describe, expect, it } from 'vitest';
16+
import { declinesBorrowedEnvelope, servesSealedOverrideRow } from './protocol-package-bindings.js';
17+
18+
const ROW_SERVED = ['page', 'app', 'dataset', 'doc', 'book'] as const;
19+
/** The base's code artifact the hydrator's lookup falls back to for a pair's row. */
20+
const BASE_ARTIFACT = { _packageId: 'com.example.showcase' };
21+
22+
describe('[#29613] the sealed row-served override types', () => {
23+
it.each(ROW_SERVED)('%s: its declared override is served from its row, and its row takes no borrowed envelope', (type) => {
24+
expect(servesSealedOverrideRow(type)).toBe(true);
25+
expect(declinesBorrowedEnvelope(type, 'com.example.showcase.custom', BASE_ARTIFACT)).toBe(true);
26+
});
27+
28+
it('folds a plural spelling to the canonical type', () => {
29+
expect(servesSealedOverrideRow('pages')).toBe(true);
30+
expect(servesSealedOverrideRow('apps')).toBe(true);
31+
});
32+
33+
it.each(['mapping', 'action', 'flow', 'object', 'permission', 'view', 'dashboard', 'report', 'email_template'])(
34+
'%s: not a sealed row-served override type',
35+
(type) => {
36+
expect(servesSealedOverrideRow(type)).toBe(false);
37+
},
38+
);
39+
40+
it.each(ROW_SERVED)('%s: the #4624 case keeps its bare entry (a row of the package that ships the artifact)', (type) => {
41+
expect(declinesBorrowedEnvelope(type, 'com.example.showcase', BASE_ARTIFACT)).toBe(false);
42+
expect(declinesBorrowedEnvelope(type, null, BASE_ARTIFACT)).toBe(false);
43+
});
44+
});

‎packages/metadata-protocol/src/protocol-package-bindings.ts‎

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,53 @@ function boundPackageOf(packageId: unknown): string | undefined {
306306
* no reader the row does not. `view` is not here: it has its own, wider rule
307307
* (#22057, `anotherPackageShips`).
308308
*
309-
* ⛔ A closed list, judged on the canonical type. `page`, `app`, `dataset`,
310-
* `doc` and `book` are left out on purpose: their reads serve a `.custom`
311-
* pair's override FROM the bare entry (the by-name read, the list, and
312-
* `registry.getAllApps`), so they need a row-served path before their bare
313-
* entry can go. `flow` registers with no artifact envelope at all (Regime C,
309+
* ⛔ A closed list, judged on the canonical type. [#29613] It includes the
310+
* five {@link SEALED_ROW_SERVED_OVERRIDE_TYPES} since their reads serve a
311+
* `.custom` pair's declared override from its row, as these three types'
312+
* reads do. `flow` registers with no artifact envelope at all (Regime C,
314313
* #20913), and the boot pull arms its declared override from the bare slot.
315314
*/
316-
const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set(['dashboard', 'report', 'email_template']);
315+
const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
316+
'dashboard',
317+
'report',
318+
'email_template',
319+
// [#29613] The SEALED_ROW_SERVED_OVERRIDE_TYPES, spelled out (`protocol-package-bindings.sealed-override.test.ts` pins both lists).
320+
'page',
321+
'app',
322+
'dataset',
323+
'doc',
324+
'book',
325+
]);
326+
327+
/**
328+
* [#29613, ADR-0146 D8 item 2] The SEALED types (ADR-0131 D6: a registry
329+
* entry with `allowEnvironmentOverlay: false`) whose stored row the reads
330+
* serve when it is the shipping package's declared `.custom` override
331+
* ({@link isServedOverrideRowOf}): `page`, `app`, `dataset`, `doc` and
332+
* `book`, the overridable types of ADR-0146 D2 that open no environment
333+
* overlay. Every other stored row of a name a managed package ships stays
334+
* unserved for them ({@link isUnservedSealedOverlay}).
335+
*
336+
* Before, the reads declined the override's row with the rest, and the
337+
* override was served only from the registry's bare slot, where the hydrator
338+
* registered it wearing the base's envelope. With a metadata service (every
339+
* real boot) the by-name read answered the service's copy of the base's body
340+
* instead, while the lists answered the override, and the layered read's
341+
* effective layer and the pair's per-package layer never saw it.
342+
*
343+
* ⛔ A closed list, judged on the canonical type. `mapping` is overridable and
344+
* sealed too, and is not here: no card has measured its reads. `action` and
345+
* `flow` wait for ADR-0146 D9's conditions (`flow` has its own admission,
346+
* `declinesStoredRow`'s first branch). Every type here is also in
347+
* {@link BORROWED_ENVELOPE_DECLINED_TYPES}: the bare entry served no reader
348+
* the row does not serve now.
349+
*/
350+
const SEALED_ROW_SERVED_OVERRIDE_TYPES: ReadonlySet<string> = new Set(['page', 'app', 'dataset', 'doc', 'book']);
351+
352+
/** [#29613] Whether `type` is one of {@link SEALED_ROW_SERVED_OVERRIDE_TYPES}, folded to the canonical singular. */
353+
function servesSealedOverrideRow(type: string): boolean {
354+
return SEALED_ROW_SERVED_OVERRIDE_TYPES.has(canonicalMetaType(type));
355+
}
317356

318357
/**
319358
* [#29600] Whether the hydrator keeps a stored row of `canonicalType`, bound
@@ -462,6 +501,7 @@ export {
462501
isShippedViewItem,
463502
mergeArtifactProtection,
464503
perPackageLayerEnvelope,
504+
servesSealedOverrideRow,
465505
stateTenantAuthorship,
466506
stripDerivedProvenance,
467507
};

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8764,7 +8764,10 @@ export class ObjectStackProtocolImplementation implements
87648764
* regime D6 recognises — the package definition wins. Such rows were
87658765
* written through the `OS_METADATA_WRITABLE` hatch before the seal; they
87668766
* stay at rest, untouched, and the boot report names them per type with
8767-
* the two remedies ({@link reportSealedOverlayRows}).
8767+
* the two remedies ({@link reportSealedOverlayRows}). [#29613] Except,
8768+
* for a `page`, `app`, `dataset`, `doc` or `book`, the shipping
8769+
* package's declared `.custom` override (`row`, below): ADR-0146 D8
8770+
* item 2 serves it, as a dashboard's.
87688771
*
87698772
* ⛔ Never a row's, slot's or body's `origin` — the caller sets it.
87708773
*
@@ -10851,11 +10854,11 @@ export class ObjectStackProtocolImplementation implements
1085110854
* after, measured for each reader. What changes is the registry-direct
1085210855
* read of the name, which answers the shipping package's own item.
1085310856
*
10854-
* ⛔ Those three types only. A row of a package that ships its own artifact
10855-
* of the name keeps its bare entry (#4624). `page`, `app`, `dataset`, `doc`
10856-
* and `book` still register: their reads serve a pair's override from the
10857-
* bare entry, so it can go only once they serve it from the row. `flow`
10858-
* registers with no artifact envelope (below).
10857+
* ⛔ Those three types, and [#29613] `page`, `app`, `dataset`, `doc` and
10858+
* `book`, whose reads serve a pair's declared override from its row since
10859+
* {@link declinesStoredRow} admits it. A row of a package that ships its own
10860+
* artifact of the name keeps its bare entry (#4624). `flow` registers with
10861+
* no artifact envelope (below).
1085910862
*
1086010863
* ## [#23233] The entry carries the row's package binding
1086110864
*
@@ -11015,7 +11018,7 @@ export class ObjectStackProtocolImplementation implements
1101511018
// bound to a package that ships no artifact of its name, under a name
1101611019
// another package ships, is not registered under the bare name, where
1101711020
// it would wear that other package's envelope. Its reads answer it from
11018-
// its row. ⛔ Those three types only.
11021+
// its row. ⛔ Those three types, and [#29613] page, app, dataset, doc and book.
1101911022
if (declinesBorrowedEnvelope(canonicalType, options.packageId, artifact)) return true;
1102011023
// [#16702] Say what this row IS before the artifact envelope is grafted
1102111024
// on top of it. Every body reaching this hydrator came out of a

0 commit comments

Comments
 (0)