@forgeax/engine-import 0.1.3 → 0.1.6

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 (79) 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-host.unit.test.d.ts +2 -0
  6. package/dist/__tests__/scriptable-pack-host.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +2 -0
  8. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
  9. package/dist/browser.d.ts +5 -0
  10. package/dist/browser.d.ts.map +1 -0
  11. package/dist/browser.mjs +782 -0
  12. package/dist/browser.mjs.map +1 -0
  13. package/dist/build-production.d.ts +48 -0
  14. package/dist/build-production.d.ts.map +1 -0
  15. package/dist/catalog-importer-policy.d.ts +13 -0
  16. package/dist/catalog-importer-policy.d.ts.map +1 -0
  17. package/dist/catalog-inventory.d.ts +4 -0
  18. package/dist/catalog-inventory.d.ts.map +1 -0
  19. package/dist/import-product.d.ts +16 -25
  20. package/dist/import-product.d.ts.map +1 -1
  21. package/dist/import-runner.d.ts +6 -3
  22. package/dist/import-runner.d.ts.map +1 -1
  23. package/dist/importer-registry.d.ts +9 -1
  24. package/dist/importer-registry.d.ts.map +1 -1
  25. package/dist/index.d.ts +11 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.mjs +1380 -155
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mesh-bin.d.ts +1 -1
  30. package/dist/mesh-bin.d.ts.map +1 -1
  31. package/dist/mesh-bin.mjs +182 -0
  32. package/dist/mesh-bin.mjs.map +1 -0
  33. package/dist/pack-projection.d.ts +5 -0
  34. package/dist/pack-projection.d.ts.map +1 -0
  35. package/dist/scriptable-pack-host.d.ts +72 -0
  36. package/dist/scriptable-pack-host.d.ts.map +1 -0
  37. package/dist/scriptable-pack-output-producers.d.ts +3 -2
  38. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  39. package/dist/scriptable-pack-staged-snapshot.d.ts +1 -2
  40. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
  41. package/dist/scriptable-pack.d.ts +6 -18
  42. package/dist/scriptable-pack.d.ts.map +1 -1
  43. package/dist/scriptable-source-package.d.ts +14 -0
  44. package/dist/scriptable-source-package.d.ts.map +1 -0
  45. package/dist/source-package-errors.d.ts +52 -0
  46. package/dist/source-package-errors.d.ts.map +1 -0
  47. package/dist/source-package-publication.d.ts +63 -0
  48. package/dist/source-package-publication.d.ts.map +1 -0
  49. package/dist/source-package.d.ts +60 -0
  50. package/dist/source-package.d.ts.map +1 -0
  51. package/package.json +13 -7
  52. package/src/__tests__/import-contract-migration.test.ts +33 -0
  53. package/src/__tests__/import-local-artifacts.test.ts +3 -1
  54. package/src/__tests__/owner-chain.integration.test.ts +37 -0
  55. package/src/__tests__/scriptable-pack-host.unit.test.ts +86 -0
  56. package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
  57. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +78 -19
  58. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +52 -20
  59. package/src/browser.ts +14 -0
  60. package/src/build-production.ts +493 -0
  61. package/src/catalog-importer-policy.ts +58 -0
  62. package/src/catalog-inventory.ts +24 -0
  63. package/src/import-product.ts +37 -69
  64. package/src/import-runner.ts +36 -7
  65. package/src/importer-registry.ts +24 -1
  66. package/src/index.ts +80 -8
  67. package/src/mesh-bin.ts +2 -2
  68. package/src/pack-projection.ts +21 -0
  69. package/src/scriptable-pack-host.ts +544 -0
  70. package/src/scriptable-pack-output-producers.ts +48 -58
  71. package/src/scriptable-pack-staged-snapshot.ts +17 -6
  72. package/src/scriptable-pack.ts +37 -26
  73. package/src/scriptable-source-package.ts +88 -0
  74. package/src/source-package-errors.ts +201 -0
  75. package/src/source-package-publication.ts +295 -0
  76. package/src/source-package.ts +187 -0
  77. package/dist/__tests__/material-import-product.unit.test.d.ts +0 -2
  78. package/dist/__tests__/material-import-product.unit.test.d.ts.map +0 -1
  79. package/src/__tests__/material-import-product.unit.test.ts +0 -56
@@ -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
  }
@@ -1,29 +1,35 @@
1
- import type { Component } from '@forgeax/engine-ecs';
2
1
  import { isScriptablePackAssetKind, type ScriptablePackAssetKind } from '@forgeax/engine-pack';
3
2
  import { AssetGuid } from '@forgeax/engine-pack/guid';
4
3
  import {
5
4
  type AssetReader,
6
5
  projectScriptablePackMeta,
6
+ projectScriptablePackSceneComponents,
7
7
  type ScriptablePackDefinition,
8
8
  type ScriptablePackError,
9
9
  type ScriptablePackReadError,
10
+ type ScriptablePackSourceClosureEntry,
10
11
  } from '@forgeax/engine-pack/source';
11
12
  import type {
12
13
  AnimationGraph,
13
14
  Asset,
14
15
  AssetGuid as AssetGuidType,
15
16
  AssetPublicationEnvelope,
16
- AssetRef,
17
17
  FontAsset,
18
- ImportedArtifactBody,
19
18
  ImportedAsset,
20
- ImportProduct,
21
19
  MaterialAsset,
22
20
  MeshAsset,
23
21
  Result,
24
22
  TilesetAsset,
25
23
  } from '@forgeax/engine-types';
24
+
25
+ export type { ScriptablePackSourceClosureEntry } from '@forgeax/engine-pack/source';
26
+
26
27
  import { AssetError, err, ImportError, ok } from '@forgeax/engine-types';
28
+ import {
29
+ createImportProduct,
30
+ type ImportAssetProduct,
31
+ type TerminalImportProduct,
32
+ } from './import-product.js';
27
33
 
28
34
  function commonPathPrefix(paths: readonly string[]): string {
29
35
  const first = paths[0];
@@ -83,8 +89,6 @@ export interface AssetOutputInput {
83
89
  readonly guid: string;
84
90
  readonly sourceKey: string;
85
91
  readonly asset: Asset;
86
- /** World-local schema projection for scene ref externalization. */
87
- readonly components?: ReadonlyMap<string, Component>;
88
92
  }
89
93
 
90
94
  type MaterialPackPayload = Omit<MaterialAsset, 'parent' | 'values'> & {
@@ -132,11 +136,7 @@ export type AssetOutputPayloadByKind = {
132
136
 
133
137
  export type AssetOutputPayload = AssetOutputPayloadByKind[ScriptablePackAssetKind];
134
138
 
135
- export interface AssetOutputProduct {
136
- readonly payload: AssetOutputPayload;
137
- readonly refs: readonly AssetRef[];
138
- readonly artifacts: Readonly<Record<string, ImportedArtifactBody>>;
139
- }
139
+ export type AssetOutputProduct = ImportAssetProduct<AssetOutputPayload>;
140
140
 
141
141
  export interface AssetOutputProducer {
142
142
  readonly kind: string;
@@ -181,13 +181,8 @@ export interface ScriptablePackExternalEvidence {
181
181
  readonly digest?: string;
182
182
  }
183
183
 
184
- export interface ScriptablePackSourceClosureEntry {
185
- readonly path: string;
186
- readonly digest: string;
187
- }
188
-
189
184
  export interface ScriptablePackBuildProduct {
190
- readonly product: ImportProduct<unknown>;
185
+ readonly product: TerminalImportProduct<unknown>;
191
186
  /** Raw private outputs for the current staged build generation, never a runtime publication path. */
192
187
  readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
193
188
  readonly externalEvidence: readonly ScriptablePackExternalEvidence[];
@@ -269,7 +264,7 @@ function observedAssetReader(source?: ScriptablePackAssetSnapshotSource): {
269
264
  new AssetError({
270
265
  code: 'asset-not-found',
271
266
  expected: `a host Asset snapshot source for ScriptablePack content read ${key}`,
272
- hint: 'configure pluginPack({ scriptablePack: { assetSource, ... } }) or remove the content read',
267
+ hint: 'configure the standard ScriptablePack asset source or remove the content read',
273
268
  }),
274
269
  );
275
270
  }
@@ -435,8 +430,6 @@ export interface BuildScriptablePackOptions {
435
430
  readonly outputs: AssetOutputProducerRegistry;
436
431
  readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
437
432
  readonly authoringContractVersion: string;
438
- /** Optional owner-provided component schema projection for scene output. */
439
- readonly components?: ReadonlyMap<string, Component>;
440
433
  }
441
434
 
442
435
  /**
@@ -527,7 +520,6 @@ export async function buildScriptablePack(
527
520
  guid: AssetGuid.format(descriptor.guid),
528
521
  sourceKey,
529
522
  asset,
530
- ...(options.components === undefined ? {} : { components: options.components }),
531
523
  });
532
524
  if (!product.ok) return err(product.error);
533
525
  const productError = productContractError(sourceKey, descriptor.kind, product.value);
@@ -579,19 +571,38 @@ export async function buildScriptablePack(
579
571
  usage,
580
572
  ...(digest === undefined ? {} : { digest }),
581
573
  }));
582
- const product: ImportProduct<unknown> = {
583
- assets,
584
- sourceDependencies: options.sourceClosure.map((entry) => entry.path),
585
- };
586
574
  const inputFingerprint = await fingerprint({
587
575
  meta: projectScriptablePackMeta(options.definition, options.sourcePath),
576
+ sceneComponents: projectScriptablePackSceneComponents(options.definition.sceneComponents),
588
577
  sourceClosure: stableSourceClosure(options.sourceClosure),
589
578
  externalEvidence: fingerprintEvidence,
590
579
  authoringContractVersion: options.authoringContractVersion,
591
580
  producerVersions: options.outputs.versions(),
592
581
  });
582
+ const refs = assets.flatMap((asset) => asset.refs);
583
+ const artifacts = Object.fromEntries(
584
+ assets.flatMap((asset) =>
585
+ Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact]),
586
+ ),
587
+ );
588
+ const product = createImportProduct({
589
+ assets,
590
+ sourceDependencies: options.sourceClosure.map((entry) => entry.path),
591
+ refs,
592
+ artifacts,
593
+ receipts: assets.map((asset) => ({
594
+ guid: asset.guid,
595
+ origin: 'sourceMeta' as const,
596
+ status: 'succeeded' as const,
597
+ inputFingerprint,
598
+ })),
599
+ diagnostics: [],
600
+ sourceRevision: inputFingerprint,
601
+ sourceKey: options.sourcePath,
602
+ });
603
+ if (!product.ok) return err(product.error);
593
604
  return ok({
594
- product,
605
+ product: product.value,
595
606
  stagedOutputs: await Promise.all(
596
607
  Object.keys(options.definition.assets)
597
608
  .sort()
@@ -0,0 +1,88 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import { projectScriptablePackMeta, type ScriptablePackError } from '@forgeax/engine-pack/source';
3
+ import type {
4
+ AssetError,
5
+ ImportError,
6
+ ImportedAsset,
7
+ ImportProduct,
8
+ Result,
9
+ } from '@forgeax/engine-types';
10
+ import { err, ok } from '@forgeax/engine-types';
11
+ import {
12
+ type BuildScriptablePackOptions,
13
+ buildScriptablePack,
14
+ type ScriptablePackDomainError,
15
+ type ScriptablePackExternalEvidence,
16
+ type ScriptablePackStagedOutput,
17
+ } from './scriptable-pack.js';
18
+
19
+ const META_ARTIFACT_KEY = 'scriptable-pack.meta.json';
20
+
21
+ export interface ScriptableSourcePackageProduct {
22
+ readonly anchorGuid: string;
23
+ readonly declaredGuids: readonly string[];
24
+ readonly product: ImportProduct<unknown>;
25
+ readonly inputFingerprint: string;
26
+ readonly externalEvidence: readonly ScriptablePackExternalEvidence[];
27
+ readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
28
+ }
29
+
30
+ export type ScriptableSourcePackageResult = Result<
31
+ ScriptableSourcePackageProduct,
32
+ ScriptablePackError | AssetError | ImportError | ScriptablePackDomainError
33
+ >;
34
+
35
+ function withPrebuiltMeta<P>(
36
+ product: ImportProduct<P>,
37
+ anchorGuid: string,
38
+ meta: unknown,
39
+ ): ImportProduct<P> {
40
+ const bytes = new TextEncoder().encode(`${JSON.stringify(meta)}\n`);
41
+ return {
42
+ ...product,
43
+ assets: product.assets.map(
44
+ (asset): ImportedAsset<P> =>
45
+ asset.guid.toLowerCase() === anchorGuid
46
+ ? {
47
+ ...asset,
48
+ artifacts: {
49
+ ...asset.artifacts,
50
+ [META_ARTIFACT_KEY]: { mediaType: 'application/json', bytes },
51
+ },
52
+ }
53
+ : asset,
54
+ ),
55
+ };
56
+ }
57
+
58
+ export async function produceScriptableSourcePackage(
59
+ options: BuildScriptablePackOptions,
60
+ ): Promise<ScriptableSourcePackageResult> {
61
+ const built = await buildScriptablePack(options);
62
+ if (!built.ok) return err(built.error);
63
+ const declaredGuids = Object.values(options.definition.assets)
64
+ .map((descriptor) => AssetGuid.format(descriptor.guid).toLowerCase())
65
+ .sort();
66
+ const anchorGuid = declaredGuids[0];
67
+ if (anchorGuid === undefined) {
68
+ return err({
69
+ code: 'pack-source-output-invalid',
70
+ expected: 'at least one declared ScriptablePack output',
71
+ hint: 'add an output descriptor before building the package',
72
+ detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: [] },
73
+ });
74
+ }
75
+ const product = withPrebuiltMeta(
76
+ built.value.product,
77
+ anchorGuid,
78
+ projectScriptablePackMeta(options.definition, options.sourcePath),
79
+ );
80
+ return ok({
81
+ anchorGuid,
82
+ declaredGuids,
83
+ product,
84
+ stagedOutputs: built.value.stagedOutputs,
85
+ inputFingerprint: built.value.inputFingerprint,
86
+ externalEvidence: built.value.externalEvidence,
87
+ });
88
+ }
@@ -0,0 +1,201 @@
1
+ import type { ImportError } from '@forgeax/engine-types';
2
+
3
+ const SOURCE_PACKAGE_ERROR_POLICY = {
4
+ 'source-package-meta-invalid': {
5
+ expected: 'a valid source Meta declaration with complete GUID topology',
6
+ hint: 'repair the Meta declaration, then rebuild or cold-cook the source package',
7
+ },
8
+ 'source-package-importer-missing': {
9
+ expected: 'a registered importer for the source Meta importer key',
10
+ hint: 'register the named importer, then rebuild or cold-cook the source package',
11
+ },
12
+ 'source-package-conversion-failed': {
13
+ expected: 'the configured importer to convert the source successfully',
14
+ hint: 'repair the source or importer, then rebuild or cold-cook the source package',
15
+ },
16
+ 'source-package-ddc-failed': {
17
+ expected: 'a readable persistent DDC entry with matching integrity evidence',
18
+ hint: 'discard the invalid derived entry, then rebuild or cold-cook the source package',
19
+ },
20
+ 'source-package-publication-invalid': {
21
+ expected: 'a complete Pack body, refs, artifacts, and route integrity',
22
+ hint: 'repair the missing product bytes, then rebuild or cold-cook the source package',
23
+ },
24
+ 'source-package-guid-closure-mismatch': {
25
+ expected: 'exactly one produced asset for every declared GUID',
26
+ hint: 'repair the Meta topology or importer output, then rebuild the whole source package',
27
+ },
28
+ } as const;
29
+
30
+ export type SourcePackageErrorCode = keyof typeof SOURCE_PACKAGE_ERROR_POLICY;
31
+
32
+ export type SourcePackageErrorStage =
33
+ | 'meta'
34
+ | 'importer'
35
+ | 'conversion'
36
+ | 'closure'
37
+ | 'ddc'
38
+ | 'route-integrity';
39
+
40
+ export interface SourcePackageErrorContext {
41
+ readonly sourceMeta: string;
42
+ readonly anchorGuid: string;
43
+ readonly affectedGuids: readonly string[];
44
+ readonly producer: string;
45
+ readonly importer: string;
46
+ }
47
+
48
+ export interface SourcePackageErrorDetail extends SourcePackageErrorContext {
49
+ readonly stage: SourcePackageErrorStage;
50
+ readonly reason?: string;
51
+ readonly missing?: readonly string[];
52
+ readonly unexpected?: readonly string[];
53
+ readonly registeredImporters?: readonly string[];
54
+ }
55
+
56
+ export interface SourcePackageError {
57
+ readonly code: SourcePackageErrorCode;
58
+ readonly expected: string;
59
+ readonly hint: string;
60
+ readonly detail: SourcePackageErrorDetail;
61
+ }
62
+
63
+ export function sourcePackageError(
64
+ code: SourcePackageErrorCode,
65
+ context: SourcePackageErrorContext,
66
+ detail: Omit<SourcePackageErrorDetail, keyof SourcePackageErrorContext>,
67
+ ): SourcePackageError {
68
+ const policy = SOURCE_PACKAGE_ERROR_POLICY[code];
69
+ return {
70
+ code,
71
+ expected: policy?.expected as string,
72
+ hint: policy?.hint as string,
73
+ detail: { ...context, ...detail },
74
+ };
75
+ }
76
+
77
+ function unknownReason(error: unknown): string {
78
+ if (error !== null && typeof error === 'object') {
79
+ const value = error as Record<string, unknown>;
80
+ if (typeof value.code === 'string') {
81
+ const detail = value.detail;
82
+ const reason =
83
+ detail !== null && typeof detail === 'object' && 'reason' in detail
84
+ ? (detail as { readonly reason?: unknown }).reason
85
+ : undefined;
86
+ const diagnostic =
87
+ detail !== null && typeof detail === 'object' && 'diagnostic' in detail
88
+ ? (detail as { readonly diagnostic?: unknown }).diagnostic
89
+ : undefined;
90
+ return `${value.code}: ${
91
+ typeof diagnostic === 'string'
92
+ ? diagnostic
93
+ : typeof reason === 'string'
94
+ ? reason
95
+ : String(value.expected ?? 'producer failure')
96
+ }`;
97
+ }
98
+ }
99
+ return error instanceof Error ? error.message : String(error);
100
+ }
101
+
102
+ function importFailureCode(error: ImportError): SourcePackageErrorCode {
103
+ switch (error.code) {
104
+ case 'importer-not-registered':
105
+ return 'source-package-importer-missing';
106
+ case 'import-produced-no-assets':
107
+ case 'guid-mismatch':
108
+ case 'source-validation-failed':
109
+ return 'source-package-guid-closure-mismatch';
110
+ case 'source-read-failed':
111
+ case 'import-internal-error':
112
+ case 'mesh-material-slot-topology-change':
113
+ case 'unknown-source-key':
114
+ case 'duplicate-source-key':
115
+ case 'invalid-source-overrides':
116
+ case 'invalid-source-override-payload':
117
+ return 'source-package-conversion-failed';
118
+ }
119
+ }
120
+
121
+ export function normalizeSourcePackageError(
122
+ error: unknown,
123
+ context: SourcePackageErrorContext,
124
+ ): SourcePackageError {
125
+ if (isSourcePackageError(error)) return error;
126
+ if (isScriptablePackFailure(error)) {
127
+ return sourcePackageError('source-package-conversion-failed', context, {
128
+ stage: 'conversion',
129
+ reason: unknownReason(error),
130
+ });
131
+ }
132
+ if (isImportError(error)) {
133
+ const code = importFailureCode(error);
134
+ const detail: Omit<SourcePackageErrorDetail, keyof SourcePackageErrorContext> = {
135
+ stage: code === 'source-package-importer-missing' ? 'importer' : 'conversion',
136
+ reason: unknownReason(error),
137
+ ...(code === 'source-package-importer-missing' && 'registeredImporters' in error.detail
138
+ ? { registeredImporters: error.detail.registeredImporters }
139
+ : {}),
140
+ };
141
+ return sourcePackageError(code, context, detail);
142
+ }
143
+ return sourcePackageError(contextErrorStage(error), context, {
144
+ stage: 'conversion',
145
+ reason: unknownReason(error),
146
+ });
147
+ }
148
+
149
+ function isSourcePackageError(error: unknown): error is SourcePackageError {
150
+ return (
151
+ error !== null &&
152
+ typeof error === 'object' &&
153
+ 'code' in error &&
154
+ typeof error.code === 'string' &&
155
+ error.code in SOURCE_PACKAGE_ERROR_POLICY &&
156
+ 'expected' in error &&
157
+ typeof error.expected === 'string' &&
158
+ 'hint' in error &&
159
+ typeof error.hint === 'string' &&
160
+ 'detail' in error &&
161
+ error.detail !== null &&
162
+ typeof error.detail === 'object'
163
+ );
164
+ }
165
+
166
+ function contextErrorStage(error: unknown): SourcePackageErrorCode {
167
+ return error instanceof SyntaxError
168
+ ? 'source-package-meta-invalid'
169
+ : 'source-package-conversion-failed';
170
+ }
171
+
172
+ function isImportError(error: unknown): error is ImportError {
173
+ return (
174
+ error !== null &&
175
+ typeof error === 'object' &&
176
+ 'code' in error &&
177
+ 'expected' in error &&
178
+ 'hint' in error &&
179
+ 'detail' in error
180
+ );
181
+ }
182
+
183
+ function isScriptablePackFailure(error: unknown): error is {
184
+ readonly code: string;
185
+ readonly expected: string;
186
+ readonly hint: string;
187
+ readonly detail: unknown;
188
+ } {
189
+ return (
190
+ error !== null &&
191
+ typeof error === 'object' &&
192
+ 'code' in error &&
193
+ typeof error.code === 'string' &&
194
+ error.code.startsWith('pack-source-') &&
195
+ 'expected' in error &&
196
+ typeof error.expected === 'string' &&
197
+ 'hint' in error &&
198
+ typeof error.hint === 'string' &&
199
+ 'detail' in error
200
+ );
201
+ }