@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
@@ -0,0 +1,725 @@
1
+ import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
2
+ import {
3
+ type AnyParameterizedPackDefinition,
4
+ AssetGuid,
5
+ isScriptablePackAssetKind,
6
+ isValidPackSourceKey,
7
+ type PackAuthoringError,
8
+ PackageId,
9
+ type PackBuildContextWithoutParameters,
10
+ type PackBuildContextWithParameters,
11
+ type PackParameterDefinition,
12
+ type PackParameterValue,
13
+ resolvePackParameterValues,
14
+ } from '@forgeax/engine-pack/source';
15
+ import type {
16
+ Asset,
17
+ AssetGuid as AssetGuidType,
18
+ AssetPublicationEnvelope,
19
+ ImportedAsset,
20
+ Result,
21
+ } from '@forgeax/engine-types';
22
+ import { AssetError, err, ImportError, ok } from '@forgeax/engine-types';
23
+ import {
24
+ createImportProduct,
25
+ type ImportAssetProduct,
26
+ type TerminalImportProduct,
27
+ } from './import-product.js';
28
+ import type {
29
+ AssetOutputProducer,
30
+ AssetOutputProducerRegistry,
31
+ ScriptablePackAssetSnapshotSource,
32
+ ScriptablePackBuildProduct,
33
+ ScriptablePackDomainError,
34
+ ScriptablePackExternalEvidence,
35
+ ScriptablePackSourceClosureEntry,
36
+ ScriptablePackStagedOutput,
37
+ } from './scriptable-pack.js';
38
+
39
+ export interface ParameterizedScriptablePackBuildOptions {
40
+ readonly definition: AnyParameterizedPackDefinition;
41
+ readonly sourcePath: string;
42
+ /** Instance identity. Omitted for a root source and defaults to definition.packageId. */
43
+ readonly subjectPackageId?: PackageId;
44
+ /** Sparse instance values; defaults are applied before build. */
45
+ readonly values?: Readonly<Record<string, unknown>>;
46
+ /** Effective parent values supplied by the inheritance resolver. */
47
+ readonly inheritedValues?: Readonly<Record<string, PackParameterValue>>;
48
+ readonly assetSource?: ScriptablePackAssetSnapshotSource;
49
+ readonly outputs: AssetOutputProducerRegistry;
50
+ readonly sourceClosure?: readonly ScriptablePackSourceClosureEntry[];
51
+ readonly authoringContractVersion?: string;
52
+ /** Known published GUIDs used for the optional global reference gate. */
53
+ readonly availableGuids?: ReadonlySet<string>;
54
+ /** Worklist mode defers reference closure until every subject is materialized. */
55
+ readonly deferReferenceValidation?: boolean;
56
+ readonly publication?: AssetPublicationEnvelope;
57
+ }
58
+
59
+ export interface ParameterizedScriptablePackBuildProduct extends ScriptablePackBuildProduct {
60
+ readonly publication?: AssetPublicationEnvelope;
61
+ }
62
+
63
+ export type ParameterizedScriptablePackBuildResult = Result<
64
+ ParameterizedScriptablePackBuildProduct,
65
+ | PackAuthoringError
66
+ | ScriptablePackDomainError
67
+ | AssetError
68
+ | ImportError
69
+ | {
70
+ readonly code: string;
71
+ readonly expected: string;
72
+ readonly hint: string;
73
+ readonly detail?: unknown;
74
+ }
75
+ >;
76
+
77
+ interface ObservedRead {
78
+ readonly guid: string;
79
+ readonly asset: Asset;
80
+ readonly generation: number;
81
+ readonly digest: string;
82
+ }
83
+
84
+ function record(value: unknown): value is Record<string, unknown> {
85
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
86
+ }
87
+
88
+ function isAsset(value: unknown): value is Asset {
89
+ return record(value) && typeof value.kind === 'string' && isScriptablePackAssetKind(value.kind);
90
+ }
91
+
92
+ function clone<T>(value: T): T {
93
+ return structuredClone(value);
94
+ }
95
+
96
+ function stable(value: unknown): string {
97
+ if (value instanceof Uint8Array) return JSON.stringify(Array.from(value));
98
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
99
+ if (value !== null && typeof value === 'object') {
100
+ const object = value as Record<string, unknown>;
101
+ return `{${Object.keys(object)
102
+ .sort()
103
+ .map((key) => `${JSON.stringify(key)}:${stable(object[key])}`)
104
+ .join(',')}}`;
105
+ }
106
+ return JSON.stringify(value) ?? 'null';
107
+ }
108
+
109
+ async function digest(value: unknown): Promise<string> {
110
+ const crypto = globalThis.crypto?.subtle;
111
+ if (crypto === undefined) throw new Error('Web Crypto API is required for Pack fingerprints');
112
+ const bytes = await crypto.digest('SHA-256', new TextEncoder().encode(stable(value)));
113
+ return `sha256:${Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
114
+ }
115
+
116
+ function observedReader(source: ScriptablePackAssetSnapshotSource | undefined): {
117
+ readonly reader: {
118
+ readByGuid<TAsset extends Asset = Asset>(guid: AssetGuidType): Promise<Result<TAsset, unknown>>;
119
+ };
120
+ readonly reads: ReadonlyMap<string, ObservedRead>;
121
+ } {
122
+ const reads = new Map<string, ObservedRead>();
123
+ const reader = {
124
+ async readByGuid<TAsset extends Asset = Asset>(
125
+ guid: AssetGuidType,
126
+ ): Promise<Result<TAsset, unknown>> {
127
+ const key = AssetGuid.format(guid).toLowerCase();
128
+ const cached = reads.get(key);
129
+ if (cached !== undefined) return ok(clone(cached.asset) as TAsset);
130
+ if (source === undefined) {
131
+ return err(
132
+ new AssetError({
133
+ code: 'asset-not-found',
134
+ expected: `a published Asset snapshot for content dependency ${key}`,
135
+ hint: 'publish the dependency or remove the content read from the Pack build',
136
+ detail: { sourcePath: key },
137
+ }),
138
+ );
139
+ }
140
+ const result = await source.readByGuid(guid);
141
+ if (!result.ok) return result;
142
+ const asset = clone(result.value.asset);
143
+ reads.set(key, {
144
+ guid: key,
145
+ asset,
146
+ generation: result.value.generation,
147
+ digest: result.value.digest,
148
+ });
149
+ return ok(clone(asset) as TAsset);
150
+ },
151
+ };
152
+ return { reader, reads };
153
+ }
154
+
155
+ function buildContext(
156
+ packageId: PackageId,
157
+ values: Readonly<Record<string, PackParameterValue>> | undefined,
158
+ reader: ReturnType<typeof observedReader>['reader'],
159
+ ):
160
+ | PackBuildContextWithParameters<readonly [PackParameterDefinition, ...PackParameterDefinition[]]>
161
+ | PackBuildContextWithoutParameters {
162
+ if (values === undefined) return { packageId, readByGuid: reader.readByGuid };
163
+ return { packageId, values, readByGuid: reader.readByGuid } as PackBuildContextWithParameters<
164
+ readonly [PackParameterDefinition, ...PackParameterDefinition[]]
165
+ >;
166
+ }
167
+
168
+ function sourceKeyFailure(sourcePath: string, sourceKey: unknown): PackAuthoringError {
169
+ return {
170
+ code: 'pack-source-key-invalid',
171
+ expected: 'a sourceKey matching the Pack source-key grammar',
172
+ hint: 'return stable lower-case semantic keys instead of paths or output indexes',
173
+ detail: { sourcePath, sourceKey },
174
+ };
175
+ }
176
+
177
+ function outputValueError(
178
+ sourcePath: string,
179
+ sourceKey: string,
180
+ expected: string,
181
+ actual: unknown,
182
+ ): PackAuthoringError {
183
+ return {
184
+ code: 'pack-parameter-invalid',
185
+ expected,
186
+ hint: 'repair the build output and rebuild the Pack from a fresh generation',
187
+ detail: { sourcePath, sourceKey, actual: typeof actual === 'string' ? actual : typeof actual },
188
+ };
189
+ }
190
+
191
+ function referenceError(
192
+ code: 'pack-output-reference-missing' | 'pack-output-reference-conflict',
193
+ sourcePath: string,
194
+ guids: readonly string[],
195
+ ): PackAuthoringError {
196
+ return {
197
+ code,
198
+ expected:
199
+ code === 'pack-output-reference-missing'
200
+ ? 'every output reference to resolve to a local or published AssetGuid'
201
+ : 'removed output GUIDs to have no incoming references',
202
+ hint:
203
+ code === 'pack-output-reference-missing'
204
+ ? 'build or publish the referenced Pack before verifying this output'
205
+ : 'migrate incoming references before publishing the topology change',
206
+ detail: { sourcePath, guids: [...guids].sort() },
207
+ };
208
+ }
209
+
210
+ function productError(
211
+ sourcePath: string,
212
+ sourceKey: string,
213
+ producer: AssetOutputProducer,
214
+ product: unknown,
215
+ ): PackAuthoringError | undefined {
216
+ if (!record(product)) {
217
+ return outputValueError(
218
+ sourcePath,
219
+ sourceKey,
220
+ `producer ${producer.kind} to return an asset product object`,
221
+ product,
222
+ );
223
+ }
224
+ if (!Array.isArray(product.refs)) {
225
+ return outputValueError(
226
+ sourcePath,
227
+ sourceKey,
228
+ `producer ${producer.kind} to return a refs array`,
229
+ product.refs,
230
+ );
231
+ }
232
+ if (!record(product.artifacts)) {
233
+ return outputValueError(
234
+ sourcePath,
235
+ sourceKey,
236
+ `producer ${producer.kind} to return an artifacts object`,
237
+ product.artifacts,
238
+ );
239
+ }
240
+ const payload = product.payload;
241
+ if (!record(payload) || payload.kind !== producer.kind) {
242
+ return outputValueError(
243
+ sourcePath,
244
+ sourceKey,
245
+ `producer ${producer.kind} to return a matching payload kind`,
246
+ payload,
247
+ );
248
+ }
249
+ for (const ref of product.refs) {
250
+ if (!record(ref) || typeof ref.guid !== 'string' || !AssetGuid.parse(ref.guid).ok) {
251
+ return outputValueError(
252
+ sourcePath,
253
+ sourceKey,
254
+ 'producer refs to contain valid AssetGuid values',
255
+ ref,
256
+ );
257
+ }
258
+ }
259
+ for (const [key, artifact] of Object.entries(product.artifacts)) {
260
+ if (
261
+ key.length === 0 ||
262
+ key.startsWith('/') ||
263
+ key.includes('..') ||
264
+ key.includes('\\') ||
265
+ !record(artifact) ||
266
+ typeof artifact.mediaType !== 'string' ||
267
+ !(artifact.bytes instanceof Uint8Array)
268
+ ) {
269
+ return outputValueError(
270
+ sourcePath,
271
+ sourceKey,
272
+ 'asset-local artifacts with safe keys and bytes',
273
+ key,
274
+ );
275
+ }
276
+ }
277
+ return undefined;
278
+ }
279
+
280
+ function errorCode(value: unknown): string | undefined {
281
+ return record(value) && typeof value.code === 'string' ? value.code : undefined;
282
+ }
283
+
284
+ function normalizedGuidSet(value: ReadonlySet<string> | undefined): Set<string> | undefined {
285
+ return value === undefined ? undefined : new Set([...value].map((guid) => guid.toLowerCase()));
286
+ }
287
+
288
+ /**
289
+ * Execute a v2 Pack source and project its dynamic output map through the
290
+ * existing importer producers. No output GUID or output manifest is read.
291
+ */
292
+ export async function buildParameterizedScriptablePack(
293
+ options: ParameterizedScriptablePackBuildOptions,
294
+ ): Promise<ParameterizedScriptablePackBuildResult> {
295
+ const subjectPackageId = options.subjectPackageId ?? options.definition.packageId;
296
+ const availableGuids = normalizedGuidSet(options.availableGuids);
297
+ const observed = observedReader(options.assetSource);
298
+ let effectiveValues: Readonly<Record<string, PackParameterValue>> | undefined;
299
+ if ('parameters' in options.definition) {
300
+ const resolved = resolvePackParameterValues(
301
+ options.definition,
302
+ options.values ?? {},
303
+ options.inheritedValues,
304
+ );
305
+ if (!resolved.ok) return resolved;
306
+ effectiveValues = resolved.value;
307
+ } else if (options.values !== undefined && Object.keys(options.values).length > 0) {
308
+ return err(
309
+ outputValueError(
310
+ options.sourcePath,
311
+ '$.values',
312
+ 'zero-parameter Packs to omit values and instance capabilities',
313
+ options.values,
314
+ ),
315
+ );
316
+ } else if (
317
+ options.subjectPackageId !== undefined &&
318
+ PackageId.format(options.subjectPackageId).toLowerCase() !==
319
+ PackageId.format(options.definition.packageId).toLowerCase()
320
+ ) {
321
+ return err({
322
+ code: 'pack-parent-not-parameterized',
323
+ expected: 'a parameterized Pack source for an independent instance packageId',
324
+ hint: 'use clone for a zero-parameter Pack instead of building it as an instance',
325
+ detail: {
326
+ sourcePath: options.sourcePath,
327
+ rootPackageId: PackageId.format(options.definition.packageId),
328
+ subjectPackageId: PackageId.format(options.subjectPackageId),
329
+ },
330
+ });
331
+ }
332
+
333
+ let built:
334
+ | Result<Record<string, Asset>, unknown>
335
+ | Promise<Result<Record<string, Asset>, unknown>>;
336
+ try {
337
+ const context = buildContext(subjectPackageId, effectiveValues, observed.reader);
338
+ built = options.definition.build(context as never);
339
+ built = await built;
340
+ } catch (cause) {
341
+ return err(
342
+ new ImportError({
343
+ code: 'import-internal-error',
344
+ expected: 'Pack build to return a structured Result without throwing',
345
+ hint: 'repair the authoring function and return err(...) for expected failures',
346
+ detail: {
347
+ reason: `${options.sourcePath}: ${cause instanceof Error ? cause.message : String(cause)}`,
348
+ },
349
+ }),
350
+ );
351
+ }
352
+ if (!record(built) || typeof built.ok !== 'boolean') {
353
+ return err(
354
+ new ImportError({
355
+ code: 'import-internal-error',
356
+ expected: 'Pack build to return a Result object',
357
+ hint: 'return ok(sourceKeyToAsset) or err(structuredError) from the authoring function',
358
+ detail: { reason: `${options.sourcePath}: malformed build result` },
359
+ }),
360
+ );
361
+ }
362
+ if (!built.ok) {
363
+ if (errorCode(built.error) !== undefined) return err(built.error as never);
364
+ return err(
365
+ new ImportError({
366
+ code: 'import-internal-error',
367
+ expected: 'a structured Pack build error',
368
+ hint: 'return an error carrying code, expected, hint and detail',
369
+ detail: { reason: `${options.sourcePath}: ${String(built.error)}` },
370
+ }),
371
+ );
372
+ }
373
+ if (!record(built.value)) {
374
+ return err(
375
+ outputValueError(
376
+ options.sourcePath,
377
+ '$',
378
+ 'build to return a sourceKey-to-Asset object',
379
+ built.value,
380
+ ),
381
+ );
382
+ }
383
+
384
+ const imported: ImportedAsset<unknown>[] = [];
385
+ const stagedOutputs: ScriptablePackStagedOutput[] = [];
386
+ const localGuids = new Set<string>();
387
+ for (const sourceKey of Object.keys(built.value).sort()) {
388
+ if (!isValidPackSourceKey(sourceKey))
389
+ return err(sourceKeyFailure(options.sourcePath, sourceKey));
390
+ const asset = built.value[sourceKey];
391
+ if (!isAsset(asset)) {
392
+ return err(
393
+ outputValueError(
394
+ options.sourcePath,
395
+ sourceKey,
396
+ 'a concrete Asset with a supported kind',
397
+ asset,
398
+ ),
399
+ );
400
+ }
401
+ const guid = AssetGuid.format(AssetGuid.derive(subjectPackageId, sourceKey));
402
+ const normalizedGuid = guid.toLowerCase();
403
+ if (localGuids.has(normalizedGuid) || availableGuids?.has(normalizedGuid)) {
404
+ return err({
405
+ code: 'pack-guid-collision',
406
+ expected: 'derived output GUIDs to be unique in the global source index',
407
+ hint: 'change the colliding packageId or repair the source index before publishing',
408
+ detail: { sourcePath: options.sourcePath, guid },
409
+ });
410
+ }
411
+ localGuids.add(normalizedGuid);
412
+ const producer = options.outputs.get(asset.kind);
413
+ if (producer === undefined) {
414
+ return err(
415
+ outputValueError(
416
+ options.sourcePath,
417
+ sourceKey,
418
+ `a registered output producer for ${asset.kind}`,
419
+ asset.kind,
420
+ ),
421
+ );
422
+ }
423
+ let produced: unknown;
424
+ try {
425
+ produced = await producer.produce({ guid, sourceKey, asset });
426
+ } catch (cause) {
427
+ return err(
428
+ new ImportError({
429
+ code: 'import-internal-error',
430
+ expected: `producer ${producer.kind} to return a structured Result without throwing`,
431
+ hint: 'repair the output producer and return err(...) for expected failures',
432
+ detail: {
433
+ reason: `${options.sourcePath}:${sourceKey}: ${cause instanceof Error ? cause.message : String(cause)}`,
434
+ },
435
+ }),
436
+ );
437
+ }
438
+ if (!record(produced) || typeof produced.ok !== 'boolean') {
439
+ return err(
440
+ outputValueError(
441
+ options.sourcePath,
442
+ sourceKey,
443
+ `producer ${producer.kind} to return a Result`,
444
+ produced,
445
+ ),
446
+ );
447
+ }
448
+ if (!produced.ok) {
449
+ if (errorCode(produced.error) !== undefined) return err(produced.error as never);
450
+ return err(
451
+ new ImportError({
452
+ code: 'import-internal-error',
453
+ expected: `producer ${producer.kind} to return a structured error`,
454
+ hint: 'return an error carrying code, expected, hint and detail',
455
+ detail: { reason: `${options.sourcePath}:${sourceKey}: ${String(produced.error)}` },
456
+ }),
457
+ );
458
+ }
459
+ const invalidProduct = productError(options.sourcePath, sourceKey, producer, produced.value);
460
+ if (invalidProduct !== undefined) return err(invalidProduct);
461
+ const product = produced.value as ImportAssetProduct<unknown>;
462
+ imported.push({
463
+ guid,
464
+ kind: asset.kind,
465
+ payload: product.payload,
466
+ refs: product.refs,
467
+ artifacts: product.artifacts,
468
+ });
469
+ stagedOutputs.push({
470
+ guid: AssetGuid.derive(subjectPackageId, sourceKey),
471
+ sourceKey,
472
+ asset: clone(asset),
473
+ digest: await digest(asset),
474
+ });
475
+ }
476
+
477
+ const referenced = new Set<string>();
478
+ for (const asset of imported) {
479
+ for (const ref of asset.refs) {
480
+ const guid = ref.guid.toLowerCase();
481
+ if (!localGuids.has(guid)) referenced.add(guid);
482
+ }
483
+ }
484
+ if (options.deferReferenceValidation !== true) {
485
+ const missing = [...referenced].filter(
486
+ (guid) => availableGuids !== undefined && !availableGuids.has(guid),
487
+ );
488
+ if (missing.length > 0)
489
+ return err(referenceError('pack-output-reference-missing', options.sourcePath, missing));
490
+ }
491
+
492
+ const externalEvidence: ScriptablePackExternalEvidence[] = [...observed.reads.values()]
493
+ .sort((left, right) => left.guid.localeCompare(right.guid))
494
+ .map(
495
+ (read): ScriptablePackExternalEvidence => ({
496
+ guid: read.guid,
497
+ usage: referenced.has(read.guid) ? 'both' : 'content',
498
+ generation: read.generation,
499
+ digest: read.digest,
500
+ }),
501
+ );
502
+ const inputFingerprint = await digest({
503
+ packageId: PackageId.format(subjectPackageId),
504
+ sourcePath: options.sourcePath,
505
+ sourceClosure: options.sourceClosure ?? [],
506
+ values: effectiveValues,
507
+ externalEvidence: externalEvidence.map(({ guid, usage, digest: evidenceDigest }) => ({
508
+ guid,
509
+ usage,
510
+ digest: evidenceDigest,
511
+ })),
512
+ authoringContractVersion: options.authoringContractVersion ?? 'parameterized-pack/1',
513
+ producerVersions: options.outputs.versions(),
514
+ });
515
+ const refs = imported.flatMap((asset) => asset.refs);
516
+ const artifacts = Object.fromEntries(
517
+ imported.flatMap((asset) =>
518
+ Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact]),
519
+ ),
520
+ );
521
+ const product = createImportProduct({
522
+ assets: imported,
523
+ sourceDependencies: (options.sourceClosure ?? []).map((entry) => entry.path),
524
+ refs,
525
+ artifacts,
526
+ receipts: imported.map((asset) => ({
527
+ guid: asset.guid,
528
+ origin: 'authoredPack' as const,
529
+ status: 'succeeded' as const,
530
+ inputFingerprint,
531
+ })),
532
+ diagnostics: [],
533
+ sourceRevision: inputFingerprint,
534
+ sourceKey: options.sourcePath,
535
+ });
536
+ if (!product.ok) return err(product.error);
537
+ return ok({
538
+ product: product.value,
539
+ stagedOutputs,
540
+ externalEvidence,
541
+ inputFingerprint,
542
+ ...(options.publication === undefined ? {} : { publication: options.publication }),
543
+ });
544
+ }
545
+
546
+ export interface ParameterizedScriptablePackWorkItem {
547
+ readonly definition: AnyParameterizedPackDefinition;
548
+ readonly sourcePath: string;
549
+ readonly subjectPackageId?: PackageId;
550
+ readonly values?: Readonly<Record<string, unknown>>;
551
+ readonly inheritedValues?: Readonly<Record<string, PackParameterValue>>;
552
+ readonly sourceClosure?: readonly ScriptablePackSourceClosureEntry[];
553
+ }
554
+
555
+ export interface ParameterizedScriptablePackWorklistOptions {
556
+ readonly subjects: readonly ParameterizedScriptablePackWorkItem[];
557
+ readonly outputs: AssetOutputProducerRegistry;
558
+ readonly assetSource?: ScriptablePackAssetSnapshotSource;
559
+ readonly availableGuids?: ReadonlySet<string>;
560
+ readonly incomingRefs?: ReadonlyMap<string, readonly string[]>;
561
+ readonly maxPasses?: number;
562
+ }
563
+
564
+ export interface ParameterizedScriptablePackWorklistProduct {
565
+ readonly products: readonly TerminalImportProduct<unknown>[];
566
+ readonly buildProducts: readonly ParameterizedScriptablePackBuildProduct[];
567
+ readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
568
+ readonly iterations: number;
569
+ }
570
+
571
+ function stagedSource(
572
+ staged: ReadonlyMap<string, ScriptablePackStagedOutput>,
573
+ fallback: ScriptablePackAssetSnapshotSource | undefined,
574
+ ): ScriptablePackAssetSnapshotSource {
575
+ return {
576
+ async readByGuid(guid) {
577
+ const key = AssetGuid.format(guid).toLowerCase();
578
+ const local = staged.get(key);
579
+ if (local !== undefined) {
580
+ return ok({
581
+ asset: clone(local.asset),
582
+ generation: 1,
583
+ digest: local.digest ?? 'sha256:staged',
584
+ });
585
+ }
586
+ if (fallback === undefined) {
587
+ return err({
588
+ code: 'asset-not-found',
589
+ expected: 'a staged or published content dependency',
590
+ hint: 'wait for the dependency subject to materialize',
591
+ detail: { guid: key },
592
+ });
593
+ }
594
+ return fallback.readByGuid(guid);
595
+ },
596
+ };
597
+ }
598
+
599
+ /** Build subjects in stable passes; content reads may wait for a later subject. */
600
+ export async function buildParameterizedScriptablePackWorklist(
601
+ options: ParameterizedScriptablePackWorklistOptions,
602
+ ): Promise<
603
+ Result<
604
+ ParameterizedScriptablePackWorklistProduct,
605
+ | PackAuthoringError
606
+ | ScriptablePackDomainError
607
+ | AssetError
608
+ | ImportError
609
+ | {
610
+ readonly code: string;
611
+ readonly expected: string;
612
+ readonly hint: string;
613
+ readonly detail?: unknown;
614
+ }
615
+ >
616
+ > {
617
+ const orderedSubjects = [...options.subjects].sort((left, right) =>
618
+ left.sourcePath.localeCompare(right.sourcePath),
619
+ );
620
+ const pending = new Map(
621
+ orderedSubjects.map((subject, index) => [`${index}:${subject.sourcePath}`, subject]),
622
+ );
623
+ const staged = new Map<string, ScriptablePackStagedOutput>();
624
+ const results = new Map<string, ParameterizedScriptablePackBuildProduct>();
625
+ const availableGuids = new Set([
626
+ ...(normalizedGuidSet(options.availableGuids) ?? []),
627
+ ...BUILTIN_MESH_ASSETS.map((asset) => asset.guid.toLowerCase()),
628
+ ]);
629
+ const maxPasses = options.maxPasses ?? Math.max(1, options.subjects.length + 1);
630
+ let iterations = 0;
631
+ for (; iterations < maxPasses && pending.size > 0; iterations += 1) {
632
+ let progress = false;
633
+ const waiting = new Set<string>();
634
+ for (const [key, subject] of pending) {
635
+ const result = await buildParameterizedScriptablePack({
636
+ definition: subject.definition,
637
+ sourcePath: subject.sourcePath,
638
+ ...(subject.subjectPackageId === undefined
639
+ ? {}
640
+ : { subjectPackageId: subject.subjectPackageId }),
641
+ ...(subject.values === undefined ? {} : { values: subject.values }),
642
+ ...(subject.inheritedValues === undefined
643
+ ? {}
644
+ : { inheritedValues: subject.inheritedValues }),
645
+ ...(subject.sourceClosure === undefined ? {} : { sourceClosure: subject.sourceClosure }),
646
+ outputs: options.outputs,
647
+ assetSource: stagedSource(staged, options.assetSource),
648
+ availableGuids: new Set([...availableGuids, ...staged.keys()]),
649
+ deferReferenceValidation: true,
650
+ });
651
+ if (result.ok) {
652
+ results.set(key, result.value);
653
+ for (const output of result.value.stagedOutputs)
654
+ staged.set(AssetGuid.format(output.guid).toLowerCase(), output);
655
+ pending.delete(key);
656
+ progress = true;
657
+ continue;
658
+ }
659
+ if (errorCode(result.error) === 'asset-not-found') {
660
+ const detail =
661
+ record(result.error) && record(result.error.detail) ? result.error.detail : undefined;
662
+ const guid =
663
+ detail !== undefined && typeof detail.guid === 'string' ? detail.guid : undefined;
664
+ if (guid !== undefined) waiting.add(guid.toLowerCase());
665
+ continue;
666
+ }
667
+ return result;
668
+ }
669
+ if (pending.size === 0) break;
670
+ if (!progress) {
671
+ return err({
672
+ code: 'pack-content-dependency-stalled',
673
+ expected: 'the content dependency worklist to make progress',
674
+ hint: 'inspect waitingGuids and repair the missing output or content-read cycle, then rebuild',
675
+ detail: {
676
+ waitingGuids: [...waiting].sort(),
677
+ pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
678
+ iterations: iterations + 1,
679
+ },
680
+ });
681
+ }
682
+ }
683
+ if (pending.size > 0) {
684
+ return err({
685
+ code: 'pack-content-dependency-stalled',
686
+ expected: 'the content dependency worklist to finish within its bounded retry budget',
687
+ hint: 'inspect pendingSubjects and waitingGuids, then repair the dependency graph',
688
+ detail: {
689
+ pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
690
+ waitingGuids: [],
691
+ iterations,
692
+ },
693
+ });
694
+ }
695
+ const knownGuids = new Set([...availableGuids, ...staged.keys()]);
696
+ const missingReferences = new Set<string>();
697
+ for (const product of results.values()) {
698
+ const contentReads = new Set(product.externalEvidence.map((read) => read.guid.toLowerCase()));
699
+ for (const asset of product.product.assets) {
700
+ for (const reference of asset.refs) {
701
+ const guid = reference.guid.toLowerCase();
702
+ if (!knownGuids.has(guid) && !contentReads.has(guid)) missingReferences.add(guid);
703
+ }
704
+ }
705
+ }
706
+ if (missingReferences.size > 0) {
707
+ return err(
708
+ referenceError('pack-output-reference-missing', 'worklist', [...missingReferences].sort()),
709
+ );
710
+ }
711
+ const removed = [...(options.incomingRefs?.keys() ?? [])].filter(
712
+ (guid) => !staged.has(guid.toLowerCase()),
713
+ );
714
+ const referencedRemoved = removed.filter(
715
+ (guid) => (options.incomingRefs?.get(guid) ?? []).length > 0,
716
+ );
717
+ if (referencedRemoved.length > 0)
718
+ return err(referenceError('pack-output-reference-conflict', 'worklist', referencedRemoved));
719
+ return ok({
720
+ products: [...results.values()].map((result) => result.product),
721
+ buildProducts: [...results.values()],
722
+ stagedOutputs: [...staged.values()],
723
+ iterations: pending.size === 0 && options.subjects.length > 0 ? iterations + 1 : iterations,
724
+ });
725
+ }