@forgeax/engine-import 0.1.3 → 0.1.4

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 (76) hide show
  1. package/README.md +6 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  4. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +2 -0
  6. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
  7. package/dist/browser.d.ts +5 -0
  8. package/dist/browser.d.ts.map +1 -0
  9. package/dist/browser.mjs +782 -0
  10. package/dist/browser.mjs.map +1 -0
  11. package/dist/build-production.d.ts +48 -0
  12. package/dist/build-production.d.ts.map +1 -0
  13. package/dist/catalog-importer-policy.d.ts +13 -0
  14. package/dist/catalog-importer-policy.d.ts.map +1 -0
  15. package/dist/catalog-inventory.d.ts +4 -0
  16. package/dist/catalog-inventory.d.ts.map +1 -0
  17. package/dist/import-product.d.ts +16 -25
  18. package/dist/import-product.d.ts.map +1 -1
  19. package/dist/import-runner.d.ts +6 -3
  20. package/dist/import-runner.d.ts.map +1 -1
  21. package/dist/importer-registry.d.ts +9 -1
  22. package/dist/importer-registry.d.ts.map +1 -1
  23. package/dist/index.d.ts +11 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +1315 -155
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/mesh-bin.d.ts +1 -1
  28. package/dist/mesh-bin.d.ts.map +1 -1
  29. package/dist/mesh-bin.mjs +182 -0
  30. package/dist/mesh-bin.mjs.map +1 -0
  31. package/dist/pack-projection.d.ts +5 -0
  32. package/dist/pack-projection.d.ts.map +1 -0
  33. package/dist/scriptable-pack-host.d.ts +64 -0
  34. package/dist/scriptable-pack-host.d.ts.map +1 -0
  35. package/dist/scriptable-pack-output-producers.d.ts +3 -2
  36. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  37. package/dist/scriptable-pack-staged-snapshot.d.ts +1 -2
  38. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
  39. package/dist/scriptable-pack.d.ts +6 -18
  40. package/dist/scriptable-pack.d.ts.map +1 -1
  41. package/dist/scriptable-source-package.d.ts +14 -0
  42. package/dist/scriptable-source-package.d.ts.map +1 -0
  43. package/dist/source-package-errors.d.ts +52 -0
  44. package/dist/source-package-errors.d.ts.map +1 -0
  45. package/dist/source-package-publication.d.ts +63 -0
  46. package/dist/source-package-publication.d.ts.map +1 -0
  47. package/dist/source-package.d.ts +60 -0
  48. package/dist/source-package.d.ts.map +1 -0
  49. package/package.json +13 -7
  50. package/src/__tests__/import-contract-migration.test.ts +33 -0
  51. package/src/__tests__/import-local-artifacts.test.ts +3 -1
  52. package/src/__tests__/owner-chain.integration.test.ts +37 -0
  53. package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
  54. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +78 -19
  55. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +52 -20
  56. package/src/browser.ts +14 -0
  57. package/src/build-production.ts +487 -0
  58. package/src/catalog-importer-policy.ts +58 -0
  59. package/src/catalog-inventory.ts +24 -0
  60. package/src/import-product.ts +37 -69
  61. package/src/import-runner.ts +36 -7
  62. package/src/importer-registry.ts +24 -1
  63. package/src/index.ts +79 -8
  64. package/src/mesh-bin.ts +2 -2
  65. package/src/pack-projection.ts +21 -0
  66. package/src/scriptable-pack-host.ts +461 -0
  67. package/src/scriptable-pack-output-producers.ts +48 -58
  68. package/src/scriptable-pack-staged-snapshot.ts +17 -6
  69. package/src/scriptable-pack.ts +37 -26
  70. package/src/scriptable-source-package.ts +88 -0
  71. package/src/source-package-errors.ts +201 -0
  72. package/src/source-package-publication.ts +295 -0
  73. package/src/source-package.ts +187 -0
  74. package/dist/__tests__/material-import-product.unit.test.d.ts +0 -2
  75. package/dist/__tests__/material-import-product.unit.test.d.ts.map +0 -1
  76. package/src/__tests__/material-import-product.unit.test.ts +0 -56
@@ -0,0 +1,461 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { stat } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { createAcceptedPublication } from '@forgeax/engine-ddc';
5
+ import {
6
+ type AuthoredPackInput,
7
+ finalizePackageTransportSource,
8
+ type PackageFinalizePolicy,
9
+ upgradeLegacyAuthoredPack,
10
+ } from '@forgeax/engine-pack/build';
11
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
12
+ import { type NativeCooker, NativeCookerRegistry } from '@forgeax/engine-pack/native-cooker';
13
+ import type { ScanSourceDeclaration } from '@forgeax/engine-pack/scanner';
14
+ import {
15
+ projectScriptablePackSceneComponents,
16
+ type ScriptablePackDefinition,
17
+ type ScriptablePackSourceClosureEntry,
18
+ } from '@forgeax/engine-pack/source';
19
+ import { inventoryScriptablePackSource } from '@forgeax/engine-pack/source-node';
20
+ import type {
21
+ Asset,
22
+ AssetGuid as AssetGuidType,
23
+ AssetPublicationEnvelope,
24
+ AssetPublicationOutput,
25
+ ResourceRevision,
26
+ Result,
27
+ } from '@forgeax/engine-types';
28
+ import { err, ok } from '@forgeax/engine-types';
29
+ import type { DdcPack } from './import-runner.js';
30
+ import { projectImportProductForBuild } from './pack-projection.js';
31
+ import type { ScriptablePackDomainError, ScriptablePackStagedOutput } from './scriptable-pack.js';
32
+ import { createStandardAssetOutputProducerRegistry } from './scriptable-pack-output-producers.js';
33
+ import {
34
+ createScriptablePackStagedAssetSnapshotSource,
35
+ type ScriptablePackStagedOwner,
36
+ } from './scriptable-pack-staged-snapshot.js';
37
+ import {
38
+ produceScriptableSourcePackage,
39
+ type ScriptableSourcePackageProduct,
40
+ type ScriptableSourcePackageResult,
41
+ } from './scriptable-source-package.js';
42
+
43
+ type FinalizedPackageTransport = Awaited<ReturnType<typeof finalizePackageTransportSource>>;
44
+
45
+ export interface ScriptablePackInput {
46
+ readonly sourcePath: string;
47
+ readonly displaySourcePath: string;
48
+ readonly definition: Readonly<ScriptablePackDefinition>;
49
+ readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
50
+ readonly publicationGeneration: number;
51
+ readonly policy: ScriptablePackTransportPolicy;
52
+ }
53
+
54
+ export interface ScriptablePackPublicationFacts {
55
+ readonly outputs: readonly AssetPublicationOutput[];
56
+ readonly refs: ReadonlyMap<string, readonly string[]>;
57
+ }
58
+
59
+ export interface PreparedScriptablePack {
60
+ readonly product: ScriptableSourcePackageProduct;
61
+ readonly finalized: FinalizedPackageTransport;
62
+ readonly facts: ScriptablePackPublicationFacts;
63
+ readonly revision: ResourceRevision;
64
+ readonly publication: AssetPublicationEnvelope;
65
+ }
66
+
67
+ export interface ScriptablePackTransportPaths {
68
+ readonly packagePath: string;
69
+ readonly artifactPath: (path: string) => string;
70
+ readonly receiptPath: (guid: string) => string;
71
+ }
72
+
73
+ export interface ScriptablePackTransportSink {
74
+ writePackage(path: string, body: string): void | Promise<void>;
75
+ writeArtifact(path: string, bytes: Uint8Array, mimeType: string): void | Promise<void>;
76
+ writeReceipt(path: string, body: string): void | Promise<void>;
77
+ }
78
+
79
+ export type ScriptablePackTransportPolicy =
80
+ | PackageFinalizePolicy
81
+ | ((product: ScriptableSourcePackageProduct) => PackageFinalizePolicy);
82
+
83
+ type ScriptablePackHostError =
84
+ ScriptableSourcePackageResult extends Result<unknown, infer E> ? E : never;
85
+
86
+ export interface CookedAuthoredPack {
87
+ readonly logicalPackage: DdcPack;
88
+ readonly refsByGuid: ReadonlyMap<string, readonly string[]>;
89
+ }
90
+
91
+ export interface AuthoredPackTransport {
92
+ readonly pack: AuthoredPackInput;
93
+ readonly firstGuid?: string;
94
+ readonly cooked?: CookedAuthoredPack;
95
+ readonly finalized?: FinalizedPackageTransport;
96
+ }
97
+
98
+ export function canonicalScriptableSourcePath(sourcePath: string): string {
99
+ const normalized = sourcePath.replaceAll('\\', '/');
100
+ const marker = '/assets/';
101
+ const markerIndex = normalized.indexOf(marker);
102
+ return markerIndex < 0 ? normalized : normalized.slice(markerIndex + 1);
103
+ }
104
+
105
+ export function scriptablePackInputs(
106
+ sourcePaths: readonly string[],
107
+ declarations: ReadonlyMap<string, ScanSourceDeclaration>,
108
+ cwd: string,
109
+ generationFor: (displaySourcePath: string) => number,
110
+ policyFor: (displaySourcePath: string) => ScriptablePackTransportPolicy,
111
+ ): ScriptablePackInput[] {
112
+ return sourcePaths.flatMap((sourcePath) => {
113
+ const declaration = declarations.get(resolve(cwd, sourcePath));
114
+ if (declaration?.format !== 'pack.ts') return [];
115
+ const displaySourcePath = canonicalScriptableSourcePath(sourcePath);
116
+ return [
117
+ {
118
+ sourcePath: resolve(cwd, sourcePath),
119
+ displaySourcePath,
120
+ definition: declaration.definition,
121
+ sourceClosure: declaration.sourceClosure,
122
+ publicationGeneration: generationFor(displaySourcePath),
123
+ policy: policyFor(displaySourcePath),
124
+ },
125
+ ];
126
+ });
127
+ }
128
+
129
+ /** Materialize one accepted ScriptablePack without duplicating dev/build loops. */
130
+ export async function materializePreparedScriptablePack(
131
+ prepared: PreparedScriptablePack,
132
+ paths: ScriptablePackTransportPaths,
133
+ sink: ScriptablePackTransportSink,
134
+ ): Promise<ReadonlyMap<string, string>> {
135
+ const { product, finalized } = prepared;
136
+ await sink.writePackage(paths.packagePath, JSON.stringify(finalized.pack));
137
+ for (const artifact of finalized.artifacts) {
138
+ await sink.writeArtifact(
139
+ paths.artifactPath(artifact.path),
140
+ artifact.bytes,
141
+ artifact.path.endsWith('.json') ? 'application/json' : artifact.mediaType,
142
+ );
143
+ }
144
+ const receipts = new Map<string, string>();
145
+ for (const guid of product.declaredGuids) {
146
+ const path = paths.receiptPath(guid);
147
+ await sink.writeReceipt(
148
+ path,
149
+ JSON.stringify({
150
+ guid,
151
+ origin: 'sourceMeta',
152
+ status: 'succeeded',
153
+ inputFingerprint: product.inputFingerprint,
154
+ outputDigest: finalized.digest,
155
+ }),
156
+ );
157
+ receipts.set(guid, path);
158
+ }
159
+ return receipts;
160
+ }
161
+
162
+ export async function readCookedAuthoredPack(
163
+ authoredPack: AuthoredPackInput,
164
+ cookers: readonly NativeCooker[] = [],
165
+ sourcePath?: string,
166
+ ): Promise<CookedAuthoredPack | undefined> {
167
+ const parsed = upgradeLegacyAuthoredPack(authoredPack);
168
+ if (parsed.schemaVersion !== '2.0.0' || parsed.assets === undefined) return undefined;
169
+ const registry = new NativeCookerRegistry();
170
+ for (const cooker of cookers) registry.register(cooker);
171
+ const assets: DdcPack['assets'][number][] = [];
172
+ const refsByGuid = new Map<string, readonly string[]>();
173
+ let hasCookedAsset = false;
174
+ for (const asset of parsed.assets) {
175
+ const shouldCook = asset.execution === 'cooked';
176
+ if (!shouldCook) {
177
+ assets.push({
178
+ guid: asset.guid,
179
+ kind: asset.kind,
180
+ ...(asset.name === undefined ? {} : { name: asset.name }),
181
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
182
+ ...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
183
+ ...(asset.relations === undefined ? {} : { relations: asset.relations }),
184
+ payload: asset.payload,
185
+ refs: asset.refs ?? [],
186
+ artifacts: {},
187
+ });
188
+ continue;
189
+ }
190
+ hasCookedAsset = true;
191
+ const result = await registry.runDraft(asset.kind, {
192
+ guid: asset.guid,
193
+ source: asset.payload,
194
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
195
+ ...(sourcePath === undefined ? {} : { sourcePath }),
196
+ refs: asset.refs ?? [],
197
+ });
198
+ if (!result.ok) throw result.error;
199
+ const draft = result.value;
200
+ const refs = [...draft.refs];
201
+ refsByGuid.set(asset.guid.toLowerCase(), refs);
202
+ assets.push({
203
+ guid: draft.guid,
204
+ kind: asset.kind,
205
+ ...(asset.name === undefined ? {} : { name: asset.name }),
206
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
207
+ ...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
208
+ ...(asset.relations === undefined ? {} : { relations: asset.relations }),
209
+ payload: draft.payload as Record<string, unknown>,
210
+ refs,
211
+ artifacts: draft.artifacts,
212
+ });
213
+ }
214
+ return hasCookedAsset
215
+ ? {
216
+ logicalPackage: { schemaVersion: '2.0.0', kind: 'internal-text-package', assets },
217
+ refsByGuid,
218
+ }
219
+ : undefined;
220
+ }
221
+
222
+ export async function declaredPackExternalOutputs(
223
+ declarations: ReadonlyMap<string, ScanSourceDeclaration>,
224
+ cookers: readonly NativeCooker[] = [],
225
+ requiredGuids: readonly AssetGuidType[] = [],
226
+ ): Promise<readonly ScriptablePackStagedOutput[]> {
227
+ if (requiredGuids.length === 0) return [];
228
+ const required = new Set(requiredGuids.map((guid) => AssetGuid.format(guid).toLowerCase()));
229
+ const outputs: ScriptablePackStagedOutput[] = [];
230
+ for (const declaration of declarations.values()) {
231
+ if (declaration.format !== 'pack.json') continue;
232
+ const declaredAssets = declaration.value.assets.filter((asset) =>
233
+ required.has(asset.guid.toLowerCase()),
234
+ );
235
+ if (declaredAssets.length === 0) continue;
236
+ const cooked = await readCookedAuthoredPack(declaration.value, cookers, declaration.sourcePath);
237
+ for (const asset of cooked?.logicalPackage.assets.filter((item) =>
238
+ required.has(item.guid.toLowerCase()),
239
+ ) ?? declaredAssets) {
240
+ const parsed = AssetGuid.parse(asset.guid);
241
+ if (!parsed.ok) throw parsed.error;
242
+ outputs.push({ guid: parsed.value, asset: { kind: asset.kind, ...asset.payload } as Asset });
243
+ }
244
+ }
245
+ return outputs;
246
+ }
247
+
248
+ /** Normalize one authored Pack and, when needed, run its registered cooker once. */
249
+ export async function prepareAuthoredPackTransport(
250
+ authoredPack: AuthoredPackInput,
251
+ cookers: readonly NativeCooker[] | undefined,
252
+ policyFor: (guid: string) => PackageFinalizePolicy,
253
+ sourcePath?: string,
254
+ ): Promise<AuthoredPackTransport> {
255
+ const pack = upgradeLegacyAuthoredPack(authoredPack);
256
+ const firstGuid = pack.assets?.[0]?.guid?.toLowerCase();
257
+ if (pack.schemaVersion !== '2.0.0' || firstGuid === undefined) {
258
+ return { pack, ...(firstGuid === undefined ? {} : { firstGuid }) };
259
+ }
260
+ const cooked = await readCookedAuthoredPack(pack, cookers, sourcePath);
261
+ if (cooked === undefined) return { pack, firstGuid };
262
+ return {
263
+ pack,
264
+ firstGuid,
265
+ cooked,
266
+ finalized: await finalizePackageTransportSource(cooked.logicalPackage, policyFor(firstGuid)),
267
+ };
268
+ }
269
+
270
+ /** Project one produced ScriptablePack into the publication tuple shared by dev and build. */
271
+ export function projectScriptablePackPublication(
272
+ product: ScriptableSourcePackageProduct,
273
+ ): Result<ScriptablePackPublicationFacts, ScriptablePackDomainError> {
274
+ const assets = new Map(product.product.assets.map((asset) => [asset.guid.toLowerCase(), asset]));
275
+ const outputs: AssetPublicationOutput[] = [];
276
+ for (const staged of product.stagedOutputs) {
277
+ const guid = AssetGuid.format(staged.guid).toLowerCase();
278
+ const asset = assets.get(guid);
279
+ if (asset === undefined || staged.digest === undefined) {
280
+ return err({
281
+ code: 'pack-source-output-invalid',
282
+ expected: `ScriptablePack publication output ${guid} to include a product and digest`,
283
+ hint: 'repair the ScriptablePack producer output and rebuild',
284
+ detail: { stage: 'publication', guid },
285
+ });
286
+ }
287
+ outputs.push({
288
+ guid,
289
+ sourceKey: staged.sourceKey ?? guid,
290
+ kind: asset.kind,
291
+ digest: staged.digest,
292
+ refs: asset.refs.map((reference) => reference.guid),
293
+ });
294
+ }
295
+ return ok({
296
+ outputs,
297
+ refs: new Map(
298
+ product.product.assets.map((asset) => [
299
+ asset.guid.toLowerCase(),
300
+ asset.refs.map((reference) => reference.guid),
301
+ ]),
302
+ ),
303
+ });
304
+ }
305
+
306
+ /** Project the immutable module closure into the Catalog revision contract. */
307
+ async function scriptablePackResourceRevision(
308
+ displaySourcePath: string,
309
+ digest: string,
310
+ sourceClosure: readonly ScriptablePackSourceClosureEntry[],
311
+ ): Promise<ResourceRevision> {
312
+ const observedAt = Math.trunc(
313
+ Math.max(
314
+ ...(await Promise.all(sourceClosure.map(async (entry) => (await stat(entry.path)).mtimeMs))),
315
+ ),
316
+ );
317
+ return { digest, observedAt, rootId: displaySourcePath };
318
+ }
319
+
320
+ /** Produce selected ScriptablePacks through one inventory and one staged owner boundary. */
321
+ export async function produceScriptablePackProducts(
322
+ sources: readonly ScriptablePackInput[],
323
+ declaredExternalOutputs: readonly ScriptablePackStagedOutput[] = [],
324
+ ): Promise<Result<ReadonlyMap<string, PreparedScriptablePack>, ScriptablePackHostError>> {
325
+ const entries = new Map<
326
+ string,
327
+ {
328
+ readonly source: ScriptablePackInput;
329
+ readonly definition: Readonly<ScriptablePackDefinition>;
330
+ readonly closure: readonly ScriptablePackSourceClosureEntry[];
331
+ }
332
+ >();
333
+ for (const source of sources) {
334
+ if (entries.has(source.displaySourcePath)) continue;
335
+ entries.set(source.displaySourcePath, {
336
+ source,
337
+ definition: source.definition,
338
+ closure: source.sourceClosure,
339
+ });
340
+ }
341
+
342
+ const products = new Map<string, ScriptableSourcePackageProduct>();
343
+ const owners: ScriptablePackStagedOwner[] = [...entries.entries()].map(
344
+ ([displaySourcePath, entry]) => ({
345
+ id: displaySourcePath,
346
+ guids: Object.values(entry.definition.assets).map((asset) => asset.guid),
347
+ async build(source) {
348
+ const produced = await produceScriptableSourcePackage({
349
+ definition: entry.definition,
350
+ sourcePath: displaySourcePath,
351
+ assetSource: source,
352
+ outputs: createStandardAssetOutputProducerRegistry(
353
+ projectScriptablePackSceneComponents(entry.definition.sceneComponents),
354
+ ),
355
+ sourceClosure: entry.closure,
356
+ authoringContractVersion: 'scriptable-pack-production/1',
357
+ });
358
+ if (!produced.ok) return produced;
359
+ const observedClosure = await inventoryScriptablePackSource(entry.source.sourcePath);
360
+ if (JSON.stringify(observedClosure) !== JSON.stringify(entry.closure)) {
361
+ return err({
362
+ code: 'pack-source-load-failed',
363
+ expected: 'the complete ScriptablePack module closure to remain fixed during one build',
364
+ hint: 'retry the build after source and helper writes have settled',
365
+ detail: {
366
+ sourcePath: displaySourcePath,
367
+ reason: 'source-changed',
368
+ phase: 'build',
369
+ diagnostic: 'module closure changed while the staged generation was building',
370
+ },
371
+ });
372
+ }
373
+ products.set(displaySourcePath, produced.value);
374
+ return ok(produced.value.stagedOutputs);
375
+ },
376
+ }),
377
+ );
378
+ const generationDigest = createHash('sha256')
379
+ .update(
380
+ JSON.stringify(
381
+ [...entries.values()]
382
+ .flatMap((entry) => entry.closure)
383
+ .sort((a, b) => a.path.localeCompare(b.path)),
384
+ ),
385
+ )
386
+ .digest();
387
+ const stagedSource = createScriptablePackStagedAssetSnapshotSource({
388
+ generation: generationDigest.readUInt32BE(0),
389
+ owners,
390
+ declaredExternalOutputs,
391
+ });
392
+
393
+ const prepared = new Map<string, PreparedScriptablePack>();
394
+ for (const source of sources) {
395
+ const entry = entries.get(source.displaySourcePath);
396
+ if (entry === undefined) {
397
+ return err({
398
+ code: 'pack-source-path-invalid',
399
+ expected: 'a ScriptablePack source registered in the current inventory',
400
+ actual: source.displaySourcePath,
401
+ hint: 'rebuild the source inventory before producing this path',
402
+ retryable: true,
403
+ recoveryActions: ['rebuild-source-inventory'],
404
+ detail: { sourcePath: source.displaySourcePath },
405
+ });
406
+ }
407
+ const first = Object.values(entry.definition.assets)[0];
408
+ if (first === undefined) {
409
+ return err({
410
+ code: 'pack-source-output-invalid',
411
+ expected: 'at least one declared ScriptablePack output',
412
+ hint: 'add an output descriptor before building the package',
413
+ detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: [] },
414
+ });
415
+ }
416
+ const staged = await stagedSource.readByGuid(first.guid);
417
+ if (!staged.ok) return err(staged.error);
418
+ const produced = products.get(source.displaySourcePath);
419
+ if (produced === undefined) {
420
+ return err({
421
+ code: 'pack-source-output-invalid',
422
+ expected: 'the staged owner build to retain its source-package product',
423
+ hint: 'retry the ScriptablePack production attempt',
424
+ detail: {
425
+ missingGuids: [AssetGuid.format(first.guid)],
426
+ unexpectedSourceKeys: [],
427
+ kindMismatches: [],
428
+ },
429
+ });
430
+ }
431
+ const transportPolicy =
432
+ typeof source.policy === 'function' ? source.policy(produced) : source.policy;
433
+ const logicalPackage = projectImportProductForBuild(produced.product);
434
+ const finalized = await finalizePackageTransportSource(logicalPackage, transportPolicy);
435
+ const facts = projectScriptablePackPublication(produced);
436
+ if (!facts.ok) return err(facts.error);
437
+ const revision = await scriptablePackResourceRevision(
438
+ source.displaySourcePath,
439
+ produced.inputFingerprint,
440
+ source.sourceClosure,
441
+ );
442
+ const publication = createAcceptedPublication({
443
+ sourcePath: source.displaySourcePath,
444
+ sourceRevision: produced.inputFingerprint,
445
+ generation: source.publicationGeneration,
446
+ digest: finalized.digest,
447
+ packageUrl: finalized.packageUrl,
448
+ inputFingerprint: produced.inputFingerprint,
449
+ outputs: facts.value.outputs,
450
+ externalEvidence: produced.externalEvidence,
451
+ });
452
+ prepared.set(source.displaySourcePath, {
453
+ product: produced,
454
+ finalized,
455
+ facts: facts.value,
456
+ revision,
457
+ publication,
458
+ });
459
+ }
460
+ return ok(prepared);
461
+ }
@@ -1,9 +1,8 @@
1
- import { componentSchema } from '@forgeax/engine-ecs/internal';
2
1
  import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import type { ScriptablePackSceneComponent } from '@forgeax/engine-pack/source';
3
3
  import { externalizeSceneAsset } from '@forgeax/engine-scene';
4
4
  import type {
5
5
  AnimationGraph,
6
- Asset,
7
6
  AssetGuid as AssetGuidType,
8
7
  AssetRef,
9
8
  AudioClipAsset,
@@ -16,7 +15,6 @@ import type {
16
15
  MeshAsset,
17
16
  ParticleEffectAsset,
18
17
  RenderPipelineAsset,
19
- Result,
20
18
  SamplerAsset,
21
19
  SceneAsset,
22
20
  SkeletonAsset,
@@ -154,18 +152,6 @@ function materialProduct(input: AssetOutputInput): AssetOutputProduct {
154
152
  };
155
153
  }
156
154
 
157
- export const materialAssetOutputProducer: AssetOutputProducer = {
158
- kind: 'material',
159
- version: 'material-pack/1',
160
- produce(input): Result<AssetOutputProduct, ImportError> {
161
- try {
162
- return ok(materialProduct(input));
163
- } catch (error) {
164
- return err(producerError(input, error));
165
- }
166
- },
167
- };
168
-
169
155
  function meshProduct(input: AssetOutputInput): AssetOutputProduct {
170
156
  if (input.asset.kind !== 'mesh') throw new TypeError('expected MeshAsset');
171
157
  const mesh = input.asset as MeshAsset;
@@ -201,23 +187,19 @@ function meshProduct(input: AssetOutputInput): AssetOutputProduct {
201
187
  };
202
188
  }
203
189
 
204
- export const meshAssetOutputProducer: AssetOutputProducer = {
205
- kind: 'mesh',
206
- version: 'mesh-binary/4',
207
- produce(input): Result<AssetOutputProduct, ImportError> {
208
- try {
209
- return ok(meshProduct(input));
210
- } catch (error) {
211
- return err(producerError(input, error));
212
- }
213
- },
214
- };
215
-
216
- function sceneProduct(input: AssetOutputInput): AssetOutputProduct {
190
+ function sceneProduct(
191
+ input: AssetOutputInput,
192
+ components: ReadonlyMap<string, Readonly<Record<string, string>>>,
193
+ ): AssetOutputProduct {
217
194
  if (input.asset.kind !== 'scene') throw new TypeError('expected SceneAsset');
218
195
  const externalized = externalizeSceneAsset(input.asset as SceneAsset, (componentName) => {
219
- const component = input.components?.get(componentName);
220
- return component === undefined ? undefined : componentSchema(component);
196
+ const schema = components.get(componentName);
197
+ if (schema === undefined) {
198
+ throw new TypeError(
199
+ `ScriptablePack scene component ${componentName} is missing from sceneComponents`,
200
+ );
201
+ }
202
+ return schema;
221
203
  });
222
204
  if (!externalized.ok) throw new TypeError(`scene field ${externalized.error.field} is invalid`);
223
205
  return {
@@ -227,6 +209,24 @@ function sceneProduct(input: AssetOutputInput): AssetOutputProduct {
227
209
  };
228
210
  }
229
211
 
212
+ function createSafeProducer(
213
+ kind: AssetOutputProducer['kind'],
214
+ version: string,
215
+ product: (input: AssetOutputInput) => AssetOutputProduct,
216
+ ): AssetOutputProducer {
217
+ return {
218
+ kind,
219
+ version,
220
+ produce(input) {
221
+ try {
222
+ return ok(product(input));
223
+ } catch (error) {
224
+ return err(producerError(input, error));
225
+ }
226
+ },
227
+ };
228
+ }
229
+
230
230
  function jsonArtifact(value: unknown): ImportedArtifactBody {
231
231
  return {
232
232
  mediaType: 'application/json',
@@ -412,39 +412,29 @@ function ordinaryPodProduct(input: AssetOutputInput): AssetOutputProduct {
412
412
  }
413
413
  }
414
414
 
415
- const ordinaryAssetOutputProducer: AssetOutputProducer = {
416
- kind: 'texture',
417
- version: 'ordinary-pod/1',
418
- produce(input) {
419
- try {
420
- return ok(ordinaryPodProduct(input));
421
- } catch (error) {
422
- return err(producerError(input, error));
423
- }
424
- },
425
- };
415
+ export const materialAssetOutputProducer = createSafeProducer(
416
+ 'material',
417
+ 'material-pack/1',
418
+ materialProduct,
419
+ );
420
+ export const meshAssetOutputProducer = createSafeProducer('mesh', 'mesh-binary/4', meshProduct);
426
421
 
427
- function createOrdinaryAssetOutputProducer(kind: Asset['kind']): AssetOutputProducer {
428
- return { ...ordinaryAssetOutputProducer, kind };
422
+ export function createSceneAssetOutputProducer(
423
+ sceneComponents: readonly ScriptablePackSceneComponent[] = [],
424
+ ): AssetOutputProducer {
425
+ const schemas = new Map(
426
+ sceneComponents.map((component) => [component.name, component.fields] as const),
427
+ );
428
+ return createSafeProducer('scene', 'scene-pack/3', (input) => sceneProduct(input, schemas));
429
429
  }
430
430
 
431
- export const sceneAssetOutputProducer: AssetOutputProducer = {
432
- kind: 'scene',
433
- version: 'scene-pack/2',
434
- produce(input): Result<AssetOutputProduct, ImportError> {
435
- try {
436
- return ok(sceneProduct(input));
437
- } catch (error) {
438
- return err(producerError(input, error));
439
- }
440
- },
441
- };
442
-
443
- export function createStandardAssetOutputProducerRegistry(): AssetOutputProducerRegistry {
431
+ export function createStandardAssetOutputProducerRegistry(
432
+ sceneComponents: readonly ScriptablePackSceneComponent[] = [],
433
+ ): AssetOutputProducerRegistry {
444
434
  const registry = new AssetOutputProducerRegistry();
445
435
  registry.register(materialAssetOutputProducer);
446
436
  registry.register(meshAssetOutputProducer);
447
- registry.register(sceneAssetOutputProducer);
437
+ registry.register(createSceneAssetOutputProducer(sceneComponents));
448
438
  for (const kind of [
449
439
  'texture',
450
440
  'equirect',
@@ -460,7 +450,7 @@ export function createStandardAssetOutputProducerRegistry(): AssetOutputProducer
460
450
  'audio',
461
451
  'particle-effect',
462
452
  ] as const) {
463
- registry.register(createOrdinaryAssetOutputProducer(kind));
453
+ registry.register(createSafeProducer(kind, 'ordinary-pod/1', ordinaryPodProduct));
464
454
  }
465
455
  return registry;
466
456
  }
@@ -26,8 +26,7 @@ export interface ScriptablePackStagedOwner {
26
26
  export interface ScriptablePackStagedSnapshotOptions {
27
27
  readonly generation: number;
28
28
  readonly owners: readonly ScriptablePackStagedOwner[];
29
- /** A prebuilt authority only. Local source ownership always wins. */
30
- readonly fallback?: ScriptablePackAssetSnapshotSource;
29
+ readonly declaredExternalOutputs?: readonly ScriptablePackStagedOutput[];
31
30
  }
32
31
 
33
32
  function stable(value: unknown): string {
@@ -81,8 +80,21 @@ function missingOutputError(owner: string, guid: string): ScriptablePackError {
81
80
  export function createScriptablePackStagedAssetSnapshotSource(
82
81
  options: ScriptablePackStagedSnapshotOptions,
83
82
  ): ScriptablePackAssetSnapshotSource {
83
+ const owners =
84
+ options.declaredExternalOutputs === undefined || options.declaredExternalOutputs.length === 0
85
+ ? options.owners
86
+ : [
87
+ {
88
+ id: '<declared-pack-external>',
89
+ guids: options.declaredExternalOutputs.map((output) => output.guid),
90
+ async build() {
91
+ return ok(options.declaredExternalOutputs ?? []);
92
+ },
93
+ } satisfies ScriptablePackStagedOwner,
94
+ ...options.owners,
95
+ ];
84
96
  const ownerByGuid = new Map<string, ScriptablePackStagedOwner>();
85
- for (const owner of options.owners) {
97
+ for (const owner of owners) {
86
98
  if (owner.id.trim().length === 0) throw new TypeError('staged owner id must be non-empty');
87
99
  for (const guid of owner.guids) {
88
100
  const key = AssetGuid.format(guid).toLowerCase();
@@ -104,12 +116,11 @@ export function createScriptablePackStagedAssetSnapshotSource(
104
116
  if (cached !== undefined) return ok(structuredClone(cached));
105
117
  const owner = ownerByGuid.get(key);
106
118
  if (owner === undefined) {
107
- if (options.fallback !== undefined) return options.fallback.readByGuid(guid);
108
119
  return err(
109
120
  new AssetError({
110
121
  code: 'asset-not-imported',
111
- expected: 'a staged local owner or explicit prebuilt fallback for the requested GUID',
112
- hint: 'declare the local ScriptablePack output or configure a prebuilt asset authority',
122
+ expected: 'a staged local owner for the requested GUID',
123
+ hint: 'declare the local ScriptablePack output before rebuilding the generation',
113
124
  }),
114
125
  );
115
126
  }