Skip to content

Commit 3972d9a

Browse files
committed
fix(runtime): loadArtifactBundle forward-converts the loaded artifact once, before mergeRuntimeModule and every reader
An option-B artifact built before a retirement was refused by the runtime's first reader of packages[] (createStandaloneStack, through resolveArtifactCollections), before the metadata door's own conversion ran. The loader now runs applyArtifactForwardConversions once on the loaded bundle, so every runtime reader sees the converted package bodies. The AppPlugin comment that said no conversion reaches the bodies is corrected. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
1 parent a8579dc commit 3972d9a

2 files changed

Lines changed: 54 additions & 5 deletions

File tree

‎packages/runtime/src/app-plugin.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,13 @@ export class AppPlugin implements Plugin {
498498
// served the artifact door's converted copy of it, and what handed the
499499
// email loader a template it refuses to send.
500500
//
501-
// A bundle carrying `packages[]` is handed over as authored, as before:
502-
// the `manifest` service registers its package BODIES, which no
503-
// conversion reaches (the ADR-0087 pass walks the definition's own
504-
// collections, at the artifact door too), and `forwardConverted` would
505-
// resolve `packages[]` here, ahead of the `manifest` service (#15292).
501+
// A bundle carrying `packages[]` is handed over as it arrived, as before:
502+
// `forwardConverted` would resolve `packages[]` here, ahead of the
503+
// `manifest` service (#15292). Its package BODIES are already converted
504+
// when the bundle came through `loadArtifactBundle`, which runs the
505+
// ADR-0087 pass once over every body at load (#23027); a bundle handed
506+
// to this plugin by any other route reaches the `manifest` service as
507+
// authored.
506508
const servicePayload = (this.bundle as { packages?: unknown }).packages === undefined
507509
? this.forwardConverted(ctx).definition
508510
: this.bundle.manifest

‎packages/runtime/src/load-artifact-bundle.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,51 @@ import { readFile } from 'node:fs/promises';
3030
import { resolve as resolvePath, isAbsolute, dirname } from 'node:path';
3131
import { pathToFileURL } from 'node:url';
3232

33+
import { applyArtifactForwardConversions } from '@objectstack/metadata-core';
34+
3335
import { resolveArtifactCollections } from './artifact-collections.js';
3436

37+
/**
38+
* The ADR-0087 D2 forward conversion at the runtime's artifact load (#23027):
39+
* one call of `applyArtifactForwardConversions`, under the window the artifact's
40+
* own `engines.protocol` opens, over the definition and every
41+
* `packages[i].manifest` body.
42+
*
43+
* A loaded artifact is data at rest. Every runtime reader of a multi-package
44+
* one parses each package body strictly: `resolveArtifactPackageOrder` gates
45+
* each entry, reached through `resolveArtifactCollections`
46+
* (`createStandaloneStack`'s surfaced stack, `AppPlugin`'s `collections`,
47+
* `mergeRuntimeModule`) and through the `manifest` service's `register()`. So
48+
* an artifact built before a retirement was refused by the first of those
49+
* readers, before the metadata door's own conversion could run. Converting the
50+
* bundle once, where it is read from disk, gives every reader the same
51+
* converted bytes. ⛔ Never a conversion per reader: that is N copies of one
52+
* policy, and the readers could then disagree about which artifacts load.
53+
*
54+
* The one other place on the boot path that reads an artifact FILE,
55+
* `readConfigDeclaredDefault` (`resolve-project-database.ts`), calls this same
56+
* function on what it read.
57+
*
58+
* Silent by design. On an artifact boot `createStandaloneStack` composes
59+
* `MetadataPlugin` with `artifactSource` naming this same file, and its door
60+
* converts the raw file again and prints the one operator summary per
61+
* conversion. A line here as well would name each conversion twice per boot.
62+
* Converting before the protocol handshake changes no verdict: the handshake
63+
* reads `manifest.engines`, which no conversion rewrites.
64+
*
65+
* An envelope that was not unwrapped (`{ schemaVersion, metadata }`) has its
66+
* `metadata` converted, the position the metadata door converts. Copy-on-write:
67+
* the input comes back by reference when nothing converted.
68+
*/
69+
export function convertArtifactAtLoad(bundle: any): any {
70+
if (bundle === null || typeof bundle !== 'object' || Array.isArray(bundle)) return bundle;
71+
if (bundle.schemaVersion != null && bundle.metadata !== undefined) {
72+
const metadata = applyArtifactForwardConversions(bundle.metadata).definition;
73+
return metadata === bundle.metadata ? bundle : { ...bundle, metadata };
74+
}
75+
return applyArtifactForwardConversions(bundle).definition;
76+
}
77+
3578
export interface LoadArtifactBundleOptions {
3679
/** Optional log tag for warnings (defaults to `[loadArtifactBundle]`). */
3780
tag?: string;
@@ -128,6 +171,10 @@ export async function loadArtifactBundle(
128171
return null;
129172
}
130173

174+
// [#23027] Converted HERE, once, before `mergeRuntimeModule` and before any
175+
// reader is handed the bundle — see `convertArtifactAtLoad`.
176+
bundle = convertArtifactAtLoad(bundle);
177+
131178
if (isUrl) {
132179
// Remote artifacts cannot dynamic-import a sibling ESM runtime module
133180
// safely (Node does not allow importing arbitrary URLs and we never

0 commit comments

Comments
 (0)