@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
@@ -0,0 +1,60 @@
1
+ import type { ImportedAsset, ImportProduct, ImportProductFinalizeOptions, ImportProductFinalizeResult, Result } from '@forgeax/engine-types';
2
+ import type { ImportRunnerFs, RunImportMeta } from './import-runner.js';
3
+ import type { ImporterRegistry } from './importer-registry.js';
4
+ export type ProducerReadiness = 'before-consume' | 'on-demand';
5
+ export interface ProducerReadinessError {
6
+ readonly code: 'producer-readiness-invalid';
7
+ readonly expected: "'before-consume' or 'on-demand'";
8
+ readonly hint: 'set producerReadiness to before-consume or on-demand';
9
+ readonly detail: {
10
+ readonly value: unknown;
11
+ };
12
+ }
13
+ export type ProducerReadinessResult = {
14
+ readonly ok: true;
15
+ readonly value: ProducerReadiness;
16
+ } | {
17
+ readonly ok: false;
18
+ readonly error: ProducerReadinessError;
19
+ };
20
+ export declare function parseProducerReadiness(value: unknown): ProducerReadinessResult;
21
+ export interface SourcePackageProducerInput {
22
+ readonly meta: RunImportMeta;
23
+ readonly registry: ImporterRegistry;
24
+ readonly fs: ImportRunnerFs;
25
+ }
26
+ export interface SourcePackageClosureDetail {
27
+ readonly stage: 'closure';
28
+ readonly declaredGuids: readonly string[];
29
+ readonly producedGuids: readonly string[];
30
+ readonly missingGuids: readonly string[];
31
+ readonly unexpectedGuids: readonly string[];
32
+ readonly duplicateGuids: readonly string[];
33
+ }
34
+ export interface SourcePackageClosureError {
35
+ readonly code: 'source-package-guid-closure-mismatch';
36
+ readonly expected: string;
37
+ readonly hint: string;
38
+ readonly detail: SourcePackageClosureDetail;
39
+ }
40
+ export interface SourcePackageProduct {
41
+ readonly anchorGuid: string;
42
+ readonly declaredGuids: readonly string[];
43
+ readonly product: ImportProduct<unknown>;
44
+ }
45
+ type SourcePackageFinalizeError = Extract<ImportProductFinalizeResult, {
46
+ readonly ok: false;
47
+ }>['error'];
48
+ /** Apply an importer-owned transport finalizer without teaching the adapter its kind. */
49
+ export declare function finalizeSourcePackage(product: ImportProduct<unknown>, finalizer: ((product: ImportProduct<unknown>, options: ImportProductFinalizeOptions) => ImportProductFinalizeResult) | undefined, artifactUrl: ImportProductFinalizeOptions['artifactUrl']): Result<ImportProduct<unknown>, SourcePackageFinalizeError>;
50
+ export type SourcePackageProducerResult = {
51
+ readonly ok: true;
52
+ readonly value: SourcePackageProduct;
53
+ } | {
54
+ readonly ok: false;
55
+ readonly error: SourcePackageClosureError;
56
+ };
57
+ export declare function produceSourcePackage(input: SourcePackageProducerInput): Promise<SourcePackageProducerResult>;
58
+ export declare function sourcePackageAssetsByGuid(source: SourcePackageProduct): ReadonlyMap<string, ImportedAsset<unknown>>;
59
+ export {};
60
+ //# sourceMappingURL=source-package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-package.d.ts","sourceRoot":"","sources":["../src/source-package.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-import",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -9,9 +9,14 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
+ "browser": "./dist/browser.mjs",
12
13
  "import": "./dist/index.mjs"
13
14
  },
14
- "./package.json": "./package.json"
15
+ "./package.json": "./package.json",
16
+ "./mesh-bin": {
17
+ "types": "./dist/mesh-bin.d.ts",
18
+ "import": "./dist/mesh-bin.mjs"
19
+ }
15
20
  },
16
21
  "main": "./dist/index.mjs",
17
22
  "types": "./dist/index.d.ts",
@@ -22,11 +27,12 @@
22
27
  "LICENSE"
23
28
  ],
24
29
  "dependencies": {
25
- "@forgeax/engine-ecs": "0.1.3",
26
- "@forgeax/engine-geometry": "0.1.3",
27
- "@forgeax/engine-pack": "0.1.3",
28
- "@forgeax/engine-scene": "0.1.3",
29
- "@forgeax/engine-types": "0.1.3",
30
+ "@forgeax/engine-ddc": "0.1.6",
31
+ "@forgeax/engine-ecs": "0.1.6",
32
+ "@forgeax/engine-geometry": "0.1.6",
33
+ "@forgeax/engine-pack": "0.1.6",
34
+ "@forgeax/engine-scene": "0.1.6",
35
+ "@forgeax/engine-types": "0.1.6",
30
36
  "@noble/hashes": "^2.2.0"
31
37
  },
32
38
  "devDependencies": {
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import type { Asset, AssetRelation, ImportedAsset, ImportResult } from '@forgeax/engine-types';
4
4
  import { describe, expect, it } from 'vitest';
5
+ import { createImportProduct } from '../import-product.js';
5
6
  import { type RunImportMeta, runImport } from '../import-runner.js';
6
7
  import { ImporterRegistry } from '../importer-registry.js';
7
8
 
@@ -188,3 +189,35 @@ describe('import runner producer fact propagation', () => {
188
189
  expect(source).not.toContain("a.kind === 'mesh'");
189
190
  });
190
191
  });
192
+
193
+ describe('engine-import terminal product contract', () => {
194
+ it('projects refs, artifacts, receipts, diagnostics, and source identity together', () => {
195
+ const result = createImportProduct({
196
+ assets: [
197
+ {
198
+ guid: '019e3969-1d48-7c3b-ac24-6d68f457065f',
199
+ kind: 'texture',
200
+ payload: { kind: 'texture', width: 1, height: 1 },
201
+ refs: [],
202
+ artifacts: {},
203
+ },
204
+ ],
205
+ sourceDependencies: ['hero.png'],
206
+ refs: [],
207
+ artifacts: {},
208
+ receipts: [],
209
+ diagnostics: [],
210
+ sourceRevision: 'sha256:hero',
211
+ sourceKey: 'hero/albedo',
212
+ });
213
+
214
+ expect(result.ok).toBe(true);
215
+ if (!result.ok) return;
216
+ expect(result.value.sourceRevision).toBe('sha256:hero');
217
+ expect(result.value.sourceKey).toBe('hero/albedo');
218
+ expect(result.value).toHaveProperty('refs');
219
+ expect(result.value).toHaveProperty('artifacts');
220
+ expect(result.value).toHaveProperty('receipts');
221
+ expect(result.value).toHaveProperty('diagnostics');
222
+ });
223
+ });
@@ -66,7 +66,9 @@ describe('asset-local ImportProduct contract', () => {
66
66
  expect(result.ok).toBe(true);
67
67
  if (result.ok && 'product' in result.value) {
68
68
  expect(result.value).not.toHaveProperty('bins');
69
- expect(result.value.product).not.toHaveProperty('artifacts');
69
+ expect(result.value.product.artifacts).toHaveProperty(`${MESH_GUID}/body`);
70
+ expect(result.value.product.receipts).toHaveLength(2);
71
+ expect(result.value.product.sourceRevision).toBe('source:fixture.source');
70
72
  expect(result.value.pack.assets[0]?.artifacts).toHaveProperty('body');
71
73
  expect(result.value.pack.assets[1]?.artifacts).toHaveProperty('body');
72
74
  }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createImportProduct } from '../import-product.js';
3
+
4
+ describe('Import owner chain', () => {
5
+ it('hands one complete terminal product to the next owner', () => {
6
+ const result = createImportProduct({
7
+ assets: [
8
+ {
9
+ guid: '019e3969-1d48-7c3b-ac24-6d68f457065f',
10
+ kind: 'texture',
11
+ payload: { kind: 'texture' },
12
+ refs: [],
13
+ artifacts: {},
14
+ },
15
+ ],
16
+ sourceDependencies: ['hero.png'],
17
+ refs: [],
18
+ artifacts: {},
19
+ receipts: [],
20
+ diagnostics: [],
21
+ sourceRevision: 'sha256:hero',
22
+ sourceKey: 'hero/albedo',
23
+ });
24
+
25
+ expect(result.ok).toBe(true);
26
+ if (result.ok) {
27
+ expect(result.value).toMatchObject({
28
+ sourceRevision: 'sha256:hero',
29
+ sourceKey: 'hero/albedo',
30
+ refs: [],
31
+ artifacts: {},
32
+ receipts: [],
33
+ diagnostics: [],
34
+ });
35
+ }
36
+ });
37
+ });
@@ -0,0 +1,86 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import type { ScanSourceDeclaration } from '@forgeax/engine-pack/scanner';
3
+ import type {
4
+ Asset,
5
+ AssetGuid as AssetGuidType,
6
+ ImportContext,
7
+ Importer,
8
+ } from '@forgeax/engine-types';
9
+ import { describe, expect, it } from 'vitest';
10
+ import { ImporterRegistry } from '../importer-registry.js';
11
+ import { declaredPackExternalOutputs } from '../scriptable-pack-host.js';
12
+
13
+ const GUID = '019ffa97-0000-7000-8000-000000000001';
14
+
15
+ function parseGuid(value: string): AssetGuidType {
16
+ const parsed = AssetGuid.parse(value);
17
+ if (!parsed.ok) throw parsed.error;
18
+ return parsed.value;
19
+ }
20
+
21
+ describe('ScriptablePack external Meta staging', () => {
22
+ it('imports ordinary Meta dependencies into the current staged generation', async () => {
23
+ const importer: Importer = {
24
+ key: 'fixture',
25
+ import: async (ctx: ImportContext) => ({
26
+ ok: true,
27
+ value: {
28
+ assets: [
29
+ {
30
+ guid: ctx.subAssets[0]?.guid ?? GUID,
31
+ kind: 'mesh',
32
+ payload: {
33
+ kind: 'mesh',
34
+ vertices: new Float32Array([0, 1, 2]),
35
+ attributes: {},
36
+ submeshes: [],
37
+ materialSlots: [],
38
+ } satisfies Asset,
39
+ refs: [],
40
+ artifacts: {},
41
+ },
42
+ ],
43
+ sourceDependencies: [],
44
+ },
45
+ }),
46
+ };
47
+ const registry = new ImporterRegistry();
48
+ registry.register(importer);
49
+ const declaration = {
50
+ format: 'meta.json',
51
+ sourcePath: '/project/assets/model.bin.meta.json',
52
+ sourceRevision: 'sha256:meta',
53
+ value: {
54
+ schemaVersion: '1.0.0',
55
+ kind: 'external-asset-package',
56
+ importer: 'fixture',
57
+ source: 'model.bin',
58
+ importSettings: {},
59
+ subAssets: [{ guid: GUID, sourceIndex: 0, kind: 'mesh' }],
60
+ },
61
+ } satisfies Extract<ScanSourceDeclaration, { readonly format: 'meta.json' }>;
62
+
63
+ const outputs = await declaredPackExternalOutputs(
64
+ new Map([[declaration.sourcePath, declaration]]),
65
+ [],
66
+ [parseGuid(GUID)],
67
+ {
68
+ importerRegistry: registry,
69
+ fsForImport: {
70
+ readSource: async () => ({ ok: true as const, value: new Uint8Array([1, 2, 3]) }),
71
+ },
72
+ assetPaths: {},
73
+ },
74
+ );
75
+
76
+ expect(outputs).toHaveLength(1);
77
+ const output = outputs[0];
78
+ expect(output).toBeDefined();
79
+ if (output === undefined) return;
80
+ expect(AssetGuid.format(output.guid)).toBe(GUID);
81
+ expect(output.asset).toMatchObject({ kind: 'mesh' });
82
+ expect(Array.from((output.asset as Asset & { vertices: Float32Array }).vertices)).toEqual([
83
+ 0, 1, 2,
84
+ ]);
85
+ });
86
+ });
@@ -0,0 +1,47 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import { ok } from '@forgeax/engine-types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { buildScriptablePack } from '../scriptable-pack.js';
5
+ import { createStandardAssetOutputProducerRegistry } from '../scriptable-pack-output-producers.js';
6
+
7
+ const PACKAGE_ID = AssetGuid.parse('019e3969-1d48-7c3b-ac24-6d68f457065e');
8
+ const HERO_GUID = AssetGuid.parse('019e3969-1d48-7c3b-ac24-6d68f457065f');
9
+
10
+ describe('ScriptablePack terminal product contract', () => {
11
+ it('keeps one complete product shape for direct and custom producer outputs', async () => {
12
+ const result = await buildScriptablePack({
13
+ definition: {
14
+ schemaVersion: '1.0.0',
15
+ packageId: PACKAGE_ID.ok ? PACKAGE_ID.value : ({} as never),
16
+ assets: {
17
+ hero: { guid: HERO_GUID.ok ? HERO_GUID.value : ({} as never), kind: 'texture' },
18
+ },
19
+ externalAssets: {},
20
+ build: async () =>
21
+ ok({
22
+ hero: {
23
+ kind: 'texture',
24
+ width: 1,
25
+ height: 1,
26
+ format: 'rgba8unorm',
27
+ data: new Uint8Array(),
28
+ colorSpace: 'srgb',
29
+ mipmap: false,
30
+ },
31
+ }),
32
+ },
33
+ sourcePath: 'hero.pack.ts',
34
+ outputs: createStandardAssetOutputProducerRegistry(),
35
+ sourceClosure: [{ path: 'hero.pack.ts', digest: 'sha256:hero' }],
36
+ authoringContractVersion: 'scriptable-pack-production/1',
37
+ });
38
+
39
+ expect(result.ok).toBe(true);
40
+ if (!result.ok) return;
41
+ expect(result.value.product).toHaveProperty('refs');
42
+ expect(result.value.product).toHaveProperty('artifacts');
43
+ expect(result.value.product).toHaveProperty('receipts');
44
+ expect(result.value.product).toHaveProperty('diagnostics');
45
+ expect(result.value.product).toHaveProperty('sourceRevision');
46
+ });
47
+ });
@@ -1,5 +1,5 @@
1
- import { defineComponent } from '@forgeax/engine-ecs';
2
1
  import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import type { ScriptablePackSceneComponent } from '@forgeax/engine-pack/source';
3
3
  import type {
4
4
  AnimationClip,
5
5
  AnimationGraph,
@@ -31,19 +31,17 @@ function guid(value: string): AssetGuidType {
31
31
 
32
32
  const MATERIAL_GUID = guid('019ffa97-3000-7000-8000-000000000001');
33
33
  const MESH_GUID = guid('019ffa97-3000-7000-8000-000000000002');
34
- const ScriptableMeshFilter = defineComponent('ScriptablePackTestMeshFilter', {
35
- assetHandle: 'shared<MeshAsset>',
36
- });
37
- const ScriptableMeshRenderer = defineComponent('ScriptablePackTestMeshRenderer', {
38
- materials: 'array<shared<MaterialAsset>>',
39
- });
40
- const componentCatalog = new Map<
41
- string,
42
- typeof ScriptableMeshFilter | typeof ScriptableMeshRenderer
43
- >([
44
- [ScriptableMeshFilter.name, ScriptableMeshFilter],
45
- [ScriptableMeshRenderer.name, ScriptableMeshRenderer],
46
- ]);
34
+ const EQUIRECT_GUID = guid('019ffa97-3000-7000-8000-000000000005');
35
+ const sceneComponents = [
36
+ {
37
+ name: 'ScriptablePackTestMeshFilter',
38
+ fields: { assetHandle: 'shared<MeshAsset>' },
39
+ },
40
+ {
41
+ name: 'ScriptablePackTestMeshRenderer',
42
+ fields: { materials: 'array<shared<MaterialAsset>>' },
43
+ },
44
+ ] satisfies readonly ScriptablePackSceneComponent[];
47
45
 
48
46
  const OUTPUT_GUID = guid('019ffa97-3000-7000-8000-000000000010');
49
47
  const ATLAS_GUID = guid('019ffa97-3000-7000-8000-000000000011');
@@ -165,8 +163,6 @@ const matrixAssets = [
165
163
  ] as const satisfies readonly Asset[];
166
164
 
167
165
  describe('standard ScriptablePack output producers', () => {
168
- void ScriptableMeshFilter;
169
- void ScriptableMeshRenderer;
170
166
  it('registers the production producer versions for all ordinary Asset kinds', () => {
171
167
  const registry = createStandardAssetOutputProducerRegistry();
172
168
  expect(registry.versions()).toEqual({
@@ -180,7 +176,7 @@ describe('standard ScriptablePack output producers', () => {
180
176
  'particle-effect': 'ordinary-pod/1',
181
177
  'render-pipeline': 'ordinary-pod/1',
182
178
  sampler: 'ordinary-pod/1',
183
- scene: 'scene-pack/2',
179
+ scene: 'scene-pack/3',
184
180
  skeleton: 'ordinary-pod/1',
185
181
  skin: 'ordinary-pod/1',
186
182
  texture: 'ordinary-pod/1',
@@ -262,7 +258,7 @@ describe('standard ScriptablePack output producers', () => {
262
258
  });
263
259
 
264
260
  it('serializes a scene through component schemas and emits recursive GUID refs', async () => {
265
- const registry = createStandardAssetOutputProducerRegistry();
261
+ const registry = createStandardAssetOutputProducerRegistry(sceneComponents);
266
262
  const producer = registry.get('scene');
267
263
  expect(producer).toBeDefined();
268
264
  if (producer === undefined) return;
@@ -285,7 +281,6 @@ describe('standard ScriptablePack output producers', () => {
285
281
  guid: '019ffa97-3000-7000-8000-000000000003',
286
282
  sourceKey: 'scene/showcase',
287
283
  asset: scene,
288
- components: componentCatalog,
289
284
  });
290
285
 
291
286
  expect(result.ok).toBe(true);
@@ -322,6 +317,70 @@ describe('standard ScriptablePack output producers', () => {
322
317
  ]);
323
318
  });
324
319
 
320
+ it('fails closed when a scene component schema is not declared by the Pack', async () => {
321
+ const registry = createStandardAssetOutputProducerRegistry();
322
+ const producer = registry.get('scene');
323
+ expect(producer).toBeDefined();
324
+ if (producer === undefined) return;
325
+
326
+ const result = await producer.produce({
327
+ guid: '019ffa97-3000-7000-8000-000000000004',
328
+ sourceKey: 'scene/unknown-component',
329
+ asset: {
330
+ kind: 'scene',
331
+ entities: [{ localId: 0 as never, components: { Mystery: { asset: 'not-a-ref' } } }],
332
+ },
333
+ });
334
+
335
+ expect(result).toMatchObject({
336
+ ok: false,
337
+ error: {
338
+ code: 'import-internal-error',
339
+ detail: { reason: expect.stringContaining('missing from sceneComponents') },
340
+ },
341
+ });
342
+ });
343
+
344
+ it('deduplicates one shared scene GUID across multiple component fields', async () => {
345
+ const registry = createStandardAssetOutputProducerRegistry([
346
+ ...sceneComponents,
347
+ { name: 'Skylight', fields: { equirect: 'shared<EquirectAsset>' } },
348
+ { name: 'SkyboxBackground', fields: { equirect: 'shared<EquirectAsset>' } },
349
+ ]);
350
+ const producer = registry.get('scene');
351
+ expect(producer).toBeDefined();
352
+ if (producer === undefined) return;
353
+
354
+ const result = await producer.produce({
355
+ guid: '019ffa97-3000-7000-8000-000000000006',
356
+ sourceKey: 'scene/environment',
357
+ asset: {
358
+ kind: 'scene',
359
+ entities: [
360
+ {
361
+ localId: 0 as never,
362
+ components: { Skylight: { equirect: AssetGuid.format(EQUIRECT_GUID) } },
363
+ },
364
+ {
365
+ localId: 1 as never,
366
+ components: { SkyboxBackground: { equirect: AssetGuid.format(EQUIRECT_GUID) } },
367
+ },
368
+ ],
369
+ },
370
+ });
371
+
372
+ expect(result.ok).toBe(true);
373
+ if (!result.ok) return;
374
+ expect(result.value.refs).toHaveLength(1);
375
+ expect(result.value.refs[0]?.guid).toBe(AssetGuid.format(EQUIRECT_GUID));
376
+ expect(result.value.payload).toMatchObject({
377
+ entities: [
378
+ { components: { Skylight: { equirect: 0 } } },
379
+ { components: { SkyboxBackground: { equirect: 0 } } },
380
+ ],
381
+ });
382
+ });
383
+
325
384
  it('packs mesh bytes and projects material parent references', async () => {
326
385
  const registry = createStandardAssetOutputProducerRegistry();
327
386
  const mesh = registry.get('mesh');
@@ -74,8 +74,7 @@ describe('ScriptablePack staged asset snapshot source', () => {
74
74
  expect(second.ok && second.value.digest).toMatch(/^sha256:/);
75
75
  });
76
76
 
77
- it('fails a content dependency cycle structurally without consulting fallback', async () => {
78
- let fallbackReads = 0;
77
+ it('fails a content dependency cycle structurally', async () => {
79
78
  const owners: ScriptablePackStagedOwner[] = [
80
79
  {
81
80
  id: 'a.pack.ts',
@@ -97,12 +96,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
97
96
  const source = createScriptablePackStagedAssetSnapshotSource({
98
97
  generation: 1,
99
98
  owners,
100
- fallback: {
101
- async readByGuid() {
102
- fallbackReads += 1;
103
- return ok({ asset: mesh(8), generation: 0, digest: 'lkg' });
104
- },
105
- },
106
99
  });
107
100
  const result = await source.readByGuid(ROOT);
108
101
  expect(result).toMatchObject({
@@ -112,14 +105,63 @@ describe('ScriptablePack staged asset snapshot source', () => {
112
105
  actual: 'a.pack.ts -> b.pack.ts -> a.pack.ts',
113
106
  },
114
107
  });
115
- expect(fallbackReads).toBe(0);
108
+ });
109
+
110
+ it('fails a content dependency without a local owner', async () => {
111
+ const source = createScriptablePackStagedAssetSnapshotSource({
112
+ generation: 1,
113
+ owners: [
114
+ {
115
+ id: 'root.pack.ts',
116
+ guids: [ROOT],
117
+ async build(ownerSource) {
118
+ const result = await ownerSource.readByGuid(DEPENDENCY);
119
+ return result.ok ? ok([{ guid: ROOT, asset: result.value.asset }]) : result;
120
+ },
121
+ },
122
+ ],
123
+ });
124
+
125
+ await expect(source.readByGuid(ROOT)).resolves.toMatchObject({
126
+ ok: false,
127
+ error: {
128
+ code: 'asset-not-imported',
129
+ expected: 'a staged local owner for the requested GUID',
130
+ },
131
+ });
132
+ });
133
+
134
+ it('resolves declared direct Pack outputs in the same generation', async () => {
135
+ const source = createScriptablePackStagedAssetSnapshotSource({
136
+ generation: 3,
137
+ declaredExternalOutputs: [{ guid: DEPENDENCY, asset: mesh(7) }],
138
+ owners: [
139
+ {
140
+ id: 'root.pack.ts',
141
+ guids: [ROOT],
142
+ async build(ownerSource) {
143
+ const dependency = await ownerSource.readByGuid(DEPENDENCY);
144
+ return dependency.ok
145
+ ? ok([
146
+ {
147
+ guid: ROOT,
148
+ asset: mesh((dependency.value.asset as MeshAsset).vertices[0] ?? 0),
149
+ },
150
+ ])
151
+ : dependency;
152
+ },
153
+ },
154
+ ],
155
+ });
156
+ const result = await source.readByGuid(ROOT);
157
+ expect(result).toMatchObject({ ok: true, value: { generation: 3 } });
158
+ expect(result.ok && (result.value.asset as MeshAsset).vertices[0]).toBe(7);
116
159
  });
117
160
 
118
161
  it('retries a refused local owner on the same source and coalesces repaired reads', async () => {
119
162
  let rootBuilds = 0;
120
163
  let dependencyBuilds = 0;
121
164
  let healthyBuilds = 0;
122
- let fallbackReads = 0;
123
165
  let repaired = false;
124
166
  let releaseRepair!: () => void;
125
167
  const repairBuildGate = new Promise<void>((resolve) => {
@@ -166,12 +208,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
166
208
  const source = createScriptablePackStagedAssetSnapshotSource({
167
209
  generation: 27,
168
210
  owners,
169
- fallback: {
170
- async readByGuid() {
171
- fallbackReads += 1;
172
- return ok({ asset: mesh(99), generation: 0, digest: 'sha256:lkg' });
173
- },
174
- },
175
211
  });
176
212
 
177
213
  const refused = await source.readByGuid(ROOT);
@@ -185,7 +221,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
185
221
  });
186
222
  expect(rootBuilds).toBe(1);
187
223
  expect(dependencyBuilds).toBe(1);
188
- expect(fallbackReads).toBe(0);
189
224
 
190
225
  const healthy = await source.readByGuid(HEALTHY);
191
226
  expect(healthy).toMatchObject({
@@ -193,7 +228,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
193
228
  value: { generation: 27, digest: 'sha256:healthy' },
194
229
  });
195
230
  expect(healthyBuilds).toBe(1);
196
- expect(fallbackReads).toBe(0);
197
231
 
198
232
  repaired = true;
199
233
  const repairedReads = [
@@ -220,7 +254,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
220
254
  expect(rootBuilds).toBe(2);
221
255
  expect(dependencyBuilds).toBe(1);
222
256
  expect(healthyBuilds).toBe(1);
223
- expect(fallbackReads).toBe(0);
224
257
 
225
258
  const dependency = await source.readByGuid(DEPENDENCY);
226
259
  expect(dependency).toMatchObject({
@@ -232,7 +265,6 @@ describe('ScriptablePack staged asset snapshot source', () => {
232
265
  expect(memoized).toMatchObject({ ok: true, value: { generation: 27, digest: 'sha256:root' } });
233
266
  expect(memoized.ok && (memoized.value.asset as MeshAsset).vertices[0]).toBe(3);
234
267
  expect(rootBuilds).toBe(2);
235
- expect(fallbackReads).toBe(0);
236
268
  });
237
269
 
238
270
  it('carries the staged generation into every locally built snapshot', async () => {
package/src/browser.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
2
+ export {
3
+ type DdcPack,
4
+ type ImportRunnerFs,
5
+ normaliseForPack,
6
+ type RunImportMeta,
7
+ type RunImportOk,
8
+ type RunImportProductResult,
9
+ type RunImportResult,
10
+ runImport,
11
+ SHADER_RESERVED_IMPORTER_KEY,
12
+ } from './import-runner.js';
13
+ export { ImporterRegistry } from './importer-registry.js';
14
+ export { packMeshBinV4 } from './mesh-bin.js';