Skip to content

Commit 1fb274e

Browse files
fix(rest,runtime): the published door serves a code-defined datasource's code definition over a stored row (declinesStoredRow made public) (#22001)
Fixes #21986 Clause-②: yes (widening) ## Summary `GET /api/v1/meta/datasource/:name/published` served a stored `sys_metadata` row under a code-defined datasource name, while `GET /api/v1/meta/datasource/:name`, the `/meta/datasource` list and `/layers` all served the code definition. The protocol makes that decision with one predicate for both name classes, `declinesStoredRow`, but it was `private`, so both published doors asked `isShippedFlowName` alone. Triage direction A (`6016753988`), as claimed in `6017296526`: - `@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation.declinesStoredRow(type, name)` is now public, under the same name. Its doc comment says who may ask it: a door that serves a stored row out of the layered read. `isDeclaredCodeDatasource` and `isStoredEntryOfDeclinedName` stay private. - `@objectstack/rest` (`rest-server.ts`, the `publishedOverlay` branch) and `@objectstack/runtime` (`domains/meta.ts`, the `Pick` and the published branch) ask `declinesStoredRow` in place of `isShippedFlowName`, in the same duck-typed `typeof … === 'function'` shape. No door restates `isDeclaredCodeDatasource` or the host's code-datasource set. - A protocol without `declinesStoredRow` gets the stored row, as before. The door does not fall back to `isShippedFlowName`, and the existing no-predicate controls now hide `declinesStoredRow` (with `isShippedFlowName` still visible) to pin that. - `isShippedFlowName` stays public and unchanged in behaviour. ## Reproduction, before and after **Door level, at base `aa09db58c9`.** The new pin in each door's test file runs the real `ObjectStackProtocolImplementation` and the real `MetadataManager`, with a package that declares `showcase_external` and a stored row labelled `Shadow 21986`. Both doors failed the same way: ``` AssertionError: datasource: expected { name: 'showcase_external', …(4) } to match object { name: 'showcase_external', …(2) } - "label": "External Analytics (SQLite)", + "label": "Shadow 21986", - "origin": "code", + "origin": "runtime", ``` The pin's own precondition passed on the base: `getMetaItemLayered` answered `overlay` = the row and `effective` = the code definition. **Real showcase composition.** This used a throwaway `bootStack` probe that was never committed. It stored a row under `showcase_external` through the `/meta` repository (label `PROBE SHADOW`, `origin: runtime`, its own file), restarted, and then read: | read | base `aa09db58c9` | this branch | |:--|:--|:--| | `/meta/datasource/showcase_external/published` | 200, `PROBE SHADOW`, `origin: runtime`, `probe-shadow.db` | 200, `External Analytics (SQLite)`, `origin: code`, `.objectstack/data/showcase_external.db` | | `/meta/datasource/showcase_external` | the code definition | the code definition | | `/layers` `effective` | the code definition | the code definition | | `/layers` `overlay` | the row, `overlayScope: env` | the row, `overlayScope: env` | The order's mechanism check was what `layered.effective` IS for a code-defined datasource with a stored row. It is the code definition, measured in both harnesses above. So the door now serves the same body as the by-name read. A serve-shaped composition was also probed on this branch, with `MetadataPlugin` composed as `objectstack serve` does. There `/published` answers the same code definition before the row exists and after the row plus a restart. The lean harness answers `501 NOT_IMPLEMENTED` on `/published` before any row exists. That is the harness's own recorded degradation: it has no `getPublished`-capable metadata service, as `meta-published-and-state-routes.dogfood.test.ts` states. It is not in this card's scope. ## Pins - `packages/rest/src/meta-published-overlay.test.ts`, new block `[#21986]`: - a stored row under a code-defined datasource name: the door answers the code definition, which is the layered `effective` layer, for both `datasource` and `datasources`, and the row stays at rest; - control: a runtime datasource's stored row is still what the door serves (`toEqual(layered.overlay)`). - `packages/runtime/src/domains/meta-published-runtime-publish.test.ts`: the same two cases on the dispatcher twin. - `packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts` (new) pins that the published predicate answers both name classes (a shipped flow; a package-declared datasource and the host's `default`, in both spellings) and nothing else (an unshipped flow, a runtime datasource, the same names under another type, and a missing or empty name). It calls the method through the class's declared type, so the package's `tsc --noEmit`, which includes this file (`--listFiles`: 1 hit), fails if the member stops being public. - The shipped-flow pins stay green through the switch. The only edit to them is the no-predicate control in each door file, which now hides the predicate the door asks. - The datasource reads need `manage_platform_settings` (`META_TYPE_READ_CAPABILITIES`), so the new door cases read as a caller that holds it. - No dogfood pin is added. Each door's answer is fully determined by the real protocol's layered read plus the predicate, and the unit pins exercise both unmocked. The composition reading above was taken once and is recorded here. `datasource-restore-code-wins.dogfood.test.ts` already pins the by-name read and the list over the real composition after a restart. ## Ablation (each door's pin goes red with the switch reverted) Run at `69706663`, through `scripts/ablation-replace.mjs` in wrap mode. The merge of `origin/main` after it did not touch either door file. The door subjects are imported from `src` (`./rest-server.js`, and `../http-dispatcher.js` into `domains/meta.ts`), so no rebuild sits on the path. - REST: anchor `decliner.declinesStoredRow(layered.type, layered.name)` replaced by `(decliner as any).isShippedFlowName(layered.type, layered.name)`. Anchor 1 to 0, blob `bca14816` to `8b422cf0`. Result: `Tests 1 failed | 15 passed (16)`. The failing case was the `[#21986]` main case (received `Shadow 21986` / `origin: runtime`). Every shipped-flow pin stayed green. Restored: blob equals HEAD (`bca14816`), and `git diff HEAD` is empty. - Runtime: anchor `protocol.declinesStoredRow(layered.type, layered.name)` replaced the same way. Anchor 1 to 0, blob `65882c0e` to `f78f1e7f`. Result: `Tests 1 failed | 11 passed (12)`, on the same case. Restored: blob equals HEAD (`65882c0e`), and `git diff HEAD` is empty. - Cross-package type, reverse leg: the runtime `Pick` key was replaced by `'isDeclaredCodeDatasource'` (still private). `tsc --noEmit` went red with `TS2344: Type '"isDeclaredCodeDatasource"' does not satisfy the constraint 'keyof ObjectStackProtocolImplementation'`, while `'declinesStoredRow'` in the same position typechecks green. So the typecheck reads the rebuilt `.d.ts`. Restored: blob equals HEAD. ## Public surface for the contract review: the built `.d.ts` `ObjectStackProtocolImplementation` members were read with the TypeScript parser from the BUILT `packages/metadata-protocol/dist/index.d.ts` and `index.d.cts`, before (base) and after (this branch). Full declarations, whitespace collapsed, sorted, and split public / non-public: - public members: 65 before, 66 after. The `.d.ts` and `.d.cts` lists are byte-identical in both runs, and the after list is unchanged when rebuilt at `5e185d57`, after the merge. - the whole set difference, order-insensitive: - added public: `declinesStoredRow(type: string, name: unknown): boolean;` - removed non-public: `private declinesStoredRow;` - nothing else moves. ## `isShippedFlowName` census after the switch (source, not tests) - Calls: only inside the class. `packages/metadata-protocol/src/protocol.ts:16769` (`isStoredFlowEntryOfShippedName`, private) and `:16820` (`declinesStoredRow`). - Doc links in the same file: `:8976`, `:10119`, `:10904`, `:16760` and `:16785`. Also the invariant text of `scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.json`, which is not a reader. - `@objectstack/rest` and `@objectstack/runtime`: 0 readers. - `../objectui` at `9dfaca654`: 0 hits. The control `getMetaItemLayered` hits there, so the grep runs. - Readers outside the class are tests only: `protocol.flow-by-name-shipped-name.test.ts`, `protocol.flow-layered-shipped-name.test.ts`, `protocol.declines-stored-row-published.test.ts`, `meta-published-overlay.test.ts`, `meta-published-runtime-publish.test.ts` and `flow-shipped-name-published-door.dogfood.test.ts`. - So nothing in these two repositories needs it public any more. It is not retired here, as ruled. The `cloud` repository was not read. ## Tests and gates All of these were run at HEAD `5e185d57`, which is this branch after merging `origin/main` `6befe19c`. That merge added one `metadata-protocol` commit, which does not touch `protocol.ts`. The suites below were also green at `69706663` before the merge. - `pnpm --filter @objectstack/metadata-protocol test`: `Test Files 219 passed | 3 skipped (222)`, `Tests 28045 passed | 19 skipped (28064)`. - `pnpm --filter @objectstack/rest test`: `Test Files 260 passed (260)`, `Tests 4914 passed | 326 skipped (5240)`. Its `test:repo` project: `5 passed (5)`, `177 passed | 1 skipped`. - `pnpm --filter @objectstack/runtime test`: `Test Files 331 passed (331)`, `Tests 4670 passed | 19 skipped (4689)`. Its `test:repo` project: `3 passed (3)`, `751 passed`. - Typecheck of all three packages: exit 0. For rest and runtime this includes `check:test-typecheck` (rest: 0 held files; runtime: the existing ledger, unchanged). - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 66 families over the 9 changed paths, the same list the order carried. All 66 were run at `5e185d57` with their exit codes recorded: 66 exit 0. The tool reconciled them: `66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN`. - Before the merge, at `69706663`, `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`, because eight unrelated packages had no `dist/`. Once those were built it passed, and at `5e185d57` it passed on the first run: `106 published require entry point(s) across 66 package(s) load`. - `pnpm lint` (`eslint . --no-inline-config`, the whole repository, not narrowed): exit 0 at `5e185d57`. - `check-changeset-no-major --base origin/main`: `This diff introduces no major bump`. The level axis was also driven offline, with `--event` naming a payload that carries this body: `LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package … at minor or above` (`@objectstack/metadata-protocol: minor`). ## Acceptance notes - **Deviation from the claim's file surface, declared here (commit `77c8e7aa`, droppable on its own).** The claim says no other line of `protocol.ts` moves. But `isShippedFlowName`'s doc comment ships in the built `.d.ts`, and it stated: "The published doors ask this predicate alone, so for such a name they still serve the stored row … That door is not moved here." This PR makes that false. So that one paragraph now reads "[#21986] The published doors ask declinesStoredRow in its place, so for such a name they serve the code definition too." The diff is 3 lines added and 4 removed, comment only. It is a separate commit so the `domain:engine` seat can drop it if it rules otherwise. - **Changesets and the lockstep group.** `@objectstack/metadata-protocol` is `minor` (the public method, `Clause-②: yes (widening)`). `@objectstack/rest` and `@objectstack/runtime` are each `patch` (`Clause-②: no`): each published door stops serving such a row. All three packages sit in the one `fixed` group in `.changeset/config.json`, so the release versions rest and runtime at the group's minor anyway. The patch files carry their own changelog text. - The pending `.changeset/21922-metadata-protocol-meta-read-declines-code-datasource-row.md` still says "Not moved: `GET /api/v1/meta/datasource/:name/published` still serves the stored row". That was true of its own change. This PR's rest changeset states the door's new answer, and that other PR's changeset is left untouched. - Observation, not filed: the `GetPublishedMetaItemResponseSchema` JSDoc in `packages/spec/src/api/protocol.zod.ts` describes the route's producers as "the `state:'active'` overlay row via `getMetaItemLayered`, else … `getPublished`". Since the shipped-flow change, and now for code-defined datasources too, the layered producer can hand back the `effective` layer instead of the row. This is comment-only drift on a `packages/spec` path, which is outside this lane. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 803764a commit 1fb274e

9 files changed

Lines changed: 387 additions & 39 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
`ObjectStackProtocolImplementation.declinesStoredRow(type, name)` is now public, so a door that serves a stored row out of the layered read can ask the same decision the reads make
6+
7+
Clause-②: yes (widening)
8+
9+
- The method answers `true` for exactly the names whose stored `sys_metadata` row the active reads (`getMetaItem`, the list, and the `effective` layer of `getMetaItemLayered`) do not adopt: a flow name a managed package ships (the answer `isShippedFlowName` gives), and a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`). Every other type and name answers `false`.
10+
- The `GET /meta/:type/:name/published` doors in `@objectstack/rest` and `@objectstack/runtime` now ask this method in place of `isShippedFlowName`. A door asks it, and does not restate either half or the host's code-datasource set.
11+
- `isShippedFlowName` stays public and unchanged.
12+
- The only change to the public surface is this one added method. No signature, schema or accept set changes, and no behaviour of this package changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
`GET /api/v1/meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists
6+
7+
Clause-②: no
8+
9+
- For a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`), the published door now serves the layered read's `effective` layer, which is the code definition. Before this change it served the leftover stored `sys_metadata` row, with that row's label, `origin` and connection settings, while `GET /api/v1/meta/datasource/:name`, the `/meta/datasource` list and `/layers` all served the code definition. The door now asks the protocol's `declinesStoredRow`, the one decision those reads make, in place of `isShippedFlowName`. A shipped flow name is answered as before.
10+
- Unchanged: a runtime datasource's stored row is still what the door serves, and so is every stored row of every other type. A protocol that does not provide `declinesStoredRow` still gets the stored row. The row itself stays at rest, `/layers` still reports it in `overlay`, and `DELETE /api/v1/meta/datasource/:name` still removes it as the repair.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
The runtime dispatcher's `GET /meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists
6+
7+
Clause-②: no
8+
9+
- This is the dispatcher twin of the `@objectstack/rest` published door, and it now answers the same way. For a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`), the door serves the layered read's `effective` layer, which is the code definition, instead of the leftover stored row. It asks the protocol's `declinesStoredRow` in place of `isShippedFlowName`. A shipped flow name is answered as before.
10+
- Unchanged: a runtime datasource's stored row, and every stored row of every other type, is served as before. So is every row when the protocol does not provide `declinesStoredRow`.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#21986] `declinesStoredRow` is PUBLIC on `ObjectStackProtocolImplementation`,
5+
* so the published-snapshot doors (`GET /meta/:type/:name/published`, the REST
6+
* route and its dispatcher twin) can ask it instead of `isShippedFlowName`.
7+
*
8+
* What a door relies on, pinned here once: the one predicate answers BOTH name
9+
* classes whose stored row the active reads decline, and nothing else.
10+
*
11+
* - a FLOW name the loader's set holds (`isShippedFlowName`'s answer);
12+
* - a CODE-DEFINED DATASOURCE name: one an installed package declares, or one
13+
* the host registers from code (`code-datasource-names`, here `default`).
14+
*
15+
* Every other name answers false: a flow no package ships, a runtime
16+
* datasource, the same name under another type, a missing or empty name.
17+
*
18+
* The calls below go through the class's own declared type, never an `any`
19+
* cast, so `tsc --noEmit` over this file (the package's `typecheck` includes
20+
* `src/**`) fails if the member stops being public.
21+
*/
22+
import { describe, expect, it } from 'vitest';
23+
import { ObjectStackProtocolImplementation } from './protocol.js';
24+
25+
const FLOW_PACKAGE = 'com.example.pkg';
26+
const SHIPPED_FLOW = 'pkg_flow';
27+
const CUSTOMER_FLOW = 'customer_flow';
28+
const CODE_DS = 'showcase_external';
29+
const HOST_DS = 'default';
30+
const RUNTIME_DS = 'rt_datasource_21986';
31+
32+
/**
33+
* A partial registry: the loader's entry for {@link SHIPPED_FLOW} carries its
34+
* package id (the shape `lookupArtifactItem` reads off a registry with no
35+
* `getArtifactItem`), and one installed package declares {@link CODE_DS}. The
36+
* services registry carries the host's code-datasource set.
37+
*/
38+
function makeProtocol(): ObjectStackProtocolImplementation {
39+
const loaderEntry = { name: SHIPPED_FLOW, label: 'Loader', _packageId: FLOW_PACKAGE };
40+
const engine = {
41+
registry: {
42+
getItem: (type: string, name: string) =>
43+
(type === 'flow' || type === 'flows') && name === SHIPPED_FLOW ? loaderEntry : undefined,
44+
getAllPackages: () => [{
45+
manifest: { id: 'com.example.showcase', datasources: [{ name: CODE_DS, driver: 'sqlite', config: {} }] },
46+
}],
47+
},
48+
};
49+
const services = new Map<string, unknown>([['code-datasource-names', new Set([HOST_DS])]]);
50+
return new ObjectStackProtocolImplementation(engine as never, () => services);
51+
}
52+
53+
describe('[#21986] the published predicate: declinesStoredRow answers both name classes, and only those', () => {
54+
it('a shipped flow name, in either type spelling — what isShippedFlowName answers', () => {
55+
const protocol = makeProtocol();
56+
for (const type of ['flow', 'flows']) {
57+
expect(protocol.isShippedFlowName(type, SHIPPED_FLOW), type).toBe(true);
58+
expect(protocol.declinesStoredRow(type, SHIPPED_FLOW), type).toBe(true);
59+
}
60+
});
61+
62+
it('a code-defined datasource name: one a package declares, and one the host registers from code', () => {
63+
const protocol = makeProtocol();
64+
for (const type of ['datasource', 'datasources']) {
65+
expect(protocol.declinesStoredRow(type, CODE_DS), type).toBe(true);
66+
expect(protocol.declinesStoredRow(type, HOST_DS), type).toBe(true);
67+
// Not a flow answer: the datasource half is its own.
68+
expect(protocol.isShippedFlowName(type, CODE_DS), type).toBe(false);
69+
}
70+
});
71+
72+
it('control: every other name keeps its stored row', () => {
73+
const protocol = makeProtocol();
74+
expect(protocol.declinesStoredRow('flow', CUSTOMER_FLOW)).toBe(false);
75+
expect(protocol.declinesStoredRow('datasource', RUNTIME_DS)).toBe(false);
76+
// The same names under another type.
77+
expect(protocol.declinesStoredRow('object', CODE_DS)).toBe(false);
78+
expect(protocol.declinesStoredRow('object', HOST_DS)).toBe(false);
79+
expect(protocol.declinesStoredRow('view', SHIPPED_FLOW)).toBe(false);
80+
// A name that is not one.
81+
for (const name of [undefined, null, '', 42]) {
82+
expect(protocol.declinesStoredRow('datasource', name), String(name)).toBe(false);
83+
expect(protocol.declinesStoredRow('flow', name), String(name)).toBe(false);
84+
}
85+
});
86+
});

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16746,10 +16746,9 @@ export class ObjectStackProtocolImplementation implements
1674616746
*
1674716747
* [#21922] The layered read asks this predicate through
1674816748
* {@link declinesStoredRow}, which also declines the stored row of a
16749-
* code-defined datasource name. The published doors ask this predicate
16750-
* alone, so for such a name they still serve the stored row: the active
16751-
* overlay row, as the route's spec describes it. That door is not moved
16752-
* here.
16749+
* code-defined datasource name. [#21986] The published doors ask
16750+
* {@link declinesStoredRow} in its place, so for such a name they serve
16751+
* the code definition too.
1675316752
*/
1675416753
isShippedFlowName(type: string, name: unknown): boolean {
1675516754
if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false;
@@ -16806,8 +16805,18 @@ export class ObjectStackProtocolImplementation implements
1680616805
* (`storedRowServed`, the fact {@link servedLockState}'s `deletable`
1680716806
* reads), the `_lock` gate's overlay layer ({@link overlayLockLayerAt},
1680816807
* read whether or not the row is adopted), and the DELETE's own row probe.
16808+
*
16809+
* [#21986] PUBLIC so a door that serves a stored row out of the layered
16810+
* read can ASK it, never re-derive it: `GET /meta/:type/:name/published`
16811+
* (the REST route and its dispatcher twin) reads {@link getMetaItemLayered}
16812+
* and, when a stored row is present and this predicate holds for the
16813+
* answer's `type` and `name`, serves the `effective` layer (the loader's
16814+
* body, or the code definition) instead of the row. Every other stored row
16815+
* is served as before. A door asks this method alone: ⛔ no door restates
16816+
* either half, or the host's code-datasource set. A write door does not
16817+
* ask it; the writes keep their own refusals.
1680916818
*/
16810-
private declinesStoredRow(type: string, name: unknown): boolean {
16819+
declinesStoredRow(type: string, name: unknown): boolean {
1681116820
if (this.isShippedFlowName(type, name)) return true;
1681216821
return typeof name === 'string' && name !== '' && this.isDeclaredCodeDatasource(type, name);
1681316822
}

‎packages/rest/src/meta-published-overlay.test.ts‎

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -586,11 +586,14 @@ describe('[#21002] the published door follows the layered read for a shipped flo
586586
it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => {
587587
const { engine, metadata, protocol } = shippedHarness();
588588
await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED'));
589-
// The same protocol with the predicate hidden from the door only; its
590-
// own methods keep calling it on the real instance.
589+
// The same protocol with the predicate the door asks hidden from the
590+
// door only; its own methods keep calling it on the real instance.
591+
// [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName`
592+
// stays visible, so this also pins that the door does not fall back
593+
// to it: a protocol without the one predicate gets the stored row.
591594
const withoutPredicate = new Proxy(protocol, {
592595
get(target, key) {
593-
if (key === 'isShippedFlowName') return undefined;
596+
if (key === 'declinesStoredRow') return undefined;
594597
const value = Reflect.get(target, key);
595598
return typeof value === 'function' ? value.bind(target) : value;
596599
},
@@ -602,3 +605,107 @@ describe('[#21002] the published door follows the layered read for a shipped flo
602605
expect(res.body).toMatchObject({ name: SHIPPED, label: 'STORED' });
603606
}, 60_000);
604607
});
608+
609+
/**
610+
* [#21986] The other name class whose stored row the layered read declines: a
611+
* CODE-DEFINED DATASOURCE name (here one an installed package declares). The
612+
* by-name read, the list and the layered read's effective layer serve the code
613+
* definition, the MetadataService's registration, and the stored row is
614+
* residue, still reported in `overlay`. This door asks the protocol's one
615+
* predicate for both name classes, `declinesStoredRow`, with the answer's own
616+
* `type` / `name`, so it serves that effective layer too. A runtime
617+
* datasource's stored row is still served.
618+
*
619+
* The cold-boot reading over the real showcase composition is recorded on the
620+
* PR; the predicate's two halves are pinned in `metadata-protocol`.
621+
*/
622+
describe('[#21986] the published door serves a code-defined datasource\'s code definition over a stored row', () => {
623+
const CODE_DS = 'showcase_external';
624+
const RUNTIME_DS = 'rt_datasource_21986';
625+
const CODE_LABEL = 'External Analytics (SQLite)';
626+
const SHADOW_LABEL = 'Shadow 21986';
627+
const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({
628+
name, label, driver: 'sqlite', config: { filename }, origin,
629+
});
630+
631+
/**
632+
* The file's engine double, with an installed package that declares
633+
* {@link CODE_DS}, and the MetadataService holding the code definition the
634+
* runtime registers at boot (and a copy of the runtime datasource under a
635+
* label its row does not carry, so the control can tell which layer answered).
636+
*/
637+
async function datasourceHarness() {
638+
const { engine, rows } = makeStubEngine();
639+
engine.registry = {
640+
registerItem: () => {},
641+
registerObject: () => {},
642+
getPackage: () => undefined,
643+
getItem: () => undefined,
644+
getAllPackages: () => [{
645+
manifest: {
646+
id: 'com.example.showcase',
647+
datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }],
648+
},
649+
}],
650+
};
651+
const metadata = new MetadataManager({});
652+
await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`));
653+
await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db'));
654+
const protocol = makeProtocol(engine, metadata);
655+
return { engine, rows, metadata, protocol };
656+
}
657+
658+
async function storeActiveRow(engine: any, name: string, body: unknown) {
659+
await engine.insert('sys_metadata', {
660+
type: 'datasource', name, organization_id: null, package_id: null, state: 'active',
661+
metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1,
662+
});
663+
}
664+
665+
/**
666+
* {@link setup}, read by a caller the `/meta` doors admit to a datasource
667+
* read: `datasource` reads require `manage_platform_settings`, the
668+
* capability the datasource admin door requires.
669+
*/
670+
function setupAsPlatformAdmin(protocol: unknown, metadata: unknown) {
671+
const rest = setup(protocol, metadata);
672+
(rest as any).resolveExecCtx = async () => ({ userId: 'u_platform', systemPermissions: ['manage_platform_settings'] });
673+
return rest;
674+
}
675+
676+
it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => {
677+
const { engine, rows, metadata, protocol } = await datasourceHarness();
678+
await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db'));
679+
expect(rows.size).toBe(1);
680+
681+
// The decision this door follows: a stored layer IS present, and the
682+
// layered read put the code definition over it.
683+
const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS });
684+
expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL });
685+
expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL });
686+
687+
const rest = setupAsPlatformAdmin(protocol, metadata);
688+
for (const type of ['datasource', 'datasources']) {
689+
const res = await callPublished(rest, { type, name: CODE_DS });
690+
691+
expect(res.statusCode, type).toBe(200);
692+
expect(res.body, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL });
693+
expect(JSON.stringify(res.body), type).not.toContain('shadow-external.db');
694+
expect(res.body, type).toEqual(layered.effective);
695+
}
696+
// The row stays at rest: the door read past it, nothing removed it.
697+
expect(rows.size).toBe(1);
698+
}, 60_000);
699+
700+
it('control: a runtime datasource\'s stored row is still what the door serves', async () => {
701+
const { engine, metadata, protocol } = await datasourceHarness();
702+
await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db'));
703+
704+
const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS });
705+
const res = await callPublished(setupAsPlatformAdmin(protocol, metadata), { type: 'datasource', name: RUNTIME_DS });
706+
707+
expect(res.statusCode).toBe(200);
708+
expect(res.body).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' });
709+
expect(res.body).toEqual(layered.overlay);
710+
}, 60_000);
711+
});

‎packages/rest/src/rest-server.ts‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8621,24 +8621,30 @@ export class RestServer {
86218621
: {}),
86228622
});
86238623
if (layered?.overlay !== undefined && layered?.overlay !== null) {
8624-
// [#21002, ADR-0126 §2] When the layered
8625-
// read put the LOADER's body over this stored
8626-
// row — a shipped flow name, decided by the
8627-
// protocol's `isShippedFlowName` — this door
8628-
// serves that effective layer, not the row:
8629-
// `flow` is Regime C, "never an overlay read
8630-
// path". The predicate is ASKED of its owner
8631-
// with the answer's own `type` / `name`,
8632-
// never re-derived here, so this door and
8624+
// [#21002, #21986, ADR-0126 §2, ADR-0062 D4]
8625+
// When the layered read put a code layer
8626+
// over this stored row, this door serves
8627+
// that effective layer, not the row. The
8628+
// protocol decides it with one predicate,
8629+
// `declinesStoredRow`, for both name
8630+
// classes: a shipped flow name (the
8631+
// loader's body; `flow` is Regime C,
8632+
// "never an overlay read path") and a
8633+
// code-defined datasource name (the code
8634+
// definition; "code wins on collision").
8635+
// The predicate is ASKED of its owner with
8636+
// the answer's own `type` / `name`, never
8637+
// re-derived here, so this door, the
8638+
// by-name read, the list and
86338639
// `getMetaItemLayered` read one rule. Every
86348640
// other stored row is served exactly as
86358641
// before — an `object` too, whose effective
86368642
// layer differs from its row by folding, not
86378643
// by this decision — and so is every row of a
86388644
// protocol that brings no such predicate.
8639-
const shippedFlow: { isShippedFlowName?(type: string, name: unknown): boolean } = publishedProtocol;
8640-
publishedOverlay = typeof shippedFlow.isShippedFlowName === 'function'
8641-
&& shippedFlow.isShippedFlowName(layered.type, layered.name)
8645+
const decliner: { declinesStoredRow?(type: string, name: unknown): boolean } = publishedProtocol;
8646+
publishedOverlay = typeof decliner.declinesStoredRow === 'function'
8647+
&& decliner.declinesStoredRow(layered.type, layered.name)
86428648
? layered.effective
86438649
: layered.overlay;
86448650
}

0 commit comments

Comments
 (0)