Skip to content

Commit 15f808a

Browse files
fix(metadata-protocol,app-shell): serve a pair's mapping override, let a pair remove any override it holds, name a working remedy for an excluded type (#30062) (#30226)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9beNpM7b18QnbuwqgHvEwS)_ domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb ## 维护者速读 - **改了什么:** 客户在定制包(`BASE.custom`)里声明覆盖的 mapping 现在真的生效(导入用的是改过的映射);定制包里任何可覆盖类型的覆盖都能删掉,删掉后基包原件回来(doc、mapping 以前报 403);给不可覆盖类型加 `overrides` 时,拒绝信息引用该类型自己的依据并给出一条走得通的出路;「全部元数据」里的类型瓷砖在只读包上显示「可定制」当且仅当 Studio 真的给了「定制」入口(flow、app 以前误显示「只读」)。 - **为什么改:** ADR-0146 D2 原文:定制包里的项替换基包的项,删掉它就回到基包的项;ADR-0146 的 revert(「恢复包原件」)就是删掉 `.custom` 里的覆盖项。旧行为对 doc/mapping 拒绝了这条路,mapping 覆盖保存发布后不起作用,拒绝文案一律叫人「新建同类」而有些类型根本不能新建。 - **风险与回滚:** 删除只对定制包自己的行放宽(点名 `?package=BASE.custom`);点名基包的删除仍是原答案,覆盖照旧生效(消融 A3 为证)。错误码与状态不变,无 spec 改动。回滚即撤 PR。 - **先看:** `packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(`isPairRowRemoval` 的放宽与 `excludedTypeRemedy`),`packages/metadata-protocol/src/protocol-package-bindings.ts`(`SEALED_ROW_SERVED_OVERRIDE_TYPES` 加 `mapping`),`ui/packages/app-shell/src/views/metadata-admin/PageShell.tsx`(`studioOffersCustomize`)。 - **两分钟验证:** `pnpm --filter @objectstack/objectql exec vitest run src/protocol-custom-override-bare-slot-envelope.test.ts -t 30062`:每个可覆盖类型在两种内核上,删除定制包的覆盖后所有读都回到基包原件;点名基包的删除不动任何东西。 - **需要你做的:** 无,席位落地。第 3 项(翻译与覆盖自身 label 的优先级)不在本 PR,见 Acceptance notes。 Refs #30062 (items 1, 2, 4, 5) Clause-②: no — ADR-0146 D2 ("an item in the customization package replaces the base's item, and removing it brings the base's back") and ADR-0146's revert ("Restore package original" = delete the overriding item from `.custom`, so the base's document is served again) already admit both the served mapping override and the pair's removal of its own doc or mapping override; the 403 and the unserved row refused that text. A removal naming the base keeps today's answer, and no accepted write is refused that was accepted before. Metadata-protocol: semantic Skills-docs: follow-up #30229 — `skills/objectstack-platform/SKILL.md`'s ADR-0146 paragraph says the read honours a pair's declared override "today only for a view, dashboard, report, email_template, translation or flow"; that list predates page, app, dataset, doc and book, and now mapping, and it says nothing of the removal. `skills/**` is outside this card's order, so the seat carries the edit. ## What changes **Item 1 — a mapping override is served.** `packages/metadata-protocol/src/protocol-package-bindings.ts`: `mapping` joins `SEALED_ROW_SERVED_OVERRIDE_TYPES` (page, app, dataset, doc, book) and `BORROWED_ENVELOPE_DECLINED_TYPES`. A mapping is sealed (`allowEnvironmentOverlay: false`) and open for override (ADR-0146 D2, no D9 condition), and its pair row was declined with every other stored row of a name a managed package ships, so a saved and published mapping override answered the base's body on every read. Now the by-name read (no `?package` and `?package=BASE`), both lists, the layered read and the pair's per-package layer serve it. The import door (`@objectstack/rest` `import-mapping.ts:63`) and the connector pull (`service-automation` `connector-pull.ts:244`) resolve a named mapping through that package-less `getMetaItem`, so they get the pair's mapping. The served arm was chosen, not "the save refuses loudly", so `Clause-②: no` does not narrow. Comment-only updates in `protocol-lock-state.ts`, `protocol-registry-hydration.ts` and `protocol.ts` name the sixth type. **Item 2 — the pair's removal of its own override works for every overridable type.** Measured: the doc and mapping 403 came from `refusePackagedBaseRemoval` (`protocol-write-gates.ts`: a sealed managed item on a kernel that does not merge the overlay at read), which `deleteMetaItemUnaudited` skips only when `isPairRowRemoval` admits the row, and `isPairRowRemoval` admitted only `CONDITIONS_DELIVERED = {flow}`. `protocol-packaged-base-refusal.ts`: `isPairRowRemoval` now admits the pair's row for every type `isOverridableMetadataType` opens (`isOpenOverrideType`, so `action` still waits for D9); the pair check is unchanged (the package the removal names must be the base's customization package). The repository's delete gate then takes the `runtime-only` intent the declared save wrote, which every overridable type's `allowRuntimeCreate` admits. No spec flag moved. A removal naming the base (`?package=BASE`) is untouched: on the host-config kernel it answers `{ success: true, reset: false }` with "No row … remains" and removes nothing; on the environment kernel a doc or mapping still answers `403 NOT_OVERRIDABLE`. **Item 4 — the All metadata tile agrees with the Customize door.** `ui/packages/app-shell/src/views/metadata-admin/PageShell.tsx`: new `studioOffersCustomize(type)` = overridable and offered by a Studio door (`isStudioCustomizeType`: view, dashboard, report, app; `AREA_FORM_EDITOR_TYPES`: email_template, translation; and the flow header). On a read-only package `typeStateWord` reads it instead of `allowEnvironmentOverlay`, so flow and app read 可定制, and doc, mapping, page, dataset and book read 只读 until a door exists. `StudioCustomizeControls.tsx` is not touched (seat ruling): a door added to its list moves the tile with it. **Item 5 — an excluded type's refusal names a remedy that works for it.** `judgeOverrideWrite`'s `declared_type_not_overridable` sentence cited ADR-0146 D6 and told every type to "create your own TYPE under a new name". It now cites the type's own basis from `OVERRIDE_TYPE_CLASSIFICATION` (`EXCLUSION_BASIS_CITATION`: D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 otherwise) and `excludedTypeRemedy` prescribes, in order: Setup › Datasources for a datasource; the object extension for a field (`OBJECT_EXTENSION_ITEM_DOOR`); "create your own TYPE under a new name" only where the registry admits a runtime create; the type's ADR-0126 Regime C prescription where it has a row (`packagedBaseRegimePrescription`: a sharing rule names `POST /api/v1/sharing/rules` and its switch); Setup › Queues for a queue; otherwise "an X is defined only in package source, so it changes only with a new version of 'BASE'". `articleFor` fixes "a api". Code (`NOT_OVERRIDABLE`), status (403) and rule are unchanged, and the permission sentence is byte-identical (pinned by `runtime` `meta-object-extension-item-door-fit.test.ts:161`). **Docs (head `5663983b`, added by the card thread after the CI Docs Drift Check, comment 6648033732).** `content/docs/concepts/metadata-lifecycle.mdx`: a declared override is served for every type of `OVERRIDABLE_METADATA_TYPES` but `action` (which waits for ADR-0146 D9's conditions); the import door and the connector pull run the pair's mapping; a new bullet, removing the override (`DELETE …?package=hotcrm.custom`, "Restore package original") brings the base's back. `content/docs/api/metadata-api.mdx`: a capability is not copied under a new name (its registry entry has `allowRuntimeCreate: false`); it changes only with a new version of the package that declares it, and an `overrides` on a type that takes none is refused `403 NOT_OVERRIDABLE` with that type's own route. This PR made the old sentences false, so they are fixed here. ## Tests - `packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts`: `mapping` joins the sealed served types (by-name, lists, layered, per-package, after a restart, with and without a metadata service). New describe `[#30062] a pair's removal of its own override brings the base's item back`, over view, dashboard, report, email_template, page, app, dataset, doc, book, mapping, flow and translation, on both kernels: (1) the removal naming the pair succeeds, the pair row is gone, and every read serves the base's item again; (2) a removal naming the base keeps main's exact answer per kernel and type, removes nothing, and the override stays served. - `packages/objectql/src/protocol-flow-custom-override.test.ts`: the old flow-only admission pins (a mapping pair-row delete 403, a view delete `override-artifact`) are rewritten to the new behaviour (removed; `runtime-only`). They pinned the #28412 flow-only scope (contract review 6559358939), which item 2 supersedes. - `packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts`: each of the 17 excluded non-D10 types cites its own basis, never reads "a" before a vowel, names a create only where the registry admits one, and the named remedies (sharing rule route and switch, Setup › Queues, Setup › Datasources, object extension, new version of the base) appear where they belong. - `packages/metadata-protocol/src/protocol-package-bindings.sealed-override.test.ts`: the served set includes `mapping`. - `ui/packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx`: for every overridable type plus permission, object and api, under both overlay flags, the tile reads 可定制 exactly when a door offers Customize; flow and app read 可定制, doc and mapping 只读, a writable flow 可新增; the flow list header renders 可定制. ### Ablations `scripts/ablation-replace.mjs` (wrap mode); for objectql legs the mutated `metadata-protocol` was rebuilt and `scripts/ablation-dist-preflight.mjs` proved the marker in `dist/` before the run, then absent after restore. Each leg restored with blob == HEAD and `git diff HEAD` empty. | # | Mutation | Result | |:--|:--|:--| | A1 | `isPairRowRemoval` back to flow-only | 7 / 78 red: doc and mapping pair removals on both kernels, the mapping pair-row delete, the view intent | | A2 | `mapping` out of `SEALED_ROW_SERVED_OVERRIDE_TYPES` | 6 / 71 red: mapping served with and without a metadata service, mapping removal and base-named control on both kernels | | A3 | the pair check widened to admit a base-named removal | 2 / 71 red: doc and mapping, environment kernel, base-named removal | | A4 | `typeStateWord` back to reading `allowEnvironmentOverlay` | 15 / 114 red: every per-type agreement row, flow/app, the flow header | | A5 | the refusal back to the old "a TYPE … (ADR-0146 D6) … create your own" sentence | 16 / 87 red: 14 per-type citation rows, the named-remedy row, the code-only row | ## Acceptance notes - **Item 3 (label precedence) is not here.** Ruled (6631250894): a translation in the pair, then the override's own label, then the base's translation. Measured on `origin/main` `b7075d99`: the rule that keeps an override's own label over the base's translation exists only in `translateView`, `translateDashboard`, `translateReport` and `translateObject` (`packages/spec/src/system/i18n-resolver.ts`, the `packagedBase` option), and `@objectstack/rest` wires `PACKAGED_BASE_ACCESSORS` (`meta-item-read-gate.ts:2954`) for object, dashboard, view and report only. `translatePage` (`:3393`), `translateDataset` (`:2270`), `translateApp` (`:1756`) and `translateBook` (`:2441`) take only `ResolveOptions` and apply the catalog translation unconditionally, which is the card's page/dataset defect. The ruling's first rank is unmet for every type: where the value differs from the base's, the resolver keeps the authored value, so a translation in the pair never gets in. Both halves change `packages/spec/src/**`, so the seat splits the spec half to the spec lane; the server half (`getPackaged*Base` accessors and REST table rows) stays on #30062. - **Interaction with #30063 and #30064 (in-flight `domain:ui` cards).** Both add a door to `STUDIO_CUSTOMIZE_TYPES` (page and dataset). The new agreement rows derive from that list, so they follow on their own. The older #29705 pins in the same test file hard-code `page` 只读 (the tile at `:129`, the list header at `:178`–`:186`); whichever of this PR and #30063 lands second flips those to 可定制. #30064's claim names "All metadata tile for datasets": after this PR the tile follows the door with no extra change. - **The flow-only removal scope of #28412 is superseded** for the pair's own row only; a base-named removal is unchanged on both kernels and pinned per type. - **Translation** in the removal pins: the manifest door the harness boots through registers no translation artifact (a code package's translation file does, through the metadata plugin), so the base's bundle is registered with `engine.registry.registerItem('translation', …, BASE)`, and the removal pin reads the base's bundle back from there. - Serial constraint: #30106 (`protocol-served-overrides.ts`, read path) landed as PR #30207 (02:52Z) and is in `origin/main` `bb86ff96c`, merged at head `c5033471`. - **Merge-queue round (head `c5033471`).** Merged `origin/main` `bb86ff96c` after #30063 (page door, `797f9257c`) and #30064 (dataset door, `AREA_CUSTOMIZE_EDITOR_TYPES`, `6f095b9c1`) landed: `studioOffersCustomize` also reads `AREA_CUSTOMIZE_EDITOR_TYPES`, and `DirectoryPage.stateVocabulary-29705.test.tsx`'s 'a read-only package: overlay types can be customized…', 'the view list header reads 可定制, the page list header 只读' and 'typeStateWord: the mapping in every context' now expect page 可定制 (filter count 隐藏只读 (4); the read-only control row is `api`), per #30062 item 4 and ADR-0146 D2. This is the "whichever lands second flips those" case noted above. Local: that file 114/114, the 6 files that read PageShell/DirectoryPage 150/150, app-shell type-check 0. ## Verification Head `69812d8212` (base `b7075d99`, `origin/main` `474c030b` merged in once, no conflict, no rebase). The affected closure was rebuilt after the merge under the shared lock. - `pnpm --filter @objectstack/metadata-protocol test`: 312 files passed (4 skipped), 22 375 tests passed (26 skipped). `typecheck`: exit 0. - `pnpm --filter @objectstack/objectql test`: 548 / 548 files, 10 910 / 10 910 tests. `typecheck`: exit 0. - `@objectstack/runtime` `src/domains/meta-object-extension-item-door-fit.test.ts`: 6 / 6 (it pins the permission refusal sentence byte for byte). - `ui`: the six test files that import `PageShell` or `DirectoryPage` (`DirectoryPage.stateVocabulary-29705`, `.diagnosticsLinkTone-12768`, `.environmentOverlay-12141`, `PageShell.narrowHeader-12306`, `metadataAreaHost.customizations-28568`, `metadataAreaHost.scope-28561`): 149 / 149. `turbo run type-check --filter=@object-ui/app-shell`: 68 / 68 tasks. eslint on the two ui files: 0 errors (one more `react-refresh/only-export-components` warning, for the new `studioOffersCustomize` export beside the already-exported `typeStateWord`). - `pnpm lint`: exit 0. `node scripts/check-nul-bytes.mjs`: exit 0. <details><summary>Gates: dispatch-gates --commands, each exit code captured before any pipe</summary> 95 derived families at head `5663983b` (the docs commit added 29 docs families and 7 doc-reading families): 94 exit 0, 1 NOT MEASURED. `dispatch-gates --ran` exit 0. (At `69812d82`: 65 derived, 64 exit 0, 1 NOT MEASURED.) - NOT MEASURED: `pnpm check:dual-build-cjs-loads` exit 3, "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/" (32 unbuilt packages outside this diff's closure); left to CI. - `node scripts/check-plugin-teardown-shape.mjs --self-test` first exited 3 on the shallow clone (pinned fixture commit unreachable); after `git fetch origin 621a487…` it exits 0 (48 cases). - `pnpm check:type-check-debt` first exited 3 (lock queue timeout, nothing measured); re-run under the lock: exit 0, "0 raw tsc error(s) total, none above its recorded number". - Also run, outside the derivation: `check:authz-resolver`, `check:error-code-casing` (both keep a roster under `packages/`), `check:overlay-whitelist-table`, `check:changeset-fixed`: all exit 0. </details> Change set: 15 paths, +461 / −66 against `origin/main` (13 code and test paths +448 / −61, plus the two docs pages). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [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: n/a. - [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 (the maintainer brief is Chinese by the seat's convention). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CaHjE2N5aEp4ogQnWY)_ <!-- signed: domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef2ad31 commit 15f808a

15 files changed

Lines changed: 485 additions & 72 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@object-ui/app-shell': minor
4+
---
5+
6+
fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062)
7+
8+
Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer.
9+
Metadata-protocol: semantic
10+
11+
- **A mapping override in `<base>.custom` is served.** A mapping item that declares `overrides: { package: '<base>', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=<base>`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping.
12+
- **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=<base>.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=<base>`) is unchanged: the pair's row stays and its override stays served.
13+
- **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged.
14+
- **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag.
15+
16+
The label precedence of a translated override (card item 3) is not in this change.

‎content/docs/api/metadata-api.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ declaring `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (`basedOn` is the
105105
`manifest.version` of `hotcrm` it was taken from, recorded and never judged). Only the pair declares an override: an item of any other
106106
package that depends on `hotcrm` and bears a `hotcrm` item's name is refused at save
107107
and publish, and its fix is a new name. A permission set, position or capability is
108-
never overridden; it is copied under a new name. What the reads then serve is in
108+
never overridden: a permission set or position is copied under a new name, and a
109+
capability changes only with a new version of the package that declares it. An
110+
`overrides` on a type that takes none is refused `403 NOT_OVERRIDABLE`, and the
111+
sentence names that type's own route. What the reads then serve is in
109112
[Metadata lifecycle](/docs/concepts/metadata-lifecycle#customizing-an-installed-package-the-paired-custom-override-adr-0146).
110113
`POST /packages/hotcrm/ensure-customization` creates the pair when absent (Studio calls
111114
it on the first change), and `GET /packages/hotcrm/customizations` lists its overrides,

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,15 @@ document **whole** for every read that serves the base's name
128128
override, `effective` = the override), the list scoped to the base and the
129129
environment-wide list all serve the override, stamped `_packageId: 'hotcrm'`. The
130130
declaration counts only from the pair — the stored row's own `package_id` must be
131-
`hotcrm.custom` — and today only for a `view`, `dashboard`, `report`, `email_template`,
132-
`translation` or `flow`: the rest of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
133-
— `page`, `app`, `dataset`, `doc`, `book`, `mapping` and `action` — waits for
134-
its doors and is not served yet. A declaration from any other package is ignored.
131+
`hotcrm.custom` — and for every type of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
132+
but `action`: a `view`, `dashboard`, `report`, `email_template`, `translation`, `flow`,
133+
`page`, `app`, `dataset`, `doc`, `book` or `mapping`. An `action` override waits for
134+
ADR-0146 D9's conditions and is not served yet. A declaration from any other package is ignored.
135+
The import door and the connector pull read a named `mapping` through the by-name read,
136+
so they run the pair's mapping.
137+
- **Removing the override brings the base's back.** `DELETE /meta/:type/:name?package=hotcrm.custom`
138+
("Restore package original") deletes the pair's row, for every type above, and every read
139+
then serves the base's document again. A `DELETE` naming `hotcrm` removes nothing of the pair's.
135140
- **A new locale is not an override.** An `email_template` the pair saves at a locale the
136141
base does not ship is the pair's own member, served under `hotcrm.custom`, and carries no
137142
`overrides`; declaring one there is refused (`403 NOT_OVERRIDABLE` at the save, publish

‎packages/metadata-protocol/src/protocol-diff-delete-load.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,12 @@ export class ProtocolDiffDeleteLoadFacet {
401401
// here, but a stored row under its name is, as repair.
402402
const originGatedRefusal = host.originGatedRemovalRefusal(request.type, request.name);
403403
// [#28412, ADR-0146 D9 item 1] A removal naming the shipping package's
404-
// pair, of a type whose D9 conditions are delivered (`flow` only —
405-
// every other open type keeps main's verdict): it removes
406-
// the row the override is stored in, and the code package's item is
407-
// served — and armed — again. Admitted past the sealed-removal refusal
408-
// and told to the store as a runtime-only removal, as the declared
409-
// save is ({@link isPairRowRemoval}).
404+
// pair, of a type whose declared override the gate admits ([#30062]
405+
// every open type, no longer `flow` alone): it removes the row the
406+
// override is stored in, and the code package's item is served (a
407+
// flow's armed) again. Admitted past the sealed-removal refusal and
408+
// told to the store as a runtime-only removal, as the declared save
409+
// is ({@link isPairRowRemoval}).
410410
const pairRowRemoval = isPairRowRemoval(
411411
request.type, host.packagedArtifactOwner({ type: request.type, name: request.name }), request.packageId,
412412
);
@@ -755,10 +755,10 @@ export class ProtocolDiffDeleteLoadFacet {
755755
// #5927 — was an inline `this.isArtifactBacked(...)` call
756756
// with these exact arguments; now reads the method-scoped
757757
// binding the receipts share. Same fact, one call fewer.
758-
// [#28412] The pair's row of a type whose D9 conditions
759-
// are delivered (`flow` only, {@link isPairRowRemoval})
758+
// [#28412] The pair's row of a declared override
759+
// ({@link isPairRowRemoval}; [#30062] every open type)
760760
// is removed as the declared save wrote it: runtime-only.
761-
// Every other type keeps the intent main gave it.
761+
// Every other removal keeps the intent main gave it.
762762
intent: artifactBacked && !pairRowRemoval
763763
? 'override-artifact'
764764
: 'runtime-only',

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ export function declinesStoredRow(
150150
if (typeof name !== 'string' || name === '') return false;
151151
if (this.isDeclaredCodeDatasource(type, name)) return true;
152152
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
153+
// [#29613, ADR-0146 D8 item 2] …except, for a page, app, dataset, doc,
154+
// book or [#30062] mapping, the shipping package's declared override: judged as the flow's
155155
// above, on the row's own `package_id` and body, against the package
156156
// that ships the name. The reads then serve it from its row, as they
157157
// serve a dashboard's. Every other sealed row of the name stays declined.

‎packages/metadata-protocol/src/protocol-package-bindings.sealed-override.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import { describe, expect, it } from 'vitest';
1616
import { declinesBorrowedEnvelope, servesSealedOverrideRow } from './protocol-package-bindings.js';
1717

18-
const ROW_SERVED = ['page', 'app', 'dataset', 'doc', 'book'] as const;
18+
/** [#30062] `mapping` the sixth: its reads were measured and its published override was served by none. */
19+
const ROW_SERVED = ['page', 'app', 'dataset', 'doc', 'book', 'mapping'] as const;
1920
/** The base's code artifact the hydrator's lookup falls back to for a pair's row. */
2021
const BASE_ARTIFACT = { _packageId: 'com.example.showcase' };
2122

@@ -30,7 +31,7 @@ describe('[#29613] the sealed row-served override types', () => {
3031
expect(servesSealedOverrideRow('apps')).toBe(true);
3132
});
3233

33-
it.each(['mapping', 'action', 'flow', 'object', 'permission', 'view', 'dashboard', 'report', 'email_template'])(
34+
it.each(['action', 'flow', 'object', 'permission', 'view', 'dashboard', 'report', 'email_template'])(
3435
'%s: not a sealed row-served override type',
3536
(type) => {
3637
expect(servesSealedOverrideRow(type)).toBe(false);

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

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,9 @@ function boundPackageOf(packageId: unknown): string | undefined {
307307
* (#22057, `anotherPackageShips`).
308308
*
309309
* ⛔ 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,
310+
* {@link SEALED_ROW_SERVED_OVERRIDE_TYPES} ([#30062] `mapping` the sixth)
311+
* since their reads serve a `.custom` pair's declared override from its row,
312+
* as these three types' reads do. `flow` registers with no artifact envelope at all (Regime C,
313313
* #20913), and the boot pull arms its declared override from the bare slot.
314314
*/
315315
const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
@@ -322,16 +322,19 @@ const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
322322
'dataset',
323323
'doc',
324324
'book',
325+
// [#30062] An import mapping's declared override, served from its row as the five above.
326+
'mapping',
325327
]);
326328

327329
/**
328330
* [#29613, ADR-0146 D8 item 2] The SEALED types (ADR-0131 D6: a registry
329331
* entry with `allowEnvironmentOverlay: false`) whose stored row the reads
330332
* 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}).
333+
* ({@link isServedOverrideRowOf}): `page`, `app`, `dataset`, `doc`, `book`
334+
* and [#30062] `mapping`, the overridable types of ADR-0146 D2 that open no
335+
* environment overlay and wait for no D9 condition. Every other stored row of
336+
* a name a managed package ships stays unserved for them
337+
* ({@link isUnservedSealedOverlay}).
335338
*
336339
* Before, the reads declined the override's row with the rest, and the
337340
* override was served only from the registry's bare slot, where the hydrator
@@ -340,14 +343,19 @@ const BORROWED_ENVELOPE_DECLINED_TYPES: ReadonlySet<string> = new Set([
340343
* instead, while the lists answered the override, and the layered read's
341344
* effective layer and the pair's per-package layer never saw it.
342345
*
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+
* [#30062] `mapping` joined once its reads were measured: before, a pair's
347+
* mapping override saved and published, and every read (the by-name read the
348+
* import door resolves a mapping through, `@objectstack/rest`'s
349+
* `import-mapping.ts`, included) answered the base's body, so a published
350+
* override did nothing.
351+
*
352+
* ⛔ A closed list, judged on the canonical type. `action` and `flow` wait for
353+
* ADR-0146 D9's conditions (`flow` has its own admission,
346354
* `declinesStoredRow`'s first branch). Every type here is also in
347355
* {@link BORROWED_ENVELOPE_DECLINED_TYPES}: the bare entry served no reader
348356
* the row does not serve now.
349357
*/
350-
const SEALED_ROW_SERVED_OVERRIDE_TYPES: ReadonlySet<string> = new Set(['page', 'app', 'dataset', 'doc', 'book']);
358+
const SEALED_ROW_SERVED_OVERRIDE_TYPES: ReadonlySet<string> = new Set(['page', 'app', 'dataset', 'doc', 'book', 'mapping']);
351359

352360
/** [#29613] Whether `type` is one of {@link SEALED_ROW_SERVED_OVERRIDE_TYPES}, folded to the canonical singular. */
353361
function servesSealedOverrideRow(type: string): boolean {

0 commit comments

Comments
 (0)