From d95251a8b099fac56a8441ff99fb809a6dbe51b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 10:19:32 +0000 Subject: [PATCH 1/2] fix(metadata-protocol): the layered read's code layer skips a hydrated stored row, so a name no package ships answers a null code layer on both sides of a hydration (#21059) getMetaItemLayered's code-layer fallback read the registry's plain slot for items registered at runtime with no package. A stored row the hydrator registers lands in the same slot, so once a hydration had run the code layer was the stored body and the lock flags were derived from it, while the same read before the hydration answered a null code layer. The fallback now discards a registry answer that carries the tenant marker the hydrator already writes on every row it registers (ADR-0010 tenant provenance, read through isTenantAuthored). No new marker. A stored body with package-provenance stamps under an unshipped name is the same row: no code layer, per triage's ruling. A runtime-registered item with no package keeps its code layer. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude --- .../21059-layered-code-null-unshipped.md | 13 + ...otocol.layered-code-unshipped-name.test.ts | 336 ++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 35 +- ...nshipped-name-layered-code.dogfood.test.ts | 273 ++++++++++++++ 4 files changed, 655 insertions(+), 2 deletions(-) create mode 100644 .changeset/21059-layered-code-null-unshipped.md create mode 100644 packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts create mode 100644 packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts diff --git a/.changeset/21059-layered-code-null-unshipped.md b/.changeset/21059-layered-code-null-unshipped.md new file mode 100644 index 00000000000..93e9e3bf9fd --- /dev/null +++ b/.changeset/21059-layered-code-null-unshipped.md @@ -0,0 +1,13 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +fix(metadata-protocol): the layered read's code layer is empty for an item no package ships, whether or not a stored copy of it has been loaded into the registry (#21059) + +Clause-②: no + +The layered read, `GET /api/v1/meta/:type/:name/layers`, reports an item's code layer as the packaged definition, and as empty when no package ships the item and it exists only as a stored customization. For an item no package ships, it answered that correctly only until the stored copy had been loaded into the in-memory registry, for example by a restart's startup load or by a list read. After that, the code layer answered the stored copy, and the lock and provenance flags were derived from it. So the same read of the same item gave two answers, depending on what had been loaded. + +The code layer now skips a stored copy the registry holds, using the tenant-authorship mark the startup load already puts on every stored copy it registers. An item no package ships has an empty code layer before and after the load, and its flags come from the stored layer both times. This includes a stored copy whose own content claims a package's provenance: a claim is not a packaged definition. The deprecated layers flag on the by-name read, `GET /api/v1/meta/:type/:name`, answers the same. + +Packaged items keep their packaged code layer. An item registered at runtime with no package keeps its code layer, as before. The stored rows are not changed. diff --git a/packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts b/packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts new file mode 100644 index 00000000000..506db4db41b --- /dev/null +++ b/packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts @@ -0,0 +1,336 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21059] The layered read's code layer is the packaged artifact, or an item + * registered at runtime with no package — never a stored row that a hydration + * put in the registry's plain slot. For a name no package ships, the read + * answers a null code layer before a hydration and after it alike. + * + * The spec's layer 1 reads "`null` when no artifact ships this item (it exists + * only as an overlay)", and the method's own #5707 / #5840 rule allows a layer + * only from a read that happened. The fallback below the artifact lookup reads + * the registry's plain slot, which it does for items registered at runtime + * without a package; a hydrated stored row lands in that same slot. So once a + * hydration had run, the code layer was the stored body and the lock flags were + * resolved from it, while the same read before the hydration answered a null + * code layer. The discriminator is the tenant marker the hydrator already + * writes on every row it registers; nothing new is stamped. + * + * Triage's ruling on the open corner is pinned as well: a stored body that + * carries package-provenance stamps under a name no package ships is not a code + * layer either, so the lock-state resolver sees no code layer for it and its + * flags are the same before and after a hydration. + * + * The registry double is the one `protocol.flow-layered-shipped-name.test.ts` + * uses: the real `SchemaRegistry`'s key shapes (`:` for a loader + * entry, the bare name for everything else), its `getItem` precedence (the bare + * slot first) and its artifact lookup. `registerItem` stamps a package entry the + * way `applyProtection` does and leaves a bare registration alone. + * `@objectstack/objectql` cannot be imported here: it depends on this package. + * The cold-boot proof over the real composition is + * `packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts`. + * + * Controls: a shipped name keeps the loader's body as its code layer on both + * sides of a hydration, and an item registered at runtime with no package keeps + * its code layer — the fallback's stated purpose. + */ +import { describe, expect, it } from 'vitest'; +import { assertEngineFindOnePredicate, isCodeArtifactBody } from '@objectstack/metadata-core'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +const PACKAGE_ID = 'com.example.pkg'; +const SHIPPED = 'pkg_flow'; +const UNSHIPPED = 'customer_flow'; +const STAMPED = 'stamped_flow'; +const RUNTIME = 'runtime_flow'; +const UNSHIPPED_VIEW = 'customer_view'; + +const flowBody = (name: string, label: string, extra: Record = {}) => ({ + name, + label, + type: 'autolaunched', + nodes: [ + { id: 'start', type: 'start', label: 'Start' }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [{ id: 'e1', source: 'start', target: 'end' }], + ...extra, +}); + +/** A body that claims a package's provenance under a name that package does not ship. */ +const stampedBody = (name: string) => flowBody(name, 'STAMPED', { + _packageId: PACKAGE_ID, + _packageVersion: '1.0.0', + _provenance: 'package', +}); + +interface StoredRow { + id: string; + type: string; + name: string; + organization_id: string | null; + package_id: string | null; + state: string; + metadata: string; +} + +const storedRow = (type: string, name: string, body: unknown, extra: Partial = {}): StoredRow => ({ + id: `r_${type}_${name}_${extra.organization_id ?? 'env'}`, + type, + name, + organization_id: null, + package_id: null, + state: 'active', + metadata: JSON.stringify(body), + ...extra, +}); + +/** The registry double — see the header. */ +function registryDouble() { + const byType = new Map>>(); + const collection = (type: string) => { + if (!byType.has(type)) byType.set(type, new Map()); + return byType.get(type)!; + }; + return { + registerItem(type: string, item: Record, keyField = 'name', packageId?: string) { + const name = String(item[keyField]); + if (packageId) { + if (item._packageId === undefined) item._packageId = packageId; + if (item._provenance === undefined) item._provenance = 'package'; + collection(type).set(`${packageId}:${name}`, item); + } else { + collection(type).set(name, item); + } + }, + listItems(type: string, packageId?: string) { + const all = [...(byType.get(type)?.values() ?? [])]; + return packageId ? all.filter((it) => it._packageId === packageId) : all; + }, + /** The real precedence: the bare slot, then prefer-local, then the first composite. */ + getItem(type: string, name: string, packageId?: string) { + const entries = byType.get(type); + if (!entries) return undefined; + const direct = entries.get(name); + if (direct) return direct; + if (packageId) { + const local = entries.get(`${packageId}:${name}`); + if (local) return local; + } + for (const [key, item] of entries) if (key.endsWith(`:${name}`)) return item; + return undefined; + }, + getArtifactItem(type: string, name: string, packageId?: string) { + const entries = [...(byType.get(type)?.entries() ?? [])]; + const scoped = entries.filter(([key, it]) => key.endsWith(`:${name}`) && isCodeArtifactBody(it)); + const local = packageId ? scoped.find(([, it]) => it._packageId === packageId) : undefined; + if (local) return local[1]; + if (scoped[0]) return scoped[0][1]; + const bare = byType.get(type)?.get(name); + return bare && isCodeArtifactBody(bare) && (!packageId || bare._packageId === packageId) ? bare : undefined; + }, + bare(type: string, name: string) { + return byType.get(type)?.get(name); + }, + getObject: () => undefined, + registerObject: () => undefined, + getPackage: () => undefined, + isPackageDisabled: () => false, + applyNavContributions: (app: unknown) => app, + }; +} + +/** + * The engine double: `find` / `findOne` over `sys_metadata` rows, plus the + * registry. ⛔ No `insert` / `update` / `delete` — the paths under test (the + * layered read and the two hydrations) issue no write verb. + */ +function harness(rows: StoredRow[]) { + const registry = registryDouble(); + // What the loader registered: one packaged flow. + registry.registerItem('flow', flowBody(SHIPPED, 'LOADER'), 'name', PACKAGE_ID); + const matching = (where: Record) => { + for (const k of Object.keys(where)) { + if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`); + } + return rows.filter((r) => + Object.entries(where).every(([k, v]) => v === undefined || (r as unknown as Record)[k] === v), + ); + }; + const engine: any = { + async find(table: string, opts?: { where?: Record; limit?: number }) { + if (table !== 'sys_metadata') return []; + const matched = matching(opts?.where ?? {}); + // `check:objectql-double-limit` — the caller's bound, applied after the filter. + return opts?.limit === undefined ? matched : matched.slice(0, opts.limit); + }, + async findOne(table: string, opts?: { where?: Record }) { + // `check:engine-double-contract` — refuses what the real engine refuses. + assertEngineFindOnePredicate(table, opts); + if (table !== 'sys_metadata') return null; + return matching(opts?.where ?? {})[0] ?? null; + }, + registry, + }; + const protocol = new ObjectStackProtocolImplementation(engine, () => new Map()); + return { protocol, registry }; +} + +type Layer = Record & { label?: unknown }; +interface LayeredAnswer { + code: Layer | null; + overlay: Layer | null; + overlayScope: 'org' | 'env' | null; + effective: Layer | null; + lock: string; + lockSource?: string; + provenance?: string; + packageId?: string; + packageVersion?: string; + editable: boolean; + deletable: boolean; + resettable: boolean; +} + +const layered = async (protocol: ObjectStackProtocolImplementation, request: Record) => + (await protocol.getMetaItemLayered(request as any)) as unknown as LayeredAnswer; + +/** The ADR-0010 protection flags the response derives from its layers. */ +const flagsOf = (answer: LayeredAnswer) => ({ + lock: answer.lock, + lockSource: answer.lockSource, + provenance: answer.provenance, + packageId: answer.packageId, + packageVersion: answer.packageVersion, + editable: answer.editable, + deletable: answer.deletable, + resettable: answer.resettable, +}); + +/** Boot hydration: the stored rows go into the registry, as on a cold boot. */ +async function bootHydrate(protocol: ObjectStackProtocolImplementation) { + const receipt = await protocol.loadMetaFromDb(); + expect(receipt.storeUnavailable).toBe(false); + expect(receipt.errors).toBe(0); + return receipt; +} + +describe('[#21059] the layered read answers a null code layer for a name no package ships, before and after hydration', () => { + it('an unshipped flow name: null before boot hydration and null after it, with the stored row reported as the stored layer', async () => { + const { protocol, registry } = harness([storedRow('flow', UNSHIPPED, flowBody(UNSHIPPED, 'STORED'))]); + + const before = await layered(protocol, { type: 'flow', name: UNSHIPPED }); + expect(registry.bare('flow', UNSHIPPED)).toBeUndefined(); + expect(before.code).toBeNull(); + expect(before.overlay?.label).toBe('STORED'); + + await bootHydrate(protocol); + // The hydration really ran: the row now sits in the registry's plain slot. + expect(registry.bare('flow', UNSHIPPED)?.label).toBe('STORED'); + + const after = await layered(protocol, { type: 'flow', name: UNSHIPPED }); + expect(after.code).toBeNull(); + expect(after.overlay?.label).toBe('STORED'); + expect(after.overlayScope).toBe('env'); + expect(after.effective?.label).toBe('STORED'); + expect(flagsOf(after)).toEqual(flagsOf(before)); + }); + + it('the read-side hydration is held the same way', async () => { + const { protocol, registry } = harness([storedRow('flow', UNSHIPPED, flowBody(UNSHIPPED, 'STORED'))]); + await protocol.getMetaItemsForExecution({ type: 'flow' }); + expect(registry.bare('flow', UNSHIPPED)?.label).toBe('STORED'); + + const after = await layered(protocol, { type: 'flow', name: UNSHIPPED }); + + expect(after.code).toBeNull(); + expect(after.effective?.label).toBe('STORED'); + }); + + it('the package-scoped read and the plural type spelling answer the same', async () => { + for (const request of [ + { type: 'flow', name: UNSHIPPED, packageId: PACKAGE_ID }, + { type: 'flows', name: UNSHIPPED }, + ]) { + const { protocol } = harness([storedRow('flow', UNSHIPPED, flowBody(UNSHIPPED, 'STORED'))]); + await bootHydrate(protocol); + const after = await layered(protocol, request); + expect(after.code).toBeNull(); + expect(after.overlay?.label).toBe('STORED'); + } + }); + + it('a stored body carrying package-provenance stamps under an unshipped name is no code layer, and the lock-state flags do not move across hydration', async () => { + for (const row of [ + storedRow('flow', STAMPED, stampedBody(STAMPED)), + storedRow('flow', STAMPED, stampedBody(STAMPED), { package_id: PACKAGE_ID }), + ]) { + const { protocol, registry } = harness([row]); + + const before = await layered(protocol, { type: 'flow', name: STAMPED }); + expect(before.code).toBeNull(); + + await bootHydrate(protocol); + expect(registry.bare('flow', STAMPED)?.label).toBe('STAMPED'); + + for (const request of [ + { type: 'flow', name: STAMPED }, + { type: 'flow', name: STAMPED, packageId: PACKAGE_ID }, + ]) { + const after = await layered(protocol, request); + expect(after.code).toBeNull(); + expect(after.overlay?.label).toBe('STAMPED'); + expect(flagsOf(after)).toEqual(flagsOf(before)); + } + } + }); + + it('not a flow-only rule: an unshipped view\'s hydrated row is no code layer either', async () => { + const { protocol, registry } = harness([ + storedRow('view', UNSHIPPED_VIEW, { name: UNSHIPPED_VIEW, label: 'STORED VIEW' }), + ]); + + const before = await layered(protocol, { type: 'view', name: UNSHIPPED_VIEW }); + expect(before.code).toBeNull(); + + await bootHydrate(protocol); + expect(registry.bare('view', UNSHIPPED_VIEW)?.label).toBe('STORED VIEW'); + + const after = await layered(protocol, { type: 'view', name: UNSHIPPED_VIEW }); + expect(after.code).toBeNull(); + expect(after.effective?.label).toBe('STORED VIEW'); + expect(flagsOf(after)).toEqual(flagsOf(before)); + }); + + it('control: a shipped name keeps the loader\'s body as its code layer, before and after hydration', async () => { + const { protocol } = harness([storedRow('flow', SHIPPED, flowBody(SHIPPED, 'STORED'))]); + + const before = await layered(protocol, { type: 'flow', name: SHIPPED }); + expect(before.code?.label).toBe('LOADER'); + + await bootHydrate(protocol); + + const after = await layered(protocol, { type: 'flow', name: SHIPPED }); + expect(after.code?.label).toBe('LOADER'); + expect(after.overlay?.label).toBe('STORED'); + expect(after.packageId).toBe(PACKAGE_ID); + expect(after.provenance).toBe('package'); + expect(flagsOf(after)).toEqual(flagsOf(before)); + }); + + it('control: an item registered at runtime with no package keeps its code layer', async () => { + const { protocol, registry } = harness([]); + registry.registerItem('flow', flowBody(RUNTIME, 'RUNTIME')); + + const before = await layered(protocol, { type: 'flow', name: RUNTIME }); + expect(before.code?.label).toBe('RUNTIME'); + expect(before.overlay).toBeNull(); + expect(before.effective?.label).toBe('RUNTIME'); + + await bootHydrate(protocol); + + const after = await layered(protocol, { type: 'flow', name: RUNTIME }); + expect(after.code?.label).toBe('RUNTIME'); + expect(after.effective?.label).toBe('RUNTIME'); + }); +}); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index cc1be8d293b..d161599aaa9 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -84,6 +84,11 @@ import { // ObjectStackProtocolImplementation.tenantAuthoredWriteRefusal} to tell a // body that CLAIMS to be code-shipped from one that does not. isCodeArtifactBody, + // [#21059] The tenant marker the hydrator writes on every stored row it + // registers (ADR-0010 `_provenance: 'org'`) — read by + // {@link ObjectStackProtocolImplementation.getMetaItemLayered}'s code-layer + // fallback so a hydrated row is never answered as the code layer. + isTenantAuthored, } from '@objectstack/metadata-core'; // [#5532] One vocabulary of "which driver read errors are benign", shared with // `sys-metadata-repository.ts` in this package and with `DatabaseLoader` in @@ -9263,11 +9268,37 @@ export class ObjectStackProtocolImplementation implements // Prefer the artifact-only lookup so an overlay row hydrated // into the registry's plain key can't masquerade as the "code // default" layer; fall back to getItem for runtime-only items. + // + // [#21059] …and "runtime-only" is held to what it says. The plain + // key is one slot for two populations: an item registered at + // runtime with no package, and a stored row the hydrator put there + // ({@link hydrateOverlayIntoRegistry}, the boot `object` limb of + // {@link loadMetaFromDb}). For a name no package ships the + // artifact lookup misses, so `getItem` answered the hydrated row + // and the code layer became the stored body — but only once a + // hydration had run, so the same read answered `code: null` before + // it. The spec's layer 1 is "`null` when no artifact ships this + // item (it exists only as an overlay)", and #5707 / #5840 allow a + // layer only from a read that happened: a stored row is the overlay + // read below, never an artifact read. + // + // The discriminator is the tenant marker the hydrator ALREADY + // writes on every row it registers — ADR-0010 `_provenance: 'org'`, + // read through {@link isTenantAuthored}. ⛔ No new marker. A body + // that carries package-provenance stamps under a name no package + // ships is the same row: the hydrator restates its authorship over + // whatever its bytes claim, and a stamp is not an artifact read, so + // `resolveLockState` below reads this item's code layer as `null` + // too (triage's ruling, overturnable by the maintainer). A + // runtime-registered item with no package carries no tenant marker + // and keeps its code layer. + const runtimeOnly = (item: unknown): unknown => + item !== undefined && isTenantAuthored(item) ? undefined : item; let regItem = this.lookupArtifactItem(request.type, request.name, request.packageId) - ?? this.engine.registry.getItem(request.type, request.name, request.packageId); + ?? runtimeOnly(this.engine.registry.getItem(request.type, request.name, request.packageId)); if (regItem === undefined) { const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type]; - if (alt) regItem = this.engine.registry.getItem(alt, request.name, request.packageId); + if (alt) regItem = runtimeOnly(this.engine.registry.getItem(alt, request.name, request.packageId)); } if (regItem !== undefined) code = regItem; } diff --git a/packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts b/packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts new file mode 100644 index 00000000000..f97acca708b --- /dev/null +++ b/packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts @@ -0,0 +1,273 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#21059] The LAYERED read's code layer for a flow name no package ships is +// `null` before the stored row is hydrated and after a cold boot hydrated it — +// the spec's layer 1, "`null` when no artifact ships this item (it exists only +// as an overlay)". Over the real showcase composition, on a database file. +// +// ## What was broken +// +// The code layer is the artifact lookup, then a fallback that reads the +// registry's plain slot for items registered at runtime without a package. A +// stored row that a hydration registers lands in that same slot, so once the +// cold boot had hydrated the row, `GET /meta/flow/NAME/layers` answered the +// stored body as the code layer — and derived the lock and provenance flags +// from it — while the same read before the hydration answered a null code +// layer. The answer depended on hydration state. +// +// ## Why a booted stack, over two boots +// +// The unit pins (`protocol.layered-code-unshipped-name.test.ts`) hold the +// read's branches with doubles. What they cannot answer is the composition: the +// real registry, the real boot hydration and the metadata service behind the +// layered door. The first boot reads the door with the rows in the store and +// none of them hydrated; the second boot is cold, on the same file, after the +// boot hydration put the rows in the registry. +// +// ## What each case pins (triage's pins, plus the controls) +// +// - an unshipped name's layered read answers a null code layer before and +// after hydration, on the layered door and on the deprecated layers flag; +// - a stored body carrying package-provenance stamps under an unshipped name +// is no code layer either, and its lock-state flags read the same on both +// sides of the hydration (triage's ruling on the open corner); +// - a shipped name keeps the loader's body as its code layer (control); +// - an item registered at runtime with no package keeps its code layer +// (control — the fallback's stated purpose). + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import showcaseStack from '@objectstack/example-showcase'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { RecordChangeTriggerPlugin } from '@objectstack/trigger-record-change'; +import { ConnectorRestPlugin } from '@objectstack/connector-rest'; +import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi'; +import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; +import { fileURLToPath } from 'node:url'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +/** Package-relative connector refs resolve against the cwd — see the sibling boots. */ +const SHOWCASE_DIR = fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url)); + +/** The package the showcase composition loads its flows from. */ +const SHOWCASE_PACKAGE = 'com.example.showcase'; +/** A shipped screen flow (no trigger) whose body seeds the stored rows below. */ +const SOURCE = 'showcase_reassign_wizard'; +/** A shipped flow given an environment-wide stored row of its name: the control. */ +const SHIPPED = 'showcase_urgent_task_alert'; +/** A flow name no package ships, given an environment-wide stored row: the subject. */ +const UNSHIPPED = 'dogfood_21059_unshipped_flow'; +/** A flow name no package ships, whose stored body claims the showcase package. */ +const STAMPED = 'dogfood_21059_stamped_flow'; +/** A flow registered at runtime with no package, on the measured boot. */ +const RUNTIME = 'dogfood_21059_runtime_flow'; + +const STORED_LABEL = 'Stored body 21059'; +const RUNTIME_LABEL = 'Runtime flow 21059'; + +const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] }; + +interface FlowBody { + name?: string; + label?: string; + nodes?: Array<{ id: string }>; +} +interface Layered { + code?: FlowBody | null; + overlay?: FlowBody | null; + overlayScope?: 'org' | 'env' | null; + effective?: FlowBody | null; + lock?: string; + lockSource?: string; + provenance?: string; + packageId?: string; + packageVersion?: string; + editable?: boolean; + deletable?: boolean; + resettable?: boolean; +} +interface Engine { + getFlow(name: string): Promise; +} +interface Ql { + insert(object: string, data: Record, options?: unknown): Promise; + registry: { + getItem(type: string, name: string): unknown; + registerItem(type: string, item: Record, keyField?: string): void; + }; +} + +const plugins = () => [ + new RecordChangeTriggerPlugin(), + new ConnectorRestPlugin(), + new ConnectorOpenApiPlugin(), + new ConnectorMcpPlugin({ declarativeStdio: ['node'] }), +]; + +async function boot(databaseFile: string): Promise { + return bootStack(showcaseStack, { automation: true, databaseFile, extraPlugins: plugins() }); +} + +/** A copy of a loader's body with every underscore-prefixed key dropped. */ +function plainCopy(loader: FlowBody | null): Record { + const body: Record = JSON.parse(JSON.stringify(loader)); + for (const key of Object.keys(body)) if (key.startsWith('_')) delete body[key]; + return body; +} + +/** The ADR-0010 protection flags the layered response derives from its layers. */ +const flagsOf = (doc: Layered) => ({ + lock: doc.lock, + lockSource: doc.lockSource, + provenance: doc.provenance, + packageId: doc.packageId, + packageVersion: doc.packageVersion, + editable: doc.editable, + deletable: doc.deletable, + resettable: doc.resettable, +}); + +describe('the layered flow read answers a null code layer for a name no package ships, across a cold boot (showcase)', () => { + let stack: VerifyStack; + let token: string; + let prevCwd: string; + let dir: string; + let dbFile: string; + let loaderShipped: FlowBody | null = null; + /** What the first boot answered, with the rows stored and none hydrated. */ + const before: Record = {}; + + const engine = () => stack.kernel.getServiceAsync('automation') as unknown as Promise; + const ql = () => stack.kernel.getServiceAsync('objectql') as unknown as Promise; + + const unwrap = (json: unknown) => { + const body = json as Record; + return (body?.data ?? body) as Record; + }; + /** `GET /meta/flow/:name/layers` — the three-layer diagnostic. */ + const layers = async (name: string, path = `/meta/flow/${name}/layers`) => { + const res = await stack.apiAs(token, 'GET', path); + const json: unknown = await res.json().catch(() => ({})); + return { status: res.status, doc: unwrap(json) as Layered }; + }; + + beforeAll(async () => { + prevCwd = process.cwd(); + process.chdir(SHOWCASE_DIR); + dir = mkdtempSync(join(tmpdir(), 'dogfood-21059-')); + dbFile = join(dir, 'showcase.db'); + + // First boot: put the rows in the store, then read the layered door + // before anything has hydrated them. + stack = await boot(dbFile); + token = await stack.signIn(); + const first = await engine(); + const store = await ql(); + const source = await first.getFlow(SOURCE); + loaderShipped = await first.getFlow(SHIPPED); + + const now = new Date().toISOString(); + const row = (name: string, body: Record) => ({ + type: 'flow', + name, + organization_id: null, + package_id: null, + state: 'active', + version: 1, + checksum: null, + created_at: now, + updated_at: now, + metadata: JSON.stringify(body), + }); + const stored = (name: string) => ({ ...plainCopy(source), name, label: STORED_LABEL }); + for (const data of [ + row(UNSHIPPED, stored(UNSHIPPED)), + row(STAMPED, { ...stored(STAMPED), _packageId: SHOWCASE_PACKAGE, _provenance: 'package' }), + row(SHIPPED, { ...plainCopy(loaderShipped), label: STORED_LABEL }), + ]) { + await store.insert('sys_metadata', data, { context: SYSTEM_CTX }); + } + for (const name of [UNSHIPPED, STAMPED, SHIPPED]) { + const registered = (store.registry.getItem('flow', name) as FlowBody | undefined)?.label === STORED_LABEL; + before[name] = { ...(await layers(name)), registered }; + } + await stack.stop(); + + // The measured boot: cold, on the same file — the boot hydration reads the rows. + stack = await boot(dbFile); + token = await stack.signIn(); + }, 360_000); + + afterAll(async () => { + await stack?.stop(); + if (prevCwd) process.chdir(prevCwd); + if (dir) rmSync(dir, { recursive: true, force: true }); + }); + + it('the first boot read the rows unhydrated, and the cold boot hydrated them into the registry', async () => { + for (const name of [UNSHIPPED, STAMPED, SHIPPED]) expect(before[name].registered).toBe(false); + const registry = (await ql()).registry; + for (const name of [UNSHIPPED, STAMPED]) { + expect((registry.getItem('flow', name) as FlowBody | undefined)?.label).toBe(STORED_LABEL); + } + }); + + it('an unshipped name\'s layered read answers a null code layer before and after hydration', async () => { + expect(before[UNSHIPPED].status).toBe(200); + expect(before[UNSHIPPED].doc.code).toBeNull(); + + const after = await layers(UNSHIPPED); + + expect(after.status).toBe(200); + expect(after.doc.code).toBeNull(); + expect(after.doc.overlay?.label).toBe(STORED_LABEL); + expect(after.doc.overlayScope).toBe('env'); + expect(after.doc.effective?.label).toBe(STORED_LABEL); + expect(flagsOf(after.doc)).toEqual(flagsOf(before[UNSHIPPED].doc)); + }); + + it('the deprecated layers flag on the by-name door answers the same null code layer', async () => { + const after = await layers(UNSHIPPED, `/meta/flow/${UNSHIPPED}?layers=true`); + + expect(after.status).toBe(200); + expect(after.doc.code).toBeNull(); + expect(after.doc.overlay?.label).toBe(STORED_LABEL); + }); + + it('a stored body carrying package-provenance stamps under an unshipped name is no code layer, and its lock-state flags do not move across hydration', async () => { + expect(before[STAMPED].status).toBe(200); + expect(before[STAMPED].doc.code).toBeNull(); + + const after = await layers(STAMPED); + + expect(after.status).toBe(200); + expect(after.doc.code).toBeNull(); + expect(after.doc.overlay?.label).toBe(STORED_LABEL); + expect(flagsOf(after.doc)).toEqual(flagsOf(before[STAMPED].doc)); + }); + + it('control: a shipped name keeps the loader\'s body as its code layer, before and after hydration', async () => { + expect(before[SHIPPED].doc.code?.label).toBe(loaderShipped?.label); + + const after = await layers(SHIPPED); + + expect(after.status).toBe(200); + expect(after.doc.code?.label).toBe(loaderShipped?.label); + expect(after.doc.overlay?.label).toBe(STORED_LABEL); + expect(after.doc.packageId).toBe(SHOWCASE_PACKAGE); + expect(after.doc.provenance).toBe('package'); + }); + + it('control: a flow registered at runtime with no package keeps its code layer', async () => { + const store = await ql(); + store.registry.registerItem('flow', { ...plainCopy(loaderShipped), name: RUNTIME, label: RUNTIME_LABEL }); + + const read = await layers(RUNTIME); + + expect(read.status).toBe(200); + expect(read.doc.code?.label).toBe(RUNTIME_LABEL); + expect(read.doc.overlay).toBeNull(); + expect(read.doc.effective?.label).toBe(RUNTIME_LABEL); + }); +}); From 4a5639294d7e19c9e08610b6b9124c205ff1d632 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 10:21:51 +0000 Subject: [PATCH 2/2] test(metadata-protocol): record the new layered-read pin's engine double in the engine-double-contract ledger (#21059) Generated by `node scripts/check-engine-double-contract.mjs --write`: one findOne row for protocol.layered-code-unshipped-name.test.ts, whose double is the pinned one from protocol.flow-layered-shipped-name.test.ts. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude --- scripts/engine-double-contract.pinned.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index ed17b7cbffe..3d931266e8e 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -731,6 +731,11 @@ "verb": "update", "pinned": 1 }, + { + "file": "packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts", + "verb": "findOne", + "pinned": 1 + }, { "file": "packages/metadata-protocol/src/protocol.legacy-overlay-delete.test.ts", "verb": "delete",