@forgeax/engine-import 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +91 -3
  2. package/dist/__tests__/ies-contract.unit.test.d.ts +2 -0
  3. package/dist/__tests__/ies-contract.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/ies-producer.unit.test.d.ts +2 -0
  5. package/dist/__tests__/ies-producer.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/mesh-lod-contract.unit.test.d.ts +2 -0
  7. package/dist/__tests__/mesh-lod-contract.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts +2 -0
  9. package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +2 -0
  11. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts +2 -0
  13. package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/source-package-publication-lod.integration.test.d.ts +2 -0
  15. package/dist/__tests__/source-package-publication-lod.integration.test.d.ts.map +1 -0
  16. package/dist/browser.d.ts +1 -0
  17. package/dist/browser.d.ts.map +1 -1
  18. package/dist/browser.mjs +230 -3
  19. package/dist/browser.mjs.map +1 -1
  20. package/dist/build-production.d.ts +1 -8
  21. package/dist/build-production.d.ts.map +1 -1
  22. package/dist/ies/ies-importer.d.ts +4 -0
  23. package/dist/ies/ies-importer.d.ts.map +1 -0
  24. package/dist/ies/parse-lm63.d.ts +13 -0
  25. package/dist/ies/parse-lm63.d.ts.map +1 -0
  26. package/dist/ies/resample-type-c.d.ts +4 -0
  27. package/dist/ies/resample-type-c.d.ts.map +1 -0
  28. package/dist/import-runner.d.ts.map +1 -1
  29. package/dist/index.d.ts +11 -6
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.mjs +2550 -1029
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/mesh-bin.d.ts.map +1 -1
  34. package/dist/mesh-bin.mjs +29 -1
  35. package/dist/mesh-bin.mjs.map +1 -1
  36. package/dist/mesh-lod.d.ts +60 -0
  37. package/dist/mesh-lod.d.ts.map +1 -0
  38. package/dist/parameterized-scriptable-pack.d.ts +68 -0
  39. package/dist/parameterized-scriptable-pack.d.ts.map +1 -0
  40. package/dist/scriptable-pack-file-snapshot.d.ts +71 -0
  41. package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -0
  42. package/dist/scriptable-pack-host.d.ts +70 -39
  43. package/dist/scriptable-pack-host.d.ts.map +1 -1
  44. package/dist/scriptable-pack-output-producers.d.ts +1 -0
  45. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  46. package/dist/scriptable-pack-staged-snapshot.d.ts +2 -2
  47. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
  48. package/dist/scriptable-pack.d.ts +13 -26
  49. package/dist/scriptable-pack.d.ts.map +1 -1
  50. package/dist/source-package-errors.d.ts.map +1 -1
  51. package/dist/source-package-publication.d.ts.map +1 -1
  52. package/package.json +8 -7
  53. package/src/__tests__/ies-contract.unit.test.ts +31 -0
  54. package/src/__tests__/ies-producer.unit.test.ts +104 -0
  55. package/src/__tests__/mesh-bin.test.ts +69 -0
  56. package/src/__tests__/mesh-lod-contract.unit.test.ts +92 -0
  57. package/src/__tests__/mesh-lod-reimport.integration.test.ts +15 -0
  58. package/src/__tests__/parameterized-scriptable-pack.unit.test.ts +270 -0
  59. package/src/__tests__/scriptable-pack-file-snapshot.unit.test.ts +177 -0
  60. package/src/__tests__/scriptable-pack-host.unit.test.ts +196 -4
  61. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +7 -1
  62. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +43 -12
  63. package/src/__tests__/source-package-publication-lod.integration.test.ts +29 -0
  64. package/src/__tests__/source-package-publication.integration.test.ts +26 -1
  65. package/src/browser.ts +5 -0
  66. package/src/build-production.ts +498 -141
  67. package/src/ies/ies-importer.ts +126 -0
  68. package/src/ies/parse-lm63.ts +98 -0
  69. package/src/ies/resample-type-c.ts +108 -0
  70. package/src/import-runner.ts +58 -0
  71. package/src/index.ts +49 -19
  72. package/src/mesh-bin.ts +38 -0
  73. package/src/mesh-lod.ts +265 -0
  74. package/src/parameterized-scriptable-pack.ts +725 -0
  75. package/src/scriptable-pack-file-snapshot.ts +346 -0
  76. package/src/scriptable-pack-host.ts +790 -343
  77. package/src/scriptable-pack-output-producers.ts +58 -1
  78. package/src/scriptable-pack-staged-snapshot.ts +12 -11
  79. package/src/scriptable-pack.ts +16 -485
  80. package/src/source-package-errors.ts +6 -0
  81. package/src/source-package-publication.ts +32 -2
  82. package/dist/.tsbuildinfo +0 -1
  83. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +0 -2
  84. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +0 -1
  85. package/dist/__tests__/scriptable-pack.integration.test.d.ts +0 -2
  86. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +0 -1
  87. package/dist/scriptable-source-package.d.ts +0 -14
  88. package/dist/scriptable-source-package.d.ts.map +0 -1
  89. package/src/__tests__/scriptable-pack-product.contract.test.ts +0 -46
  90. package/src/__tests__/scriptable-pack.integration.test.ts +0 -435
  91. package/src/scriptable-source-package.ts +0 -88
@@ -1,29 +1,43 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { stat } from 'node:fs/promises';
3
- import { isAbsolute, relative, resolve } from 'node:path';
4
2
  import { createAcceptedPublication } from '@forgeax/engine-ddc';
3
+ import { normalizeMeshPayload } from '@forgeax/engine-geometry';
5
4
  import {
6
- type AuthoredPackInput,
7
5
  finalizePackageTransportSource,
8
- loadAssetConfig,
9
6
  type PackageFinalizePolicy,
7
+ type PackageProductAsset,
8
+ packageTransportRevision,
10
9
  resolveAssetSource,
11
- upgradeLegacyAuthoredPack,
12
10
  } from '@forgeax/engine-pack/build';
13
- import { AssetGuid } from '@forgeax/engine-pack/guid';
14
- import { type NativeCooker, NativeCookerRegistry } from '@forgeax/engine-pack/native-cooker';
15
- import type { ScanSourceDeclaration } from '@forgeax/engine-pack/scanner';
11
+ import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
12
+ import { AssetGuid, PackageId, type PackageId as PackageIdType } from '@forgeax/engine-pack/guid';
16
13
  import {
14
+ type NativeCookDraft,
15
+ type NativeCooker,
16
+ NativeCookerRegistry,
17
+ } from '@forgeax/engine-pack/native-cooker';
18
+ import type {
19
+ LegacyPackInventoryDocument,
20
+ ScanSourceDeclaration,
21
+ } from '@forgeax/engine-pack/scanner';
22
+ import {
23
+ type AnyParameterizedPackDefinition,
24
+ type DirectPackAssetProjection,
25
+ isScriptablePackAssetKind,
26
+ parseParameterizedPackJson,
27
+ projectDirectPackJson,
17
28
  projectScriptablePackSceneComponents,
18
- type ScriptablePackDefinition,
19
29
  type ScriptablePackSourceClosureEntry,
20
30
  } from '@forgeax/engine-pack/source';
21
- import { inventoryScriptablePackSource } from '@forgeax/engine-pack/source-node';
31
+ import { isEngineMaterial } from '@forgeax/engine-shader';
22
32
  import type {
23
33
  Asset,
24
34
  AssetGuid as AssetGuidType,
25
35
  AssetPublicationEnvelope,
26
36
  AssetPublicationOutput,
37
+ AssetRef,
38
+ ImportedArtifactBody,
39
+ ImportedAsset,
40
+ MaterialAsset,
27
41
  ResourceRevision,
28
42
  Result,
29
43
  } from '@forgeax/engine-types';
@@ -31,43 +45,63 @@ import { AssetError, err, ok } from '@forgeax/engine-types';
31
45
  import type { DdcPack, ImportRunnerFs, RunImportMeta } from './import-runner.js';
32
46
  import type { ImporterRegistry } from './importer-registry.js';
33
47
  import { projectImportProductForBuild } from './pack-projection.js';
34
- import type { ScriptablePackDomainError, ScriptablePackStagedOutput } from './scriptable-pack.js';
35
- import { createStandardAssetOutputProducerRegistry } from './scriptable-pack-output-producers.js';
36
48
  import {
37
- createScriptablePackStagedAssetSnapshotSource,
38
- type ScriptablePackStagedOwner,
39
- } from './scriptable-pack-staged-snapshot.js';
49
+ buildParameterizedScriptablePack,
50
+ buildParameterizedScriptablePackWorklist,
51
+ type ParameterizedScriptablePackBuildProduct,
52
+ type ParameterizedScriptablePackWorkItem,
53
+ } from './parameterized-scriptable-pack.js';
54
+ import type {
55
+ ScriptablePackAssetSnapshotSource,
56
+ ScriptablePackDomainError,
57
+ ScriptablePackStagedOutput,
58
+ } from './scriptable-pack.js';
40
59
  import {
41
- produceScriptableSourcePackage,
42
- type ScriptableSourcePackageProduct,
43
- type ScriptableSourcePackageResult,
44
- } from './scriptable-source-package.js';
60
+ createPreExternalizedSceneAssetOutputProducer,
61
+ createStandardAssetOutputProducerRegistry,
62
+ } from './scriptable-pack-output-producers.js';
45
63
  import { produceSourcePackage } from './source-package.js';
46
64
 
47
65
  type FinalizedPackageTransport = Awaited<ReturnType<typeof finalizePackageTransportSource>>;
48
66
 
49
- export interface ScriptablePackInput {
67
+ export interface ParameterizedScriptablePackInput {
50
68
  readonly sourcePath: string;
51
69
  readonly displaySourcePath: string;
52
- readonly definition: Readonly<ScriptablePackDefinition>;
70
+ readonly definition: AnyParameterizedPackDefinition;
53
71
  readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
54
72
  readonly publicationGeneration: number;
55
- readonly policy: ScriptablePackTransportPolicy;
73
+ readonly policy:
74
+ | PackageFinalizePolicy
75
+ | ((product: ParameterizedScriptablePackBuildProduct) => PackageFinalizePolicy);
76
+ readonly subjectPackageId?: PackageIdType;
77
+ readonly values?: Readonly<Record<string, unknown>>;
78
+ readonly inheritedValues?: Readonly<
79
+ Record<string, import('@forgeax/engine-pack/source').PackParameterValue>
80
+ >;
56
81
  }
57
82
 
58
- export interface ScriptablePackPublicationFacts {
59
- readonly outputs: readonly AssetPublicationOutput[];
60
- readonly refs: ReadonlyMap<string, readonly string[]>;
61
- }
62
-
63
- export interface PreparedScriptablePack {
64
- readonly product: ScriptableSourcePackageProduct;
83
+ export interface PreparedParameterizedScriptablePack {
84
+ readonly product: ParameterizedScriptablePackBuildProduct;
65
85
  readonly finalized: FinalizedPackageTransport;
66
86
  readonly facts: ScriptablePackPublicationFacts;
67
87
  readonly revision: ResourceRevision;
68
88
  readonly publication: AssetPublicationEnvelope;
69
89
  }
70
90
 
91
+ export interface ParameterizedScriptablePackProductionOptions {
92
+ readonly sources: readonly ParameterizedScriptablePackInput[];
93
+ readonly declaredExternalOutputs?: readonly ScriptablePackStagedOutput[];
94
+ readonly assetSource?: ScriptablePackAssetSnapshotSource;
95
+ readonly availableGuids?: ReadonlySet<string>;
96
+ readonly incomingRefs?: ReadonlyMap<string, readonly string[]>;
97
+ readonly maxPasses?: number;
98
+ }
99
+
100
+ export interface ScriptablePackPublicationFacts {
101
+ readonly outputs: readonly AssetPublicationOutput[];
102
+ readonly refs: ReadonlyMap<string, readonly string[]>;
103
+ }
104
+
71
105
  export interface ScriptablePackTransportPaths {
72
106
  readonly packagePath: string;
73
107
  readonly artifactPath: (path: string) => string;
@@ -80,31 +114,10 @@ export interface ScriptablePackTransportSink {
80
114
  writeReceipt(path: string, body: string): void | Promise<void>;
81
115
  }
82
116
 
83
- export type ScriptablePackTransportPolicy =
84
- | PackageFinalizePolicy
85
- | ((product: ScriptableSourcePackageProduct) => PackageFinalizePolicy);
86
-
87
- type ScriptablePackHostError =
88
- ScriptableSourcePackageResult extends Result<unknown, infer E> ? E : never;
89
-
90
- export interface CookedAuthoredPack {
91
- readonly logicalPackage: DdcPack;
92
- readonly refsByGuid: ReadonlyMap<string, readonly string[]>;
93
- }
94
-
95
- export interface AuthoredPackTransport {
96
- readonly pack: AuthoredPackInput;
97
- readonly firstGuid?: string;
98
- readonly cooked?: CookedAuthoredPack;
99
- readonly finalized?: FinalizedPackageTransport;
100
- }
101
-
102
- /** Build-time capabilities required to expose ordinary Meta products to a ScriptablePack. */
117
+ /** Build-time capabilities required to expose ordinary Meta products to a Pack. */
103
118
  export interface ScriptablePackExternalImportOptions {
104
119
  readonly importerRegistry: ImporterRegistry;
105
120
  readonly fsForImport: ImportRunnerFs;
106
- /** Package path aliases from forgeax.assets.paths; defaults to the current project config. */
107
- readonly assetPaths?: Readonly<Record<string, string>>;
108
121
  }
109
122
 
110
123
  export function canonicalScriptableSourcePath(sourcePath: string): string {
@@ -114,127 +127,7 @@ export function canonicalScriptableSourcePath(sourcePath: string): string {
114
127
  return markerIndex < 0 ? normalized : normalized.slice(markerIndex + 1);
115
128
  }
116
129
 
117
- export function scriptablePackInputs(
118
- sourcePaths: readonly string[],
119
- declarations: ReadonlyMap<string, ScanSourceDeclaration>,
120
- cwd: string,
121
- generationFor: (displaySourcePath: string) => number,
122
- policyFor: (displaySourcePath: string) => ScriptablePackTransportPolicy,
123
- sourceIdentityFor?: (sourcePath: string) => string,
124
- ): ScriptablePackInput[] {
125
- return sourcePaths.flatMap((sourcePath) => {
126
- const declaration = declarations.get(resolve(cwd, sourcePath));
127
- if (declaration?.format !== 'pack.ts') return [];
128
- const projected = sourceIdentityFor?.(sourcePath);
129
- const catalogSourcePath =
130
- projected === undefined || isAbsolute(projected) ? relative(cwd, sourcePath) : projected;
131
- const displaySourcePath = canonicalScriptableSourcePath(catalogSourcePath);
132
- return [
133
- {
134
- sourcePath: resolve(cwd, sourcePath),
135
- displaySourcePath,
136
- definition: declaration.definition,
137
- sourceClosure: declaration.sourceClosure,
138
- publicationGeneration: generationFor(displaySourcePath),
139
- policy: policyFor(displaySourcePath),
140
- },
141
- ];
142
- });
143
- }
144
-
145
- /** Materialize one accepted ScriptablePack without duplicating dev/build loops. */
146
- export async function materializePreparedScriptablePack(
147
- prepared: PreparedScriptablePack,
148
- paths: ScriptablePackTransportPaths,
149
- sink: ScriptablePackTransportSink,
150
- ): Promise<ReadonlyMap<string, string>> {
151
- const { product, finalized } = prepared;
152
- await sink.writePackage(paths.packagePath, JSON.stringify(finalized.pack));
153
- for (const artifact of finalized.artifacts) {
154
- await sink.writeArtifact(
155
- paths.artifactPath(artifact.path),
156
- artifact.bytes,
157
- artifact.path.endsWith('.json') ? 'application/json' : artifact.mediaType,
158
- );
159
- }
160
- const receipts = new Map<string, string>();
161
- for (const guid of product.declaredGuids) {
162
- const path = paths.receiptPath(guid);
163
- await sink.writeReceipt(
164
- path,
165
- JSON.stringify({
166
- guid,
167
- origin: 'sourceMeta',
168
- status: 'succeeded',
169
- inputFingerprint: product.inputFingerprint,
170
- outputDigest: finalized.digest,
171
- }),
172
- );
173
- receipts.set(guid, path);
174
- }
175
- return receipts;
176
- }
177
-
178
- export async function readCookedAuthoredPack(
179
- authoredPack: AuthoredPackInput,
180
- cookers: readonly NativeCooker[] = [],
181
- sourcePath?: string,
182
- ): Promise<CookedAuthoredPack | undefined> {
183
- const parsed = upgradeLegacyAuthoredPack(authoredPack);
184
- if (parsed.schemaVersion !== '2.0.0' || parsed.assets === undefined) return undefined;
185
- const registry = new NativeCookerRegistry();
186
- for (const cooker of cookers) registry.register(cooker);
187
- const assets: DdcPack['assets'][number][] = [];
188
- const refsByGuid = new Map<string, readonly string[]>();
189
- let hasCookedAsset = false;
190
- for (const asset of parsed.assets) {
191
- const shouldCook = asset.execution === 'cooked';
192
- if (!shouldCook) {
193
- assets.push({
194
- guid: asset.guid,
195
- kind: asset.kind,
196
- ...(asset.name === undefined ? {} : { name: asset.name }),
197
- ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
198
- ...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
199
- ...(asset.relations === undefined ? {} : { relations: asset.relations }),
200
- payload: asset.payload,
201
- refs: asset.refs ?? [],
202
- artifacts: {},
203
- });
204
- continue;
205
- }
206
- hasCookedAsset = true;
207
- const result = await registry.runDraft(asset.kind, {
208
- guid: asset.guid,
209
- source: asset.payload,
210
- ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
211
- ...(sourcePath === undefined ? {} : { sourcePath }),
212
- refs: asset.refs ?? [],
213
- });
214
- if (!result.ok) throw result.error;
215
- const draft = result.value;
216
- const refs = [...draft.refs];
217
- refsByGuid.set(asset.guid.toLowerCase(), refs);
218
- assets.push({
219
- guid: draft.guid,
220
- kind: asset.kind,
221
- ...(asset.name === undefined ? {} : { name: asset.name }),
222
- ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
223
- ...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
224
- ...(asset.relations === undefined ? {} : { relations: asset.relations }),
225
- payload: draft.payload as Record<string, unknown>,
226
- refs,
227
- artifacts: draft.artifacts,
228
- });
229
- }
230
- return hasCookedAsset
231
- ? {
232
- logicalPackage: { schemaVersion: '2.0.0', kind: 'internal-text-package', assets },
233
- refsByGuid,
234
- }
235
- : undefined;
236
- }
237
-
130
+ /** Stage materialized outputs owned by Meta or an already-cooked Pack v2 document. */
238
131
  export async function declaredPackExternalOutputs(
239
132
  declarations: ReadonlyMap<string, ScanSourceDeclaration>,
240
133
  cookers: readonly NativeCooker[] = [],
@@ -243,6 +136,15 @@ export async function declaredPackExternalOutputs(
243
136
  ): Promise<readonly ScriptablePackStagedOutput[]> {
244
137
  if (requiredGuids.length === 0) return [];
245
138
  const required = new Set(requiredGuids.map((guid) => AssetGuid.format(guid).toLowerCase()));
139
+ const available = new Set(required);
140
+ for (const declaration of declarations.values()) {
141
+ if (declaration.format !== 'meta.json') continue;
142
+ for (const asset of declaration.value.subAssets) {
143
+ const parsed = AssetGuid.parse(asset.guid);
144
+ if (!parsed.ok) throw parsed.error;
145
+ available.add(AssetGuid.format(parsed.value).toLowerCase());
146
+ }
147
+ }
246
148
  const outputs: ScriptablePackStagedOutput[] = [];
247
149
  for (const declaration of declarations.values()) {
248
150
  if (declaration.format === 'meta.json') {
@@ -250,22 +152,10 @@ export async function declaredPackExternalOutputs(
250
152
  if (!declaration.value.subAssets.some((asset) => required.has(asset.guid.toLowerCase()))) {
251
153
  continue;
252
154
  }
253
- const assetPaths = externalImport.assetPaths ?? loadAssetConfig(process.cwd()).paths;
254
- const resolved = resolveAssetSource(
255
- declaration.sourcePath,
256
- declaration.value.source,
257
- assetPaths,
258
- );
259
- if (!resolved.ok) {
260
- throw new AssetError({
261
- code: 'asset-not-imported',
262
- expected: `a resolvable source for Meta dependency ${declaration.sourcePath}`,
263
- hint: 'repair the Meta source path before rebuilding the ScriptablePack',
264
- });
265
- }
155
+ const resolved = resolveAssetSource(declaration.sourcePath, declaration.value.source);
266
156
  const meta: RunImportMeta = {
267
157
  importer: declaration.value.importer,
268
- source: resolved.value,
158
+ source: resolved,
269
159
  sourceRevision: declaration.sourceRevision,
270
160
  ...(declaration.value.packageId === undefined
271
161
  ? {}
@@ -299,81 +189,629 @@ export async function declaredPackExternalOutputs(
299
189
  if (!sourcePackage.ok) {
300
190
  throw new AssetError({
301
191
  code: 'asset-not-imported',
302
- expected: `the ${declaration.value.importer} importer to produce Meta dependency outputs`,
303
- hint: `repair ${declaration.sourcePath} before rebuilding the ScriptablePack`,
192
+ expected: 'the Meta importer to produce the requested dependency',
193
+ hint: 'repair the Meta source and rerun the Pack build',
194
+ detail: { sourcePath: declaration.sourcePath },
304
195
  });
305
196
  }
306
197
  for (const asset of sourcePackage.value.product.assets) {
307
- const key = asset.guid.toLowerCase();
308
- if (!required.has(key)) continue;
198
+ if (!required.has(asset.guid.toLowerCase())) continue;
309
199
  const parsed = AssetGuid.parse(asset.guid);
310
200
  if (!parsed.ok) throw parsed.error;
201
+ const declared = declaration.value.subAssets.find(
202
+ (candidate) => candidate.guid.toLowerCase() === asset.guid.toLowerCase(),
203
+ );
311
204
  outputs.push({
312
205
  guid: parsed.value,
313
- asset: { kind: asset.kind, ...(asset.payload as Record<string, unknown>) } as Asset,
206
+ sourceKey: declared?.sourceKey ?? asset.guid,
207
+ asset: { ...(asset.payload as Record<string, unknown>), kind: asset.kind } as Asset,
314
208
  });
315
209
  }
316
210
  continue;
317
211
  }
318
212
  if (declaration.format !== 'pack.json') continue;
319
- const declaredAssets = declaration.value.assets.filter((asset) =>
320
- required.has(asset.guid.toLowerCase()),
321
- );
322
- if (declaredAssets.length === 0) continue;
213
+ if (declaration.value.schemaVersion === '3.0.0') {
214
+ const parsed = parseParameterizedPackJson(declaration.value);
215
+ if (!parsed.ok || parsed.value.format !== 'direct') continue;
216
+ const projected = projectDirectPackJson(parsed.value);
217
+ if (!projected.ok) continue;
218
+ const prepared = await prepareDirectPackTransport({
219
+ projected: projected.value,
220
+ sourcePath: declaration.sourcePath,
221
+ sourceRevision: declaration.sourceRevision,
222
+ availableGuids: available,
223
+ ...(cookers.length === 0 ? {} : { cookers }),
224
+ policy: {
225
+ base: '/',
226
+ packagePath: `assets/${projected.value.packageId}.pack.json`,
227
+ artifactPath: (assetGuid, key) => `${assetGuid}/${key}.bin`,
228
+ sink: () => {},
229
+ },
230
+ });
231
+ if (!prepared.ok) throw prepared.error;
232
+ const cookedByGuid = new Map(
233
+ prepared.value.finalized.pack.assets.map((asset) => [asset.guid.toLowerCase(), asset]),
234
+ );
235
+ for (const asset of projected.value.assets) {
236
+ if (!required.has(asset.guid.toLowerCase())) continue;
237
+ const parsedGuid = AssetGuid.parse(asset.guid);
238
+ if (!parsedGuid.ok) throw parsedGuid.error;
239
+ const cooked = cookedByGuid.get(asset.guid.toLowerCase());
240
+ if (cooked === undefined) {
241
+ throw new AssetError({
242
+ code: 'asset-not-imported',
243
+ expected: 'the direct Pack producer to retain every declared output',
244
+ hint: 'repair the direct Pack output and rerun the Pack build',
245
+ detail: { sourcePath: declaration.sourcePath },
246
+ });
247
+ }
248
+ outputs.push({
249
+ guid: parsedGuid.value,
250
+ sourceKey: asset.sourceKey,
251
+ asset: { kind: cooked.kind, ...cooked.payload } as Asset,
252
+ });
253
+ }
254
+ continue;
255
+ }
323
256
  const cooked = await readCookedAuthoredPack(declaration.value, cookers, declaration.sourcePath);
324
- for (const asset of cooked?.logicalPackage.assets.filter((item) =>
325
- required.has(item.guid.toLowerCase()),
326
- ) ?? declaredAssets) {
327
- const parsed = AssetGuid.parse(asset.guid);
328
- if (!parsed.ok) throw parsed.error;
329
- outputs.push({ guid: parsed.value, asset: { kind: asset.kind, ...asset.payload } as Asset });
257
+ for (const asset of cooked?.logicalPackage.assets ?? declaration.value.assets) {
258
+ if (!required.has(asset.guid.toLowerCase())) continue;
259
+ const parsedGuid = AssetGuid.parse(asset.guid);
260
+ if (!parsedGuid.ok) throw parsedGuid.error;
261
+ outputs.push({
262
+ guid: parsedGuid.value,
263
+ sourceKey: asset.sourceKey ?? asset.guid,
264
+ asset: { ...asset.payload, kind: asset.kind } as Asset,
265
+ });
330
266
  }
331
267
  }
332
268
  return outputs;
333
269
  }
334
270
 
335
- /** Normalize one authored Pack and, when needed, run its registered cooker once. */
336
- export async function prepareAuthoredPackTransport(
337
- authoredPack: AuthoredPackInput,
271
+ /** Cook the explicit legacy Pack v2 transport without exposing v1 authoring APIs. */
272
+ export async function readCookedAuthoredPack(
273
+ authoredPack: LegacyPackInventoryDocument,
274
+ cookers: readonly NativeCooker[] = [],
275
+ sourcePath?: string,
276
+ ): Promise<
277
+ | {
278
+ readonly logicalPackage: Pick<DdcPack, 'assets'>;
279
+ readonly refsByGuid: ReadonlyMap<string, readonly string[]>;
280
+ }
281
+ | undefined
282
+ > {
283
+ if (authoredPack.schemaVersion !== '2.0.0') return undefined;
284
+ const registry = new NativeCookerRegistry();
285
+ for (const cooker of cookers) registry.register(cooker);
286
+ const assets: PackageProductAsset[] = [];
287
+ const refsByGuid = new Map<string, readonly string[]>();
288
+ let hasCookedAsset = false;
289
+ for (const asset of authoredPack.assets) {
290
+ if (asset.execution !== 'cooked') {
291
+ assets.push({
292
+ guid: asset.guid,
293
+ kind: asset.kind,
294
+ ...(asset.name === undefined ? {} : { name: asset.name }),
295
+ payload: asset.payload,
296
+ refs: asset.refs,
297
+ artifacts: {},
298
+ });
299
+ continue;
300
+ }
301
+ if (isEngineOwnedMaterial(asset)) {
302
+ assets.push({
303
+ guid: asset.guid,
304
+ kind: asset.kind,
305
+ ...(asset.name === undefined ? {} : { name: asset.name }),
306
+ payload: asset.payload,
307
+ refs: asset.refs,
308
+ artifacts: {},
309
+ });
310
+ continue;
311
+ }
312
+ hasCookedAsset = true;
313
+ const result = await registry.runDraft(asset.kind, {
314
+ guid: asset.guid,
315
+ // Pack v2 stores the asset kind beside payload. Native producer APIs
316
+ // consume the runtime Asset shape, so reconstruct that one boundary
317
+ // field before invoking the cooker.
318
+ source: nativeCookSource(asset),
319
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
320
+ ...(sourcePath === undefined ? {} : { sourcePath }),
321
+ refs: asset.refs,
322
+ });
323
+ if (!result.ok) throw result.error;
324
+ const draft = result.value;
325
+ refsByGuid.set(asset.guid.toLowerCase(), [...draft.refs]);
326
+ assets.push({
327
+ guid: draft.guid,
328
+ kind: asset.kind,
329
+ ...(asset.name === undefined ? {} : { name: asset.name }),
330
+ payload: draft.payload as Record<string, unknown>,
331
+ refs: [...draft.refs],
332
+ artifacts: draft.artifacts,
333
+ });
334
+ }
335
+ return hasCookedAsset
336
+ ? {
337
+ logicalPackage: { assets },
338
+ refsByGuid,
339
+ }
340
+ : undefined;
341
+ }
342
+
343
+ export interface LegacyPackTransport {
344
+ readonly pack: LegacyPackInventoryDocument;
345
+ readonly firstGuid?: string;
346
+ readonly cooked?: Awaited<ReturnType<typeof readCookedAuthoredPack>>;
347
+ readonly finalized?: FinalizedPackageTransport;
348
+ }
349
+
350
+ export interface DirectPackTransport {
351
+ readonly projected: {
352
+ readonly packageId: string;
353
+ readonly assets: readonly DirectPackAssetProjection[];
354
+ };
355
+ readonly product: ParameterizedScriptablePackBuildProduct;
356
+ readonly finalized: FinalizedPackageTransport;
357
+ readonly facts: ScriptablePackPublicationFacts;
358
+ readonly revision: ResourceRevision;
359
+ }
360
+
361
+ export interface DirectPackTransportInput {
362
+ readonly projected: DirectPackTransport['projected'];
363
+ readonly sourcePath: string;
364
+ readonly displaySourcePath?: string;
365
+ readonly sourceRevision: string;
366
+ readonly policy: PackageFinalizePolicy;
367
+ readonly availableGuids?: ReadonlySet<string>;
368
+ readonly cookers?: readonly NativeCooker[];
369
+ }
370
+
371
+ function directReference(guid: string): AssetRef {
372
+ return { guid, sourceField: { fieldName: 'refs' } };
373
+ }
374
+
375
+ function mergeDirectReferences(
376
+ produced: readonly AssetRef[],
377
+ declared: readonly string[],
378
+ ): readonly AssetRef[] {
379
+ const merged = [...produced];
380
+ const seen = new Set(produced.map((reference) => reference.guid.toLowerCase()));
381
+ for (const guid of declared) {
382
+ const parsed = AssetGuid.parse(guid);
383
+ if (!parsed.ok) throw parsed.error;
384
+ const normalized = AssetGuid.format(parsed.value);
385
+ if (seen.has(normalized.toLowerCase())) continue;
386
+ seen.add(normalized.toLowerCase());
387
+ merged.push(directReference(normalized));
388
+ }
389
+ return merged;
390
+ }
391
+
392
+ function directReferenceFailure(
393
+ sourcePath: string,
394
+ guids: readonly string[],
395
+ ): {
396
+ readonly code: 'pack-output-reference-missing';
397
+ readonly expected: string;
398
+ readonly hint: string;
399
+ readonly detail: { readonly sourcePath: string; readonly guids: readonly string[] };
400
+ } {
401
+ return {
402
+ code: 'pack-output-reference-missing',
403
+ expected: 'every direct Pack reference to resolve to a local or published AssetGuid',
404
+ hint: 'publish the referenced Pack or repair the direct refs before rebuilding the Pack',
405
+ detail: { sourcePath, guids: [...guids].sort() },
406
+ };
407
+ }
408
+
409
+ function directInputFingerprint(
410
+ sourceRevision: string,
411
+ nativeFingerprints: ReadonlyMap<string, string>,
412
+ ): string {
413
+ if (nativeFingerprints.size === 0) return sourceRevision;
414
+ return `sha256:${packageTransportRevision({
415
+ sourceRevision,
416
+ nativeCookers: [...nativeFingerprints.entries()].sort(([left], [right]) =>
417
+ left.localeCompare(right),
418
+ ),
419
+ })}`;
420
+ }
421
+
422
+ function importedAssetArtifacts(
423
+ assets: readonly ImportedAsset<unknown>[],
424
+ ): Readonly<Record<string, ImportedArtifactBody>> {
425
+ return Object.fromEntries(
426
+ assets.flatMap((asset) =>
427
+ Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact]),
428
+ ),
429
+ );
430
+ }
431
+
432
+ function rawDirectProduct(
433
+ input: DirectPackTransportInput,
434
+ ): ParameterizedScriptablePackBuildProduct {
435
+ const inputFingerprint = `sha256:${packageTransportRevision({
436
+ packageId: input.projected.packageId,
437
+ sourceRevision: input.sourceRevision,
438
+ assets: input.projected.assets,
439
+ })}`;
440
+ const assets: ImportedAsset<unknown>[] = input.projected.assets.map((asset) => ({
441
+ guid: asset.guid,
442
+ kind: asset.kind,
443
+ ...(asset.name === undefined ? {} : { name: asset.name }),
444
+ payload: directRuntimePayload(asset),
445
+ refs: asset.refs.map(directReference),
446
+ artifacts: {},
447
+ }));
448
+ return {
449
+ product: {
450
+ assets,
451
+ sourceDependencies: [input.sourcePath],
452
+ refs: assets.flatMap((asset) => asset.refs),
453
+ artifacts: importedAssetArtifacts(assets),
454
+ receipts: assets.map((asset) => ({
455
+ guid: asset.guid,
456
+ origin: 'authoredPack' as const,
457
+ status: 'succeeded' as const,
458
+ inputFingerprint,
459
+ })),
460
+ diagnostics: [],
461
+ sourceRevision: inputFingerprint,
462
+ sourceKey: input.sourcePath,
463
+ },
464
+ stagedOutputs: input.projected.assets.map((asset) => ({
465
+ guid: directGuid(asset.guid),
466
+ sourceKey: asset.sourceKey,
467
+ asset: {
468
+ ...(directRuntimePayload(asset) as Record<string, unknown>),
469
+ kind: asset.kind,
470
+ } as Asset,
471
+ digest: `sha256:${packageTransportRevision({ kind: asset.kind, payload: asset.payload })}`,
472
+ })),
473
+ externalEvidence: [],
474
+ inputFingerprint,
475
+ };
476
+ }
477
+
478
+ function directGuid(value: string): AssetGuidType {
479
+ const parsed = AssetGuid.parse(value);
480
+ if (!parsed.ok) throw parsed.error;
481
+ return parsed.value;
482
+ }
483
+
484
+ function nativeRefs(refs: readonly string[]): AssetRef[] {
485
+ return refs.map((guid) => {
486
+ const parsed = AssetGuid.parse(guid);
487
+ if (!parsed.ok) throw parsed.error;
488
+ return { guid: AssetGuid.format(parsed.value) };
489
+ });
490
+ }
491
+
492
+ function nativeCookSource(asset: { readonly kind: string; readonly payload: unknown }): unknown {
493
+ if (
494
+ asset.kind !== 'material' ||
495
+ asset.payload === null ||
496
+ typeof asset.payload !== 'object' ||
497
+ Array.isArray(asset.payload)
498
+ ) {
499
+ return asset.payload;
500
+ }
501
+ return { ...(asset.payload as Record<string, unknown>), kind: asset.kind };
502
+ }
503
+
504
+ function isEngineOwnedMaterial(asset: {
505
+ readonly kind: string;
506
+ readonly payload: unknown;
507
+ }): boolean {
508
+ if (
509
+ asset.kind !== 'material' ||
510
+ asset.payload === null ||
511
+ typeof asset.payload !== 'object' ||
512
+ Array.isArray(asset.payload)
513
+ ) {
514
+ return false;
515
+ }
516
+ return isEngineMaterial(asset.payload as Pick<MaterialAsset, 'passes'>);
517
+ }
518
+
519
+ function directRuntimePayload(asset: DirectPackAssetProjection): unknown {
520
+ if (
521
+ asset.kind !== 'ui' ||
522
+ asset.payload === null ||
523
+ typeof asset.payload !== 'object' ||
524
+ Array.isArray(asset.payload)
525
+ ) {
526
+ return asset.payload;
527
+ }
528
+ return { ...(asset.payload as Record<string, unknown>), guid: asset.guid };
529
+ }
530
+
531
+ function directAssetForProducer(asset: DirectPackAssetProjection): Asset {
532
+ const payload = {
533
+ ...(directRuntimePayload(asset) as Record<string, unknown>),
534
+ kind: asset.kind,
535
+ };
536
+ if (asset.kind !== 'mesh') return payload as Asset;
537
+ return (normalizeMeshPayload(payload, asset.refs) ?? payload) as Asset;
538
+ }
539
+
540
+ /** Cook one direct v3 Pack through the same producer/finalizer seam as pack.ts. */
541
+ export async function prepareDirectPackTransport(
542
+ input: DirectPackTransportInput,
543
+ ): Promise<Result<DirectPackTransport, unknown>> {
544
+ const localGuids = new Set(input.projected.assets.map((asset) => asset.guid.toLowerCase()));
545
+ const availableGuids = new Set(
546
+ [...(input.availableGuids ?? []), ...BUILTIN_MESH_ASSETS.map((asset) => asset.guid)].filter(
547
+ (guid) => !localGuids.has(guid.toLowerCase()),
548
+ ),
549
+ );
550
+ const directArtifacts = input.projected.assets.find(
551
+ (asset) => asset.artifacts !== undefined && Object.keys(asset.artifacts).length > 0,
552
+ );
553
+ if (directArtifacts !== undefined) {
554
+ return err({
555
+ code: 'pack-parameter-invalid',
556
+ expected: 'direct v3 authoring entries to leave artifacts empty for the producer',
557
+ hint: 'remove authored artifact data and let the registered Asset producer create it',
558
+ detail: { sourcePath: input.sourcePath, sourceKey: directArtifacts.sourceKey },
559
+ });
560
+ }
561
+ const packageId = PackageId.parse(input.projected.packageId);
562
+ if (!packageId.ok) return err(packageId.error);
563
+ const cookerRegistry = new NativeCookerRegistry();
564
+ for (const cooker of input.cookers ?? []) cookerRegistry.register(cooker);
565
+ const nativeDrafts = new Map<string, NativeCookDraft>();
566
+ const nativeFingerprints = new Map<string, string>();
567
+ for (const asset of input.projected.assets) {
568
+ if (!isScriptablePackAssetKind(asset.kind)) continue;
569
+ if (cookerRegistry.get(asset.kind) === undefined) continue;
570
+ if (isEngineOwnedMaterial(asset)) continue;
571
+ const draft = await cookerRegistry.runDraft(asset.kind, {
572
+ guid: asset.guid,
573
+ // Direct v3 entries keep `kind` at the entry boundary. Reattach it for
574
+ // producer contracts such as MaterialAsset, whose payload is otherwise
575
+ // intentionally allowed to omit the discriminant.
576
+ source: nativeCookSource(asset),
577
+ sourceKey: asset.sourceKey,
578
+ sourcePath: input.sourcePath,
579
+ refs: asset.refs,
580
+ });
581
+ if (!draft.ok) return err(draft.error);
582
+ if (draft.value.guid.toLowerCase() !== asset.guid.toLowerCase()) {
583
+ return err({
584
+ code: 'pack-parameter-invalid',
585
+ expected: 'a direct Pack cooker to preserve the derived AssetGuid',
586
+ hint: 'repair the native cooker output GUID and rebuild the direct Pack',
587
+ detail: {
588
+ sourcePath: input.sourcePath,
589
+ sourceKey: asset.sourceKey,
590
+ expectedGuid: asset.guid,
591
+ actualGuid: draft.value.guid,
592
+ },
593
+ });
594
+ }
595
+ nativeDrafts.set(asset.guid.toLowerCase(), draft.value);
596
+ nativeFingerprints.set(asset.sourceKey, draft.value.inputFingerprint);
597
+ }
598
+ const unsupported = input.projected.assets.filter(
599
+ (asset) => !isScriptablePackAssetKind(asset.kind),
600
+ );
601
+ if (unsupported.length > 0 && unsupported.length !== input.projected.assets.length) {
602
+ return err({
603
+ code: 'pack-parameter-invalid',
604
+ expected: 'a direct Pack to contain only producer-backed Engine Assets or only direct PODs',
605
+ hint: 'split custom direct PODs from producer-backed Assets into separate Packs',
606
+ detail: {
607
+ sourcePath: input.sourcePath,
608
+ unsupportedKinds: [...new Set(unsupported.map((asset) => asset.kind))].sort(),
609
+ },
610
+ });
611
+ }
612
+ if (unsupported.length === input.projected.assets.length) {
613
+ const raw = rawDirectProduct(input);
614
+ const known = new Set([...availableGuids, ...localGuids]);
615
+ const missing = new Set<string>();
616
+ for (const asset of raw.product.assets) {
617
+ for (const reference of asset.refs) {
618
+ if (!known.has(reference.guid.toLowerCase())) missing.add(reference.guid.toLowerCase());
619
+ }
620
+ }
621
+ if (missing.size > 0) return err(directReferenceFailure(input.sourcePath, [...missing]));
622
+ const finalized = await finalizePackageTransportSource(
623
+ projectImportProductForBuild(raw.product),
624
+ input.policy,
625
+ );
626
+ const facts = projectParameterizedPackPublication(raw);
627
+ if (!facts.ok) return facts;
628
+ const displaySourcePath = input.displaySourcePath ?? input.sourcePath;
629
+ const revision = await scriptablePackResourceRevision(displaySourcePath, raw.inputFingerprint, [
630
+ { path: input.sourcePath, digest: input.sourceRevision },
631
+ ]);
632
+ return ok({
633
+ projected: input.projected,
634
+ product: raw,
635
+ finalized,
636
+ facts: facts.value,
637
+ revision,
638
+ });
639
+ }
640
+ const definition = {
641
+ schemaVersion: '2.0.0' as const,
642
+ packageId: packageId.value,
643
+ build: () =>
644
+ ok(
645
+ Object.fromEntries(
646
+ input.projected.assets.map((asset) => [
647
+ asset.sourceKey,
648
+ {
649
+ ...((nativeDrafts.get(asset.guid.toLowerCase())?.payload as
650
+ | Record<string, unknown>
651
+ | undefined) ?? directAssetForProducer(asset)),
652
+ kind: asset.kind,
653
+ } as Asset,
654
+ ]),
655
+ ),
656
+ ),
657
+ } as AnyParameterizedPackDefinition;
658
+ const outputs = createStandardAssetOutputProducerRegistry();
659
+ outputs.register(createPreExternalizedSceneAssetOutputProducer());
660
+ const built = await buildParameterizedScriptablePack({
661
+ definition,
662
+ sourcePath: input.sourcePath,
663
+ outputs,
664
+ sourceClosure: [{ path: input.sourcePath, digest: input.sourceRevision }],
665
+ availableGuids,
666
+ deferReferenceValidation: true,
667
+ });
668
+ if (!built.ok) return built;
669
+ const declaredByGuid = new Map(
670
+ input.projected.assets.map((asset) => [asset.guid.toLowerCase(), asset.refs]),
671
+ );
672
+ const assets = built.value.product.assets.map((asset) => ({
673
+ ...asset,
674
+ ...(nativeDrafts.has(asset.guid.toLowerCase())
675
+ ? {
676
+ payload: nativeDrafts.get(asset.guid.toLowerCase())?.payload as Record<string, unknown>,
677
+ refs: mergeDirectReferences(
678
+ nativeRefs(nativeDrafts.get(asset.guid.toLowerCase())?.refs ?? []),
679
+ declaredByGuid.get(asset.guid.toLowerCase()) ?? [],
680
+ ),
681
+ artifacts: nativeDrafts.get(asset.guid.toLowerCase())?.artifacts ?? {},
682
+ }
683
+ : {
684
+ refs: mergeDirectReferences(
685
+ asset.refs,
686
+ declaredByGuid.get(asset.guid.toLowerCase()) ?? [],
687
+ ),
688
+ }),
689
+ }));
690
+ const missing = new Set<string>();
691
+ const known = new Set([...availableGuids, ...localGuids]);
692
+ for (const asset of assets) {
693
+ for (const reference of asset.refs) {
694
+ if (!known.has(reference.guid.toLowerCase())) missing.add(reference.guid.toLowerCase());
695
+ }
696
+ }
697
+ if (missing.size > 0) return err(directReferenceFailure(input.sourcePath, [...missing]));
698
+ const inputFingerprint = directInputFingerprint(built.value.inputFingerprint, nativeFingerprints);
699
+ const stagedOutputs = input.projected.assets.map((asset) => ({
700
+ guid: directGuid(asset.guid),
701
+ sourceKey: asset.sourceKey,
702
+ asset: directAssetForProducer(asset),
703
+ digest: `sha256:${packageTransportRevision({ kind: asset.kind, payload: asset.payload })}`,
704
+ }));
705
+ const product: ParameterizedScriptablePackBuildProduct = {
706
+ ...built.value,
707
+ inputFingerprint,
708
+ stagedOutputs,
709
+ product: {
710
+ ...built.value.product,
711
+ assets,
712
+ refs: assets.flatMap((asset) => asset.refs),
713
+ artifacts: importedAssetArtifacts(assets),
714
+ receipts: built.value.product.receipts.map((receipt) => ({
715
+ ...receipt,
716
+ inputFingerprint,
717
+ })),
718
+ sourceRevision: inputFingerprint,
719
+ },
720
+ };
721
+ const finalized = await finalizePackageTransportSource(
722
+ projectImportProductForBuild(product.product),
723
+ input.policy,
724
+ );
725
+ const facts = projectParameterizedPackPublication(product);
726
+ if (!facts.ok) return facts;
727
+ const displaySourcePath = input.displaySourcePath ?? input.sourcePath;
728
+ const revision = await scriptablePackResourceRevision(
729
+ displaySourcePath,
730
+ product.inputFingerprint,
731
+ [{ path: input.sourcePath, digest: input.sourceRevision }],
732
+ );
733
+ return ok({
734
+ projected: input.projected,
735
+ product,
736
+ finalized,
737
+ facts: facts.value,
738
+ revision,
739
+ });
740
+ }
741
+
742
+ /** Normalize one explicit Pack v2 transport and invoke its registered cookers once. */
743
+ export async function prepareLegacyPackTransport(
744
+ authoredPack: LegacyPackInventoryDocument,
338
745
  cookers: readonly NativeCooker[] | undefined,
339
746
  policyFor: (guid: string) => PackageFinalizePolicy,
340
747
  sourcePath?: string,
341
- ): Promise<AuthoredPackTransport> {
342
- const pack = upgradeLegacyAuthoredPack(authoredPack);
343
- const firstGuid = pack.assets?.[0]?.guid?.toLowerCase();
344
- if (pack.schemaVersion !== '2.0.0' || firstGuid === undefined) {
345
- return { pack, ...(firstGuid === undefined ? {} : { firstGuid }) };
748
+ ): Promise<LegacyPackTransport> {
749
+ const firstGuid = authoredPack.assets[0]?.guid?.toLowerCase();
750
+ if (authoredPack.schemaVersion !== '2.0.0' || firstGuid === undefined) {
751
+ return { pack: authoredPack, ...(firstGuid === undefined ? {} : { firstGuid }) };
346
752
  }
347
- const cooked = await readCookedAuthoredPack(pack, cookers, sourcePath);
348
- if (cooked === undefined) return { pack, firstGuid };
753
+ const cooked = await readCookedAuthoredPack(authoredPack, cookers, sourcePath);
754
+ if (cooked === undefined) return { pack: authoredPack, firstGuid };
349
755
  return {
350
- pack,
756
+ pack: authoredPack,
351
757
  firstGuid,
352
758
  cooked,
353
759
  finalized: await finalizePackageTransportSource(cooked.logicalPackage, policyFor(firstGuid)),
354
760
  };
355
761
  }
356
762
 
357
- /** Project one produced ScriptablePack into the publication tuple shared by dev and build. */
358
- export function projectScriptablePackPublication(
359
- product: ScriptableSourcePackageProduct,
763
+ /** Materialize a dynamic Pack product through the same dev transport seam. */
764
+ export async function materializePreparedParameterizedScriptablePack(
765
+ prepared: PreparedParameterizedScriptablePack,
766
+ paths: ScriptablePackTransportPaths,
767
+ sink: ScriptablePackTransportSink,
768
+ ): Promise<ReadonlyMap<string, string>> {
769
+ const { product, finalized } = prepared;
770
+ await sink.writePackage(paths.packagePath, JSON.stringify(finalized.pack));
771
+ for (const artifact of finalized.artifacts) {
772
+ await sink.writeArtifact(
773
+ paths.artifactPath(artifact.path),
774
+ artifact.bytes,
775
+ artifact.path.endsWith('.json') ? 'application/json' : artifact.mediaType,
776
+ );
777
+ }
778
+ const receipts = new Map<string, string>();
779
+ for (const asset of product.product.assets) {
780
+ const path = paths.receiptPath(asset.guid);
781
+ await sink.writeReceipt(
782
+ path,
783
+ JSON.stringify({
784
+ guid: asset.guid,
785
+ origin: 'sourceMeta',
786
+ status: 'succeeded',
787
+ inputFingerprint: product.inputFingerprint,
788
+ outputDigest: finalized.digest,
789
+ }),
790
+ );
791
+ receipts.set(asset.guid.toLowerCase(), path);
792
+ }
793
+ return receipts;
794
+ }
795
+
796
+ function projectParameterizedPackPublication(
797
+ product: ParameterizedScriptablePackBuildProduct,
360
798
  ): Result<ScriptablePackPublicationFacts, ScriptablePackDomainError> {
361
799
  const assets = new Map(product.product.assets.map((asset) => [asset.guid.toLowerCase(), asset]));
362
800
  const outputs: AssetPublicationOutput[] = [];
363
801
  for (const staged of product.stagedOutputs) {
364
802
  const guid = AssetGuid.format(staged.guid).toLowerCase();
365
803
  const asset = assets.get(guid);
366
- if (asset === undefined || staged.digest === undefined) {
804
+ if (asset === undefined || staged.digest === undefined || staged.sourceKey === undefined) {
367
805
  return err({
368
806
  code: 'pack-source-output-invalid',
369
- expected: `ScriptablePack publication output ${guid} to include a product and digest`,
370
- hint: 'repair the ScriptablePack producer output and rebuild',
807
+ expected: `parameterized Pack publication output ${guid} to include a product, sourceKey, and digest`,
808
+ hint: 'repair the dynamic Pack output and rebuild the current generation',
371
809
  detail: { stage: 'publication', guid },
372
810
  });
373
811
  }
374
812
  outputs.push({
375
813
  guid,
376
- sourceKey: staged.sourceKey ?? guid,
814
+ sourceKey: staged.sourceKey,
377
815
  kind: asset.kind,
378
816
  digest: staged.digest,
379
817
  refs: asset.refs.map((reference) => reference.guid),
@@ -390,154 +828,132 @@ export function projectScriptablePackPublication(
390
828
  });
391
829
  }
392
830
 
393
- /** Project the immutable module closure into the Catalog revision contract. */
394
- async function scriptablePackResourceRevision(
395
- displaySourcePath: string,
396
- digest: string,
397
- sourceClosure: readonly ScriptablePackSourceClosureEntry[],
398
- ): Promise<ResourceRevision> {
399
- const observedAt = Math.trunc(
400
- Math.max(
401
- ...(await Promise.all(sourceClosure.map(async (entry) => (await stat(entry.path)).mtimeMs))),
402
- ),
831
+ function snapshotSourceForStagedOutputs(
832
+ outputs: readonly ScriptablePackStagedOutput[],
833
+ ): import('./scriptable-pack.js').ScriptablePackAssetSnapshotSource | undefined {
834
+ if (outputs.length === 0) return undefined;
835
+ const byGuid = new Map(
836
+ outputs.map((output) => [AssetGuid.format(output.guid).toLowerCase(), output]),
403
837
  );
404
- return { digest, observedAt, rootId: displaySourcePath };
838
+ return {
839
+ async readByGuid(guid) {
840
+ const key = AssetGuid.format(guid).toLowerCase();
841
+ const output = byGuid.get(key);
842
+ if (output === undefined) {
843
+ return err(
844
+ new AssetError({
845
+ code: 'asset-not-found',
846
+ expected: `an available Asset snapshot for ${key}`,
847
+ hint: 'publish the referenced Pack or repair the content dependency',
848
+ }),
849
+ );
850
+ }
851
+ return ok({
852
+ asset: structuredClone(output.asset),
853
+ generation: 1,
854
+ digest: output.digest ?? 'sha256:staged',
855
+ });
856
+ },
857
+ };
405
858
  }
406
859
 
407
- /** Produce selected ScriptablePacks through one inventory and one staged owner boundary. */
408
- export async function produceScriptablePackProducts(
409
- sources: readonly ScriptablePackInput[],
410
- declaredExternalOutputs: readonly ScriptablePackStagedOutput[] = [],
411
- ): Promise<Result<ReadonlyMap<string, PreparedScriptablePack>, ScriptablePackHostError>> {
412
- const entries = new Map<
413
- string,
414
- {
415
- readonly source: ScriptablePackInput;
416
- readonly definition: Readonly<ScriptablePackDefinition>;
417
- readonly closure: readonly ScriptablePackSourceClosureEntry[];
418
- }
419
- >();
420
- for (const source of sources) {
421
- if (entries.has(source.displaySourcePath)) continue;
422
- entries.set(source.displaySourcePath, {
423
- source,
424
- definition: source.definition,
425
- closure: source.sourceClosure,
426
- });
427
- }
860
+ function composeParameterizedAssetSource(
861
+ staged: import('./scriptable-pack.js').ScriptablePackAssetSnapshotSource | undefined,
862
+ published: import('./scriptable-pack.js').ScriptablePackAssetSnapshotSource | undefined,
863
+ ): import('./scriptable-pack.js').ScriptablePackAssetSnapshotSource | undefined {
864
+ if (staged === undefined) return published;
865
+ if (published === undefined) return staged;
866
+ return {
867
+ async readByGuid(guid) {
868
+ const local = await staged.readByGuid(guid);
869
+ if (local.ok || local.error.code !== 'asset-not-found') return local;
870
+ return published.readByGuid(guid);
871
+ },
872
+ };
873
+ }
428
874
 
429
- const products = new Map<string, ScriptableSourcePackageProduct>();
430
- const owners: ScriptablePackStagedOwner[] = [...entries.entries()].map(
431
- ([displaySourcePath, entry]) => ({
432
- id: displaySourcePath,
433
- guids: Object.values(entry.definition.assets).map((asset) => asset.guid),
434
- async build(source) {
435
- const produced = await produceScriptableSourcePackage({
436
- definition: entry.definition,
437
- sourcePath: displaySourcePath,
438
- assetSource: source,
439
- outputs: createStandardAssetOutputProducerRegistry(
440
- projectScriptablePackSceneComponents(entry.definition.sceneComponents),
441
- ),
442
- sourceClosure: entry.closure,
443
- authoringContractVersion: 'scriptable-pack-production/1',
444
- });
445
- if (!produced.ok) return produced;
446
- const observedClosure = await inventoryScriptablePackSource(entry.source.sourcePath);
447
- if (JSON.stringify(observedClosure) !== JSON.stringify(entry.closure)) {
448
- return err({
449
- code: 'pack-source-load-failed',
450
- expected: 'the complete ScriptablePack module closure to remain fixed during one build',
451
- hint: 'retry the build after source and helper writes have settled',
452
- detail: {
453
- sourcePath: displaySourcePath,
454
- reason: 'source-changed',
455
- phase: 'build',
456
- diagnostic: 'module closure changed while the staged generation was building',
457
- },
458
- });
459
- }
460
- products.set(displaySourcePath, produced.value);
461
- return ok(produced.value.stagedOutputs);
462
- },
463
- }),
875
+ /**
876
+ * Build parameterized sources in one generation and hand their terminal
877
+ * products to the ordinary Pack v2 finalizer. The worklist is deliberately
878
+ * below publication: it only decides which current-generation products exist.
879
+ */
880
+ export async function produceParameterizedScriptablePackProducts(
881
+ options: ParameterizedScriptablePackProductionOptions,
882
+ ): Promise<Result<ReadonlyMap<string, PreparedParameterizedScriptablePack>, unknown>> {
883
+ if (options.sources.length === 0) return ok(new Map());
884
+ const external = options.declaredExternalOutputs ?? [];
885
+ const assetSource = composeParameterizedAssetSource(
886
+ snapshotSourceForStagedOutputs(external),
887
+ options.assetSource,
464
888
  );
465
- const generationDigest = createHash('sha256')
466
- .update(
467
- JSON.stringify(
468
- [...entries.values()]
469
- .flatMap((entry) => entry.closure)
470
- .sort((a, b) => a.path.localeCompare(b.path)),
889
+ const availableGuids = new Set([
890
+ ...(options.availableGuids ?? []),
891
+ ...external.map((output) => AssetGuid.format(output.guid).toLowerCase()),
892
+ ]);
893
+ const workItems: ParameterizedScriptablePackWorkItem[] = options.sources.map((source) => ({
894
+ definition: source.definition,
895
+ sourcePath: source.sourcePath,
896
+ ...(source.subjectPackageId === undefined ? {} : { subjectPackageId: source.subjectPackageId }),
897
+ ...(source.values === undefined ? {} : { values: source.values }),
898
+ ...(source.inheritedValues === undefined ? {} : { inheritedValues: source.inheritedValues }),
899
+ sourceClosure: source.sourceClosure,
900
+ }));
901
+ const worklist = await buildParameterizedScriptablePackWorklist({
902
+ subjects: workItems,
903
+ outputs: createStandardAssetOutputProducerRegistry(
904
+ options.sources.flatMap((source) =>
905
+ projectScriptablePackSceneComponents(source.definition.sceneComponents),
471
906
  ),
472
- )
473
- .digest();
474
- const stagedSource = createScriptablePackStagedAssetSnapshotSource({
475
- generation: generationDigest.readUInt32BE(0),
476
- owners,
477
- declaredExternalOutputs,
907
+ ),
908
+ ...(assetSource === undefined ? {} : { assetSource }),
909
+ availableGuids,
910
+ ...(options.incomingRefs === undefined ? {} : { incomingRefs: options.incomingRefs }),
911
+ ...(options.maxPasses === undefined ? {} : { maxPasses: options.maxPasses }),
478
912
  });
913
+ if (!worklist.ok) return worklist;
479
914
 
480
- const prepared = new Map<string, PreparedScriptablePack>();
481
- for (const source of sources) {
482
- const entry = entries.get(source.displaySourcePath);
483
- if (entry === undefined) {
484
- return err({
485
- code: 'pack-source-path-invalid',
486
- expected: 'a ScriptablePack source registered in the current inventory',
487
- actual: source.displaySourcePath,
488
- hint: 'rebuild the source inventory before producing this path',
489
- retryable: true,
490
- recoveryActions: ['rebuild-source-inventory'],
491
- detail: { sourcePath: source.displaySourcePath },
492
- });
493
- }
494
- const first = Object.values(entry.definition.assets)[0];
495
- if (first === undefined) {
496
- return err({
497
- code: 'pack-source-output-invalid',
498
- expected: 'at least one declared ScriptablePack output',
499
- hint: 'add an output descriptor before building the package',
500
- detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: [] },
501
- });
502
- }
503
- const staged = await stagedSource.readByGuid(first.guid);
504
- if (!staged.ok) return err(staged.error);
505
- const produced = products.get(source.displaySourcePath);
506
- if (produced === undefined) {
915
+ const productsBySource = new Map(
916
+ worklist.value.buildProducts
917
+ .filter((product) => product.product.sourceKey !== undefined)
918
+ .map((product) => [product.product.sourceKey as string, product]),
919
+ );
920
+ const prepared = new Map<string, PreparedParameterizedScriptablePack>();
921
+ for (const source of options.sources) {
922
+ const product = productsBySource.get(source.sourcePath);
923
+ if (product === undefined) {
507
924
  return err({
508
925
  code: 'pack-source-output-invalid',
509
- expected: 'the staged owner build to retain its source-package product',
510
- hint: 'retry the ScriptablePack production attempt',
511
- detail: {
512
- missingGuids: [AssetGuid.format(first.guid)],
513
- unexpectedSourceKeys: [],
514
- kindMismatches: [],
515
- },
926
+ expected: 'one terminal dynamic Pack product for every source subject',
927
+ hint: 'rerun the bounded generation worklist and inspect its subject map',
928
+ detail: { sourcePath: source.sourcePath },
516
929
  });
517
930
  }
518
- const transportPolicy =
519
- typeof source.policy === 'function' ? source.policy(produced) : source.policy;
520
- const logicalPackage = projectImportProductForBuild(produced.product);
521
- const finalized = await finalizePackageTransportSource(logicalPackage, transportPolicy);
522
- const facts = projectScriptablePackPublication(produced);
523
- if (!facts.ok) return err(facts.error);
931
+ const policy = typeof source.policy === 'function' ? source.policy(product) : source.policy;
932
+ const finalized = await finalizePackageTransportSource(
933
+ projectImportProductForBuild(product.product),
934
+ policy,
935
+ );
936
+ const facts = projectParameterizedPackPublication(product);
937
+ if (!facts.ok) return facts;
524
938
  const revision = await scriptablePackResourceRevision(
525
939
  source.displaySourcePath,
526
- produced.inputFingerprint,
940
+ product.inputFingerprint,
527
941
  source.sourceClosure,
528
942
  );
529
943
  const publication = createAcceptedPublication({
530
944
  sourcePath: source.displaySourcePath,
531
- sourceRevision: produced.inputFingerprint,
945
+ sourceRevision: product.inputFingerprint,
532
946
  generation: source.publicationGeneration,
533
947
  digest: finalized.digest,
534
948
  packageUrl: finalized.packageUrl,
535
- inputFingerprint: produced.inputFingerprint,
949
+ inputFingerprint: product.inputFingerprint,
536
950
  outputs: facts.value.outputs,
537
- externalEvidence: produced.externalEvidence,
951
+ externalEvidence: product.externalEvidence,
538
952
  });
539
953
  prepared.set(source.displaySourcePath, {
540
- product: produced,
954
+ product: {
955
+ ...product,
956
+ },
541
957
  finalized,
542
958
  facts: facts.value,
543
959
  revision,
@@ -546,3 +962,34 @@ export async function produceScriptablePackProducts(
546
962
  }
547
963
  return ok(prepared);
548
964
  }
965
+
966
+ /** Project the immutable module closure into the Catalog revision contract. */
967
+ async function scriptablePackResourceRevision(
968
+ displaySourcePath: string,
969
+ digest: string,
970
+ sourceClosure: readonly ScriptablePackSourceClosureEntry[],
971
+ ): Promise<ResourceRevision> {
972
+ const mtimes = await Promise.all(
973
+ sourceClosure.map(async (entry) => {
974
+ try {
975
+ return (await stat(entry.path)).mtimeMs;
976
+ } catch (error) {
977
+ // Direct Pack declarations may be supplied with a logical source path
978
+ // and sourceText rather than a file-backed module. The digest remains
979
+ // the authoritative revision in that case; preserve filesystem errors
980
+ // other than a missing path so real source failures stay visible.
981
+ if (
982
+ error !== null &&
983
+ typeof error === 'object' &&
984
+ 'code' in error &&
985
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')
986
+ ) {
987
+ return 0;
988
+ }
989
+ throw error;
990
+ }
991
+ }),
992
+ );
993
+ const observedAt = mtimes.length === 0 ? 0 : Math.trunc(Math.max(...mtimes));
994
+ return { digest, observedAt, rootId: displaySourcePath };
995
+ }