From 1208111091af5715501ae58c6ec9e95a9e2e246c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:46:50 +0000 Subject: [PATCH 1/3] fix(rest,runtime): the published door asks declinesStoredRow, so a code-defined datasource's stored row is no longer served `ObjectStackProtocolImplementation.declinesStoredRow` becomes public, and both `GET /meta/:type/:name/published` doors (RestServer and the runtime dispatcher) ask it in place of `isShippedFlowName`. For a shipped flow name the answer is unchanged; for a code-defined datasource name with a stored row the doors now serve the layered read's effective layer, the code definition, as the by-name read, the list and /layers already do. A runtime datasource's stored row is still served, and a protocol without the predicate still gets the stored row. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU --- ...ocol.declines-stored-row-published.test.ts | 86 +++++++++++++ packages/metadata-protocol/src/protocol.ts | 12 +- .../rest/src/meta-published-overlay.test.ts | 113 +++++++++++++++++- packages/rest/src/rest-server.ts | 30 +++-- .../meta-published-runtime-publish.test.ts | 110 ++++++++++++++++- packages/runtime/src/domains/meta.ts | 36 +++--- 6 files changed, 352 insertions(+), 35 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts diff --git a/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts b/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts new file mode 100644 index 00000000000..5173a9092c4 --- /dev/null +++ b/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21986] `declinesStoredRow` is PUBLIC on `ObjectStackProtocolImplementation`, + * so the published-snapshot doors (`GET /meta/:type/:name/published`, the REST + * route and its dispatcher twin) can ask it instead of `isShippedFlowName`. + * + * What a door relies on, pinned here once: the one predicate answers BOTH name + * classes whose stored row the active reads decline, and nothing else. + * + * - a FLOW name the loader's set holds (`isShippedFlowName`'s answer); + * - a CODE-DEFINED DATASOURCE name: one an installed package declares, or one + * the host registers from code (`code-datasource-names`, here `default`). + * + * Every other name answers false: a flow no package ships, a runtime + * datasource, the same name under another type, a missing or empty name. + * + * The calls below go through the class's own declared type, never an `any` + * cast, so `tsc --noEmit` over this file (the package's `typecheck` includes + * `src/**`) fails if the member stops being public. + */ +import { describe, expect, it } from 'vitest'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +const FLOW_PACKAGE = 'com.example.pkg'; +const SHIPPED_FLOW = 'pkg_flow'; +const CUSTOMER_FLOW = 'customer_flow'; +const CODE_DS = 'showcase_external'; +const HOST_DS = 'default'; +const RUNTIME_DS = 'rt_datasource_21986'; + +/** + * A partial registry: the loader's entry for {@link SHIPPED_FLOW} carries its + * package id (the shape `lookupArtifactItem` reads off a registry with no + * `getArtifactItem`), and one installed package declares {@link CODE_DS}. The + * services registry carries the host's code-datasource set. + */ +function makeProtocol(): ObjectStackProtocolImplementation { + const loaderEntry = { name: SHIPPED_FLOW, label: 'Loader', _packageId: FLOW_PACKAGE }; + const engine = { + registry: { + getItem: (type: string, name: string) => + (type === 'flow' || type === 'flows') && name === SHIPPED_FLOW ? loaderEntry : undefined, + getAllPackages: () => [{ + manifest: { id: 'com.example.showcase', datasources: [{ name: CODE_DS, driver: 'sqlite', config: {} }] }, + }], + }, + }; + const services = new Map([['code-datasource-names', new Set([HOST_DS])]]); + return new ObjectStackProtocolImplementation(engine as never, () => services); +} + +describe('[#21986] the published predicate: declinesStoredRow answers both name classes, and only those', () => { + it('a shipped flow name, in either type spelling — what isShippedFlowName answers', () => { + const protocol = makeProtocol(); + for (const type of ['flow', 'flows']) { + expect(protocol.isShippedFlowName(type, SHIPPED_FLOW), type).toBe(true); + expect(protocol.declinesStoredRow(type, SHIPPED_FLOW), type).toBe(true); + } + }); + + it('a code-defined datasource name: one a package declares, and one the host registers from code', () => { + const protocol = makeProtocol(); + for (const type of ['datasource', 'datasources']) { + expect(protocol.declinesStoredRow(type, CODE_DS), type).toBe(true); + expect(protocol.declinesStoredRow(type, HOST_DS), type).toBe(true); + // Not a flow answer: the datasource half is its own. + expect(protocol.isShippedFlowName(type, CODE_DS), type).toBe(false); + } + }); + + it('control: every other name keeps its stored row', () => { + const protocol = makeProtocol(); + expect(protocol.declinesStoredRow('flow', CUSTOMER_FLOW)).toBe(false); + expect(protocol.declinesStoredRow('datasource', RUNTIME_DS)).toBe(false); + // The same names under another type. + expect(protocol.declinesStoredRow('object', CODE_DS)).toBe(false); + expect(protocol.declinesStoredRow('object', HOST_DS)).toBe(false); + expect(protocol.declinesStoredRow('view', SHIPPED_FLOW)).toBe(false); + // A name that is not one. + for (const name of [undefined, null, '', 42]) { + expect(protocol.declinesStoredRow('datasource', name), String(name)).toBe(false); + expect(protocol.declinesStoredRow('flow', name), String(name)).toBe(false); + } + }); +}); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 177e9170e4d..73efe2ab80f 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -16806,8 +16806,18 @@ export class ObjectStackProtocolImplementation implements * (`storedRowServed`, the fact {@link servedLockState}'s `deletable` * reads), the `_lock` gate's overlay layer ({@link overlayLockLayerAt}, * read whether or not the row is adopted), and the DELETE's own row probe. + * + * [#21986] PUBLIC so a door that serves a stored row out of the layered + * read can ASK it, never re-derive it: `GET /meta/:type/:name/published` + * (the REST route and its dispatcher twin) reads {@link getMetaItemLayered} + * and, when a stored row is present and this predicate holds for the + * answer's `type` and `name`, serves the `effective` layer (the loader's + * body, or the code definition) instead of the row. Every other stored row + * is served as before. A door asks this method alone: ⛔ no door restates + * either half, or the host's code-datasource set. A write door does not + * ask it; the writes keep their own refusals. */ - private declinesStoredRow(type: string, name: unknown): boolean { + declinesStoredRow(type: string, name: unknown): boolean { if (this.isShippedFlowName(type, name)) return true; return typeof name === 'string' && name !== '' && this.isDeclaredCodeDatasource(type, name); } diff --git a/packages/rest/src/meta-published-overlay.test.ts b/packages/rest/src/meta-published-overlay.test.ts index 5e03deb47af..0121e06c1af 100644 --- a/packages/rest/src/meta-published-overlay.test.ts +++ b/packages/rest/src/meta-published-overlay.test.ts @@ -586,11 +586,14 @@ describe('[#21002] the published door follows the layered read for a shipped flo it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => { const { engine, metadata, protocol } = shippedHarness(); await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED')); - // The same protocol with the predicate hidden from the door only; its - // own methods keep calling it on the real instance. + // The same protocol with the predicate the door asks hidden from the + // door only; its own methods keep calling it on the real instance. + // [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName` + // stays visible, so this also pins that the door does not fall back + // to it: a protocol without the one predicate gets the stored row. const withoutPredicate = new Proxy(protocol, { get(target, key) { - if (key === 'isShippedFlowName') return undefined; + if (key === 'declinesStoredRow') return undefined; const value = Reflect.get(target, key); return typeof value === 'function' ? value.bind(target) : value; }, @@ -602,3 +605,107 @@ describe('[#21002] the published door follows the layered read for a shipped flo expect(res.body).toMatchObject({ name: SHIPPED, label: 'STORED' }); }, 60_000); }); + +/** + * [#21986] The other name class whose stored row the layered read declines: a + * CODE-DEFINED DATASOURCE name (here one an installed package declares). The + * by-name read, the list and the layered read's effective layer serve the code + * definition, the MetadataService's registration, and the stored row is + * residue, still reported in `overlay`. This door asks the protocol's one + * predicate for both name classes, `declinesStoredRow`, with the answer's own + * `type` / `name`, so it serves that effective layer too. A runtime + * datasource's stored row is still served. + * + * The cold-boot reading over the real showcase composition is recorded on the + * PR; the predicate's two halves are pinned in `metadata-protocol`. + */ +describe('[#21986] the published door serves a code-defined datasource\'s code definition over a stored row', () => { + const CODE_DS = 'showcase_external'; + const RUNTIME_DS = 'rt_datasource_21986'; + const CODE_LABEL = 'External Analytics (SQLite)'; + const SHADOW_LABEL = 'Shadow 21986'; + const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({ + name, label, driver: 'sqlite', config: { filename }, origin, + }); + + /** + * The file's engine double, with an installed package that declares + * {@link CODE_DS}, and the MetadataService holding the code definition the + * runtime registers at boot (and a copy of the runtime datasource under a + * label its row does not carry, so the control can tell which layer answered). + */ + async function datasourceHarness() { + const { engine, rows } = makeStubEngine(); + engine.registry = { + registerItem: () => {}, + registerObject: () => {}, + getPackage: () => undefined, + getItem: () => undefined, + getAllPackages: () => [{ + manifest: { + id: 'com.example.showcase', + datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }], + }, + }], + }; + const metadata = new MetadataManager({}); + await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`)); + await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db')); + const protocol = makeProtocol(engine, metadata); + return { engine, rows, metadata, protocol }; + } + + async function storeActiveRow(engine: any, name: string, body: unknown) { + await engine.insert('sys_metadata', { + type: 'datasource', name, organization_id: null, package_id: null, state: 'active', + metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1, + }); + } + + /** + * {@link setup}, read by a caller the `/meta` doors admit to a datasource + * read: `datasource` reads require `manage_platform_settings`, the + * capability the datasource admin door requires. + */ + function setupAsPlatformAdmin(protocol: unknown, metadata: unknown) { + const rest = setup(protocol, metadata); + (rest as any).resolveExecCtx = async () => ({ userId: 'u_platform', systemPermissions: ['manage_platform_settings'] }); + return rest; + } + + it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => { + const { engine, rows, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db')); + expect(rows.size).toBe(1); + + // The decision this door follows: a stored layer IS present, and the + // layered read put the code definition over it. + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS }); + expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL }); + expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL }); + + const rest = setupAsPlatformAdmin(protocol, metadata); + for (const type of ['datasource', 'datasources']) { + const res = await callPublished(rest, { type, name: CODE_DS }); + + expect(res.statusCode, type).toBe(200); + expect(res.body, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL }); + expect(JSON.stringify(res.body), type).not.toContain('shadow-external.db'); + expect(res.body, type).toEqual(layered.effective); + } + // The row stays at rest: the door read past it, nothing removed it. + expect(rows.size).toBe(1); + }, 60_000); + + it('control: a runtime datasource\'s stored row is still what the door serves', async () => { + const { engine, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db')); + + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS }); + const res = await callPublished(setupAsPlatformAdmin(protocol, metadata), { type: 'datasource', name: RUNTIME_DS }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' }); + expect(res.body).toEqual(layered.overlay); + }, 60_000); +}); diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index b0076412c6f..bca1481694b 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -8621,24 +8621,30 @@ export class RestServer { : {}), }); if (layered?.overlay !== undefined && layered?.overlay !== null) { - // [#21002, ADR-0126 §2] When the layered - // read put the LOADER's body over this stored - // row — a shipped flow name, decided by the - // protocol's `isShippedFlowName` — this door - // serves that effective layer, not the row: - // `flow` is Regime C, "never an overlay read - // path". The predicate is ASKED of its owner - // with the answer's own `type` / `name`, - // never re-derived here, so this door and + // [#21002, #21986, ADR-0126 §2, ADR-0062 D4] + // When the layered read put a code layer + // over this stored row, this door serves + // that effective layer, not the row. The + // protocol decides it with one predicate, + // `declinesStoredRow`, for both name + // classes: a shipped flow name (the + // loader's body; `flow` is Regime C, + // "never an overlay read path") and a + // code-defined datasource name (the code + // definition; "code wins on collision"). + // The predicate is ASKED of its owner with + // the answer's own `type` / `name`, never + // re-derived here, so this door, the + // by-name read, the list and // `getMetaItemLayered` read one rule. Every // other stored row is served exactly as // before — an `object` too, whose effective // layer differs from its row by folding, not // by this decision — and so is every row of a // protocol that brings no such predicate. - const shippedFlow: { isShippedFlowName?(type: string, name: unknown): boolean } = publishedProtocol; - publishedOverlay = typeof shippedFlow.isShippedFlowName === 'function' - && shippedFlow.isShippedFlowName(layered.type, layered.name) + const decliner: { declinesStoredRow?(type: string, name: unknown): boolean } = publishedProtocol; + publishedOverlay = typeof decliner.declinesStoredRow === 'function' + && decliner.declinesStoredRow(layered.type, layered.name) ? layered.effective : layered.overlay; } diff --git a/packages/runtime/src/domains/meta-published-runtime-publish.test.ts b/packages/runtime/src/domains/meta-published-runtime-publish.test.ts index be231e603a6..18a5aaa93ca 100644 --- a/packages/runtime/src/domains/meta-published-runtime-publish.test.ts +++ b/packages/runtime/src/domains/meta-published-runtime-publish.test.ts @@ -440,11 +440,14 @@ describe('[#21002] the dispatcher published door follows the layered read for a it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => { const { engine, metadata, protocol } = shippedHarness(); await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED')); - // The same protocol with the predicate hidden from the door only; its - // own methods keep calling it on the real instance. + // The same protocol with the predicate the door asks hidden from the + // door only; its own methods keep calling it on the real instance. + // [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName` + // stays visible, so this also pins that the door does not fall back + // to it: a protocol without the one predicate gets the stored row. const withoutPredicate = new Proxy(protocol, { get(target, key) { - if (key === 'isShippedFlowName') return undefined; + if (key === 'declinesStoredRow') return undefined; const value = Reflect.get(target, key); return typeof value === 'function' ? value.bind(target) : value; }, @@ -457,3 +460,104 @@ describe('[#21002] the dispatcher published door follows the layered read for a expect(response.body.data).toMatchObject({ name: SHIPPED, label: 'STORED' }); }); }); + +/** + * [#21986] The dispatcher twin, for the other name class whose stored row the + * layered read declines: a CODE-DEFINED DATASOURCE name (here one an installed + * package declares). The layered read's effective layer for such a name is the + * code definition, the MetadataService's registration, and the stored row is + * residue, still reported in `overlay`. This door asks the protocol's one + * predicate for both name classes, `declinesStoredRow`, so it serves that + * effective layer, as `RestServer`'s `/published` does. A runtime datasource's + * stored row is still served. + */ +describe('[#21986] the dispatcher published door serves a code-defined datasource\'s code definition over a stored row', () => { + const CODE_DS = 'showcase_external'; + const RUNTIME_DS = 'rt_datasource_21986'; + const CODE_LABEL = 'External Analytics (SQLite)'; + const SHADOW_LABEL = 'Shadow 21986'; + const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({ + name, label, driver: 'sqlite', config: { filename }, origin, + }); + + /** + * This file's engine double, with an installed package that declares + * {@link CODE_DS}, and the MetadataService holding the code definition the + * runtime registers at boot (and a copy of the runtime datasource under a + * label its row does not carry, so the control can tell which layer answered). + */ + async function datasourceHarness() { + const { engine, rows } = makeStubEngine(); + engine.registry = { + registerItem: () => {}, + registerObject: () => {}, + getPackage: () => undefined, + getItem: () => undefined, + getAllPackages: () => [{ + manifest: { + id: 'com.example.showcase', + datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }], + }, + }], + }; + const metadata = new MetadataManager({}); + await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`)); + await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db')); + const protocol = makeProtocol(engine, metadata); + return { engine, rows, metadata, protocol }; + } + + async function storeActiveRow(engine: any, name: string, body: unknown) { + await engine.insert('sys_metadata', { + type: 'datasource', name, organization_id: null, package_id: null, state: 'active', + metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1, + }); + } + + /** + * {@link ctx}, for a caller the `/meta` doors admit to a datasource read: + * `datasource` reads require `manage_platform_settings`, the capability + * the datasource admin door requires. + */ + const platformAdminCtx = (): any => ({ + ...ctx(), + executionContext: { userId: 'u_platform', systemPermissions: ['manage_platform_settings'] }, + }); + + it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => { + const { engine, rows, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db')); + expect(rows.size).toBe(1); + + // The decision this door follows: a stored layer IS present, and the + // layered read put the code definition over it. + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS }); + expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL }); + expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL }); + + const dispatcher = make({ protocol, metadata }); + for (const type of ['datasource', 'datasources']) { + const response = responseOf(await dispatcher.handleMetadata(`/${type}/${CODE_DS}/published`, platformAdminCtx(), 'GET')); + + expect(response.status, type).toBe(200); + expect(response.body.data, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL }); + expect(JSON.stringify(response.body.data), type).not.toContain('shadow-external.db'); + expect(response.body.data, type).toEqual(layered.effective); + } + // The row stays at rest: the door read past it, nothing removed it. + expect(rows.size).toBe(1); + }); + + it('control: a runtime datasource\'s stored row is still what the door serves', async () => { + const { engine, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db')); + + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS }); + const response = responseOf(await make({ protocol, metadata }) + .handleMetadata(`/datasource/${RUNTIME_DS}/published`, platformAdminCtx(), 'GET')); + + expect(response.status).toBe(200); + expect(response.body.data).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' }); + expect(response.body.data).toEqual(layered.overlay); + }); +}); diff --git a/packages/runtime/src/domains/meta.ts b/packages/runtime/src/domains/meta.ts index f3f2cb560cc..65882c0e5f0 100644 --- a/packages/runtime/src/domains/meta.ts +++ b/packages/runtime/src/domains/meta.ts @@ -43,8 +43,8 @@ import { // which is the whole reason `MetaDomainProtocol` below is `Pick`ed rather than // written out. Same move `domains/packages.ts` and `domains/mcp.ts` make. import type { MetadataProtocol } from '@objectstack/spec/api'; -// [#21002] The implementation class, for the ONE member it declares that this -// domain asks (`isShippedFlowName`) — `Pick`ed below, never restated. +// [#21002, #21986] The implementation class, for the ONE member it declares +// that this domain asks (`declinesStoredRow`) — `Pick`ed below, never restated. import type { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; // [#20193] THE per-caller read gate of a `/meta/:type/:name` document — the one // `RestServer` asks, published by `@objectstack/rest` so this transport asks it @@ -152,8 +152,9 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry. * `any` requests — nothing declares its type, and the only thing that branch * asks of it is whether it is `undefined`. * - * [#21002] A third group: `isShippedFlowName`, the predicate the layered read - * decides its effective layer with, which `/published` asks so it follows that + * [#21002, #21986] A third group: `declinesStoredRow`, the one predicate the + * layered read decides its effective layer with (a shipped flow name, or a + * code-defined datasource name), which `/published` asks so it follows that * decision. Its signature is DECLARED — on `ObjectStackProtocolImplementation` * itself — so it is `Pick`ed from that class, never restated: a rename at the * producer is a compile error here, the same move `domains/automation.ts` @@ -162,7 +163,7 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry. export type MetaDomainProtocol = Partial> - & Partial> + & Partial> & { /** ⚠️ Undeclared request shapes — see "Where the ledger honestly ends". */ listDrafts?(request: any): Promise; @@ -1179,17 +1180,20 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin ...(organizationId ? { organizationId } : {}), }); if (layered?.overlay !== undefined && layered?.overlay !== null) { - // [#21002, ADR-0126 §2] As `RestServer`'s `/published`: when - // the layered read put the LOADER's body over this stored - // row — a shipped flow name, decided by the protocol's - // `isShippedFlowName`, asked with the answer's own `type` / - // `name` and never re-derived here — serve that effective - // layer, not the row (`flow` is Regime C, "never an overlay - // read path"). Every other stored row, an `object`'s - // included, and every row of a protocol without the - // predicate, is served exactly as before. - publishedOverlay = typeof protocol.isShippedFlowName === 'function' - && protocol.isShippedFlowName(layered.type, layered.name) + // [#21002, #21986, ADR-0126 §2, ADR-0062 D4] As + // `RestServer`'s `/published`: when the layered read put a + // code layer over this stored row — a shipped flow name (the + // loader's body; `flow` is Regime C, "never an overlay read + // path") or a code-defined datasource name (the code + // definition; "code wins on collision"), decided by the + // protocol's one predicate, `declinesStoredRow`, asked with + // the answer's own `type` / `name` and never re-derived + // here — serve that effective layer, not the row. Every + // other stored row, an `object`'s included, and every row of + // a protocol without the predicate, is served exactly as + // before. + publishedOverlay = typeof protocol.declinesStoredRow === 'function' + && protocol.declinesStoredRow(layered.type, layered.name) ? layered.effective : layered.overlay; } From 77c8e7aaadd34553890668881719df23bdcfb344 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:47:00 +0000 Subject: [PATCH 2/3] docs(metadata-protocol): isShippedFlowName's doc comment no longer says the published doors ask it alone The comment ships in the built declarations and stated that the published doors ask this predicate alone and still serve a code-defined datasource's stored row. They now ask declinesStoredRow; the paragraph says so. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU --- packages/metadata-protocol/src/protocol.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 73efe2ab80f..92d8053a6a5 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -16746,10 +16746,9 @@ export class ObjectStackProtocolImplementation implements * * [#21922] The layered read asks this predicate through * {@link declinesStoredRow}, which also declines the stored row of a - * code-defined datasource name. The published doors ask this predicate - * alone, so for such a name they still serve the stored row: the active - * overlay row, as the route's spec describes it. That door is not moved - * here. + * code-defined datasource name. [#21986] The published doors ask + * {@link declinesStoredRow} in its place, so for such a name they serve + * the code definition too. */ isShippedFlowName(type: string, name: unknown): boolean { if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false; From 697066637f81fb5fb812821f2df681266e632a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:48:00 +0000 Subject: [PATCH 3/3] chore(changeset): metadata-protocol minor for the public declinesStoredRow, rest and runtime patch for their published doors Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU --- ...6-metadata-protocol-declines-stored-row-public.md | 12 ++++++++++++ .../21986-rest-published-door-code-datasource.md | 10 ++++++++++ .../21986-runtime-published-door-code-datasource.md | 10 ++++++++++ 3 files changed, 32 insertions(+) create mode 100644 .changeset/21986-metadata-protocol-declines-stored-row-public.md create mode 100644 .changeset/21986-rest-published-door-code-datasource.md create mode 100644 .changeset/21986-runtime-published-door-code-datasource.md diff --git a/.changeset/21986-metadata-protocol-declines-stored-row-public.md b/.changeset/21986-metadata-protocol-declines-stored-row-public.md new file mode 100644 index 00000000000..79fad4e69de --- /dev/null +++ b/.changeset/21986-metadata-protocol-declines-stored-row-public.md @@ -0,0 +1,12 @@ +--- +"@objectstack/metadata-protocol": minor +--- + +`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 + +Clause-②: yes (widening) + +- 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`. +- 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. +- `isShippedFlowName` stays public and unchanged. +- 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. diff --git a/.changeset/21986-rest-published-door-code-datasource.md b/.changeset/21986-rest-published-door-code-datasource.md new file mode 100644 index 00000000000..95f62eea4f8 --- /dev/null +++ b/.changeset/21986-rest-published-door-code-datasource.md @@ -0,0 +1,10 @@ +--- +"@objectstack/rest": patch +--- + +`GET /api/v1/meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists + +Clause-②: no + +- 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. +- 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. diff --git a/.changeset/21986-runtime-published-door-code-datasource.md b/.changeset/21986-runtime-published-door-code-datasource.md new file mode 100644 index 00000000000..d721fe19249 --- /dev/null +++ b/.changeset/21986-runtime-published-door-code-datasource.md @@ -0,0 +1,10 @@ +--- +"@objectstack/runtime": patch +--- + +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 + +Clause-②: no + +- 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. +- 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`.