@forgeax/engine-pack 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 (128) hide show
  1. package/README.md +159 -46
  2. package/dist/__tests__/inventory-schema.test.d.ts +2 -0
  3. package/dist/__tests__/inventory-schema.test.d.ts.map +1 -0
  4. package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts +2 -0
  5. package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts +2 -0
  7. package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/parameterized-pack.unit.test.d.ts +2 -0
  9. package/dist/__tests__/parameterized-pack.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
  11. package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
  12. package/dist/build.d.ts +9 -4
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.mjs +3317 -804
  15. package/dist/build.mjs.map +1 -1
  16. package/dist/builtin.mjs +37 -4
  17. package/dist/builtin.mjs.map +1 -1
  18. package/dist/catalog-builder.d.ts.map +1 -1
  19. package/dist/cli-asset.d.ts.map +1 -1
  20. package/dist/cli-asset.mjs +1368 -850
  21. package/dist/cli-asset.mjs.map +1 -1
  22. package/dist/deriveAssetName.d.ts +11 -7
  23. package/dist/deriveAssetName.d.ts.map +1 -1
  24. package/dist/evidence/material-cook.d.ts.map +1 -1
  25. package/dist/evidence/source-inventory.d.ts.map +1 -1
  26. package/dist/guid.d.ts +15 -0
  27. package/dist/guid.d.ts.map +1 -1
  28. package/dist/guid.mjs +53 -5
  29. package/dist/guid.mjs.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.mjs +1255 -561
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/inventory/binding.d.ts +6 -0
  35. package/dist/inventory/binding.d.ts.map +1 -0
  36. package/dist/inventory/declaration.d.ts +24 -0
  37. package/dist/inventory/declaration.d.ts.map +1 -0
  38. package/dist/inventory/index.d.ts +3 -0
  39. package/dist/inventory/index.d.ts.map +1 -0
  40. package/dist/inventory/sync.d.ts +5 -0
  41. package/dist/inventory/sync.d.ts.map +1 -0
  42. package/dist/material-cook.mjs +3 -1
  43. package/dist/material-cook.mjs.map +1 -1
  44. package/dist/name.mjs +4 -7
  45. package/dist/name.mjs.map +1 -1
  46. package/dist/package-finalizer.d.ts +2 -20
  47. package/dist/package-finalizer.d.ts.map +1 -1
  48. package/dist/parameterized-pack-node.d.ts +26 -0
  49. package/dist/parameterized-pack-node.d.ts.map +1 -0
  50. package/dist/parameterized-pack-node.mjs +11093 -0
  51. package/dist/parameterized-pack-node.mjs.map +1 -0
  52. package/dist/parameterized-pack.d.ts +233 -0
  53. package/dist/parameterized-pack.d.ts.map +1 -0
  54. package/dist/resolve-asset-source.d.ts +1 -3
  55. package/dist/resolve-asset-source.d.ts.map +1 -1
  56. package/dist/resolve-asset-source.mjs +6 -81
  57. package/dist/resolve-asset-source.mjs.map +1 -1
  58. package/dist/runtime.mjs +77 -7
  59. package/dist/runtime.mjs.map +1 -1
  60. package/dist/scanner.d.ts +19 -8
  61. package/dist/scanner.d.ts.map +1 -1
  62. package/dist/scanner.mjs +1169 -690
  63. package/dist/scanner.mjs.map +1 -1
  64. package/dist/schema-compiled.d.ts.map +1 -1
  65. package/dist/schema.mjs +79 -7
  66. package/dist/schema.mjs.map +1 -1
  67. package/dist/scriptable-pack-node.d.ts +8 -32
  68. package/dist/scriptable-pack-node.d.ts.map +1 -1
  69. package/dist/scriptable-pack-node.mjs +576 -1021
  70. package/dist/scriptable-pack-node.mjs.map +1 -1
  71. package/dist/scriptable-pack-worker.mjs +29 -11
  72. package/dist/scriptable-pack-worker.mjs.map +1 -1
  73. package/dist/scriptable-pack.d.ts +19 -532
  74. package/dist/scriptable-pack.d.ts.map +1 -1
  75. package/dist/scriptable-pack.mjs +1042 -527
  76. package/dist/scriptable-pack.mjs.map +1 -1
  77. package/package.json +7 -7
  78. package/schema/meta.schema.json +1 -1
  79. package/schema/pack.schema.json +76 -6
  80. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  81. package/src/__tests__/inventory-schema.test.ts +109 -0
  82. package/src/__tests__/pack.unit.test.ts +1 -238
  83. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  84. package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
  85. package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
  86. package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
  87. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  88. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  89. package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
  90. package/src/__tests__/scanner-inventory.test.ts +24 -12
  91. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  92. package/src/__tests__/topology.unit.test.ts +23 -0
  93. package/src/build.ts +28 -18
  94. package/src/catalog-builder.ts +61 -34
  95. package/src/cli-asset.ts +70 -85
  96. package/src/deriveAssetName.ts +14 -13
  97. package/src/evidence/material-cook.ts +3 -1
  98. package/src/evidence/source-inventory.ts +34 -21
  99. package/src/guid.ts +65 -4
  100. package/src/index.ts +12 -11
  101. package/src/inventory/binding.ts +25 -0
  102. package/src/inventory/declaration.ts +168 -0
  103. package/src/inventory/index.ts +18 -0
  104. package/src/inventory/sync.ts +22 -0
  105. package/src/package-finalizer.ts +2 -42
  106. package/src/parameterized-pack-node.ts +1968 -0
  107. package/src/parameterized-pack.ts +1493 -0
  108. package/src/resolve-asset-source.ts +4 -76
  109. package/src/scanner.ts +241 -59
  110. package/src/schema-compiled.ts +2 -0
  111. package/src/scriptable-pack-node.ts +111 -722
  112. package/src/scriptable-pack-worker.ts +48 -16
  113. package/src/scriptable-pack.ts +27 -939
  114. package/dist/.tsbuildinfo +0 -1
  115. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  116. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  117. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  118. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  119. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  120. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  121. package/dist/config.d.ts +0 -6
  122. package/dist/config.d.ts.map +0 -1
  123. package/dist/config.mjs +0 -29
  124. package/dist/config.mjs.map +0 -1
  125. package/src/__tests__/load-asset-config.test.ts +0 -121
  126. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  127. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  128. package/src/config.ts +0 -36
@@ -1,789 +0,0 @@
1
- import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import type { MeshAsset } from '@forgeax/engine-types';
5
- import { AssetError, err, ok } from '@forgeax/engine-types';
6
- import { describe, expect, it } from 'vitest';
7
- import { AssetGuid } from '../guid.js';
8
- import {
9
- createScriptablePackAuthoringGateway,
10
- projectScriptablePackMeta,
11
- SCRIPTABLE_PACK_ASSET_KINDS,
12
- type ScriptablePackAssetDeclarations,
13
- type ScriptablePackAuthoringMutation,
14
- type ScriptablePackDefinition,
15
- SOURCE_AUTHORING_OPERATION_DESCRIPTORS,
16
- validateScriptablePackDefinition,
17
- } from '../scriptable-pack.js';
18
- import {
19
- createFileSystemScriptablePackAuthoringPort,
20
- createScriptablePackModuleExecutorPool,
21
- loadScriptablePack,
22
- } from '../scriptable-pack-node.js';
23
-
24
- function guid(value: string) {
25
- const parsed = AssetGuid.parse(value);
26
- if (!parsed.ok) throw parsed.error;
27
- return parsed.value;
28
- }
29
-
30
- function definition(): ScriptablePackDefinition {
31
- const assets = {
32
- 'scene/house': {
33
- guid: guid('019ffa97-be3d-7234-9128-257469394b43'),
34
- kind: 'scene',
35
- name: 'House',
36
- },
37
- 'geometry/wall': {
38
- guid: guid('019ffa97-a5ee-7645-b613-043323952808'),
39
- kind: 'mesh',
40
- name: 'Wall',
41
- },
42
- } as const satisfies ScriptablePackAssetDeclarations;
43
- return {
44
- schemaVersion: '1.0.0',
45
- packageId: guid('019ffa97-8b39-7ad2-84cc-273268591ab4'),
46
- name: 'Low Poly House',
47
- assets,
48
- externalAssets: {
49
- wallTexture: guid('019ffa97-c10b-7f90-90f5-6b814226f341'),
50
- },
51
- build: () =>
52
- ok({
53
- 'geometry/wall': { kind: 'mesh' } as unknown as MeshAsset,
54
- 'scene/house': { kind: 'scene', entities: [] },
55
- }),
56
- };
57
- }
58
-
59
- describe('ScriptablePack definition and Meta projection', () => {
60
- it('exposes the complete ordinary Asset kind discovery set', () => {
61
- expect(SCRIPTABLE_PACK_ASSET_KINDS).toEqual([
62
- 'mesh',
63
- 'material',
64
- 'scene',
65
- 'texture',
66
- 'equirect',
67
- 'sampler',
68
- 'font',
69
- 'render-pipeline',
70
- 'tileset',
71
- 'video',
72
- 'skeleton',
73
- 'skin',
74
- 'animation-clip',
75
- 'animation-graph',
76
- 'audio',
77
- 'particle-effect',
78
- ]);
79
- });
80
-
81
- it('projects deterministic Meta without invoking build', () => {
82
- const source = definition();
83
- let buildCalls = 0;
84
- const tracked = {
85
- ...source,
86
- build: () => {
87
- buildCalls += 1;
88
- return source.build({} as never);
89
- },
90
- };
91
- const validated = validateScriptablePackDefinition(tracked, 'house.pack.ts');
92
- expect(validated.ok).toBe(true);
93
- if (!validated.ok) return;
94
-
95
- const wall = source.assets['geometry/wall'];
96
- if (wall === undefined) throw new Error('fixture wall descriptor missing');
97
- wall.guid[0] = 255;
98
-
99
- const meta = projectScriptablePackMeta(validated.value, 'house.pack.ts');
100
- expect(buildCalls).toBe(0);
101
- expect(meta.subAssets.map((asset) => asset.sourceKey)).toEqual([
102
- 'geometry/wall',
103
- 'scene/house',
104
- ]);
105
- expect(meta.subAssets.map((asset) => asset.sourceIndex)).toEqual([0, 1]);
106
- expect(meta.subAssets[0]?.guid).toBe('019ffa97-a5ee-7645-b613-043323952808');
107
- expect(meta.importSettings.externalAssets).toEqual([
108
- {
109
- alias: 'wallTexture',
110
- guid: '019ffa97-c10b-7f90-90f5-6b814226f341',
111
- },
112
- ]);
113
- });
114
-
115
- it('freezes component-token schemas as an ECS-neutral scene projection', () => {
116
- const source = definition();
117
- const input = {
118
- ...source,
119
- sceneComponents: [
120
- {
121
- name: 'MeshFilter',
122
- fields: { assetHandle: { type: 'shared<MeshAsset>', default: 0 } },
123
- },
124
- ],
125
- };
126
- const validated = validateScriptablePackDefinition(input, 'scene-components.pack.ts');
127
- expect(validated.ok).toBe(true);
128
- if (!validated.ok) return;
129
-
130
- expect(validated.value.sceneComponents).toEqual([
131
- { name: 'MeshFilter', fields: { assetHandle: 'shared<MeshAsset>' } },
132
- ]);
133
- expect(validated.value.sceneComponents?.[0]).not.toBe(input.sceneComponents[0]);
134
- });
135
-
136
- it('rejects duplicate scene component names before build', () => {
137
- const source = definition();
138
- const validated = validateScriptablePackDefinition(
139
- {
140
- ...source,
141
- sceneComponents: [
142
- { name: 'MeshFilter', fields: {} },
143
- { name: 'MeshFilter', fields: {} },
144
- ],
145
- },
146
- 'duplicate-scene-components.pack.ts',
147
- );
148
- expect(validated).toMatchObject({
149
- ok: false,
150
- error: {
151
- code: 'pack-source-definition-invalid',
152
- detail: { propertyPath: '$.sceneComponents[1].name' },
153
- },
154
- });
155
- });
156
-
157
- it('rejects duplicate local GUIDs before build', () => {
158
- const source = definition();
159
- const duplicate = {
160
- ...source,
161
- assets: {
162
- ...source.assets,
163
- duplicate: source.assets['geometry/wall'],
164
- },
165
- };
166
- const validated = validateScriptablePackDefinition(duplicate, 'duplicate.pack.ts');
167
- expect(validated).toMatchObject({
168
- ok: false,
169
- error: {
170
- code: 'pack-source-definition-invalid',
171
- detail: { propertyPath: '$.assets["duplicate"].guid' },
172
- },
173
- });
174
- });
175
-
176
- it('loads through an injected module executor and times out deterministically', async () => {
177
- const disposalReasons: string[] = [];
178
- const loaded = await loadScriptablePack('house.pack.ts', {
179
- executor: { load: async () => ({ default: definition() }) },
180
- });
181
- expect(loaded.ok).toBe(true);
182
-
183
- const metadata = await loadScriptablePack('house.pack.ts', {
184
- metadataOnly: true,
185
- executor: {
186
- load: async () => ({ default: definition() }),
187
- dispose: (reason) => {
188
- disposalReasons.push(reason);
189
- },
190
- },
191
- });
192
- expect(metadata.ok).toBe(true);
193
-
194
- const timedOut = await loadScriptablePack('slow.pack.ts', {
195
- timeoutMs: 1,
196
- executor: {
197
- load: () => new Promise(() => undefined),
198
- dispose: (reason) => {
199
- disposalReasons.push(reason);
200
- },
201
- },
202
- });
203
- expect(timedOut).toMatchObject({
204
- ok: false,
205
- error: {
206
- code: 'pack-source-load-failed',
207
- detail: { reason: 'timeout', sourcePath: 'slow.pack.ts' },
208
- },
209
- });
210
- expect(disposalReasons).toEqual(['complete', 'timeout']);
211
- });
212
-
213
- it('terminates a real worker whose module initialization blocks synchronously', async () => {
214
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-worker-'));
215
- try {
216
- const sourcePath = join(root, 'blocked.pack.ts');
217
- await writeFile(sourcePath, 'while (true) {}\nexport default {};\n');
218
- const started = Date.now();
219
- const loaded = await loadScriptablePack(sourcePath, { timeoutMs: 50 });
220
- expect(loaded).toMatchObject({
221
- ok: false,
222
- error: { code: 'pack-source-load-failed', detail: { reason: 'timeout' } },
223
- });
224
- expect(Date.now() - started).toBeLessThan(2_000);
225
- } finally {
226
- await rm(root, { recursive: true, force: true });
227
- }
228
- });
229
-
230
- it('returns one structured build timeout after terminating the worker and compile root', async () => {
231
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-build-timeout-'));
232
- const workerTempParent = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-build-temp-parent-'));
233
- const previousTmpdir = process.env.TMPDIR;
234
- process.env.TMPDIR = workerTempParent;
235
- const unhandled: unknown[] = [];
236
- const onUnhandled = (reason: unknown) => unhandled.push(reason);
237
- process.on('unhandledRejection', onUnhandled);
238
- try {
239
- const sourcePath = join(root, 'never-settles.pack.ts');
240
- await writeFile(
241
- sourcePath,
242
- [
243
- 'const packageId = new Uint8Array(16);',
244
- 'packageId[15] = 1;',
245
- 'const meshGuid = new Uint8Array(16);',
246
- 'meshGuid[15] = 2;',
247
- 'export default {',
248
- "schemaVersion: '1.0.0', packageId,",
249
- "assets: { mesh: { guid: meshGuid, kind: 'mesh' } }, externalAssets: {},",
250
- 'build: () => new Promise(() => undefined),',
251
- '};',
252
- ].join('\n'),
253
- );
254
-
255
- const loaded = await loadScriptablePack(sourcePath, { buildTimeoutMs: 25 });
256
- expect(loaded.ok).toBe(true);
257
- if (!loaded.ok) return;
258
- const built = await loaded.value.build({} as never);
259
- expect(built).toMatchObject({
260
- ok: false,
261
- error: {
262
- code: 'pack-source-load-failed',
263
- detail: {
264
- sourcePath,
265
- reason: 'timeout',
266
- phase: 'build',
267
- timeoutMs: 25,
268
- diagnostic: 'ScriptablePack build exceeded 25ms',
269
- },
270
- },
271
- });
272
- expect(await readdir(workerTempParent)).toEqual([]);
273
- await new Promise((resolve) => setTimeout(resolve, 25));
274
- expect(unhandled).toEqual([]);
275
- } finally {
276
- process.off('unhandledRejection', onUnhandled);
277
- if (previousTmpdir === undefined) delete process.env.TMPDIR;
278
- else process.env.TMPDIR = previousTmpdir;
279
- await rm(root, { recursive: true, force: true });
280
- await rm(workerTempParent, { recursive: true, force: true });
281
- }
282
- });
283
-
284
- it('preserves an ordinary worker module-load diagnostic', async () => {
285
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-worker-error-'));
286
- try {
287
- const sourcePath = join(root, 'broken.pack.ts');
288
- await writeFile(sourcePath, "import './missing-helper.ts';\nexport default {};\n");
289
- const loaded = await loadScriptablePack(sourcePath, { metadataOnly: true });
290
- expect(loaded).toMatchObject({
291
- ok: false,
292
- error: {
293
- code: 'pack-source-load-failed',
294
- detail: {
295
- reason: 'module-load',
296
- diagnostic: expect.stringContaining('missing-helper'),
297
- },
298
- },
299
- });
300
- } finally {
301
- await rm(root, { recursive: true, force: true });
302
- }
303
- });
304
-
305
- it('loads helper changes through a fresh worker instead of the Node module cache', async () => {
306
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-helper-'));
307
- try {
308
- const sourcePath = join(root, 'fresh.pack.ts');
309
- const helperPath = join(root, 'helper.ts');
310
- await writeFile(helperPath, 'export const marker = 1;\n');
311
- await writeFile(
312
- sourcePath,
313
- [
314
- "import { marker } from './helper.ts';",
315
- 'const guid = (last: number) => new Uint8Array([...Array(15).fill(0), last]);',
316
- 'export default {',
317
- "schemaVersion: '1.0.0', packageId: guid(1),",
318
- "assets: { mesh: { guid: guid(2), kind: 'mesh' } }, externalAssets: {},",
319
- "build: () => ({ ok: true, value: { mesh: { kind: 'mesh', marker } } }),",
320
- '};',
321
- ].join('\n'),
322
- );
323
- const first = await loadScriptablePack(sourcePath);
324
- expect(first.ok).toBe(true);
325
- if (!first.ok) return;
326
- const firstBuild = await first.value.build({} as never);
327
- expect(firstBuild.ok && (firstBuild.value.mesh as unknown as { marker: number }).marker).toBe(
328
- 1,
329
- );
330
-
331
- await writeFile(helperPath, 'export const marker = 2;\n');
332
- const second = await loadScriptablePack(sourcePath);
333
- expect(second.ok).toBe(true);
334
- if (!second.ok) return;
335
- const secondBuild = await second.value.build({} as never);
336
- expect(
337
- secondBuild.ok && (secondBuild.value.mesh as unknown as { marker: number }).marker,
338
- ).toBe(2);
339
- } finally {
340
- await rm(root, { recursive: true, force: true });
341
- }
342
- });
343
-
344
- it('recycles a bounded worker pool without retaining a build closure', async () => {
345
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-worker-pool-'));
346
- const pool = createScriptablePackModuleExecutorPool({
347
- maxWorkers: 1,
348
- maxTasksPerWorker: 8,
349
- });
350
- try {
351
- const sourcePath = join(root, 'pooled.pack.ts');
352
- const helperPath = join(root, 'helper.ts');
353
- await writeFile(helperPath, 'export const marker = 1;\n');
354
- await writeFile(
355
- sourcePath,
356
- [
357
- "import { marker } from './helper.ts';",
358
- 'const guid = (last: number) => new Uint8Array([...Array(15).fill(0), last]);',
359
- 'export default {',
360
- "schemaVersion: '1.0.0', packageId: guid(1),",
361
- "assets: { mesh: { guid: guid(2), kind: 'mesh' } }, externalAssets: {},",
362
- "build: () => ({ ok: true, value: { mesh: { kind: 'mesh', marker } } }),",
363
- '};',
364
- ].join('\n'),
365
- );
366
- const first = await loadScriptablePack(sourcePath, {
367
- executor: await pool.acquire(),
368
- });
369
- expect(first.ok).toBe(true);
370
- if (!first.ok) return;
371
- const firstBuild = await first.value.build({} as never);
372
- expect(firstBuild.ok && (firstBuild.value.mesh as unknown as { marker: number }).marker).toBe(
373
- 1,
374
- );
375
-
376
- await writeFile(helperPath, 'export const marker = 2;\n');
377
- const second = await loadScriptablePack(sourcePath, {
378
- executor: await pool.acquire(),
379
- });
380
- expect(second.ok).toBe(true);
381
- if (!second.ok) return;
382
- const secondBuild = await second.value.build({} as never);
383
- expect(
384
- secondBuild.ok && (secondBuild.value.mesh as unknown as { marker: number }).marker,
385
- ).toBe(2);
386
- } finally {
387
- await pool.dispose();
388
- await rm(root, { recursive: true, force: true });
389
- }
390
- });
391
-
392
- it('resolves bare imports past an unrelated nested Vite node_modules cache', async () => {
393
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-node-floor-'));
394
- try {
395
- const sourceRoot = join(root, 'games', 'sample', 'assets');
396
- await mkdir(join(root, 'games', 'sample', 'node_modules', '.vite', 'deps'), {
397
- recursive: true,
398
- });
399
- const packageRoot = join(root, 'node_modules', 'scriptable-pack-fixture');
400
- await mkdir(packageRoot, { recursive: true });
401
- await writeFile(
402
- join(packageRoot, 'package.json'),
403
- JSON.stringify({
404
- name: 'scriptable-pack-fixture',
405
- type: 'module',
406
- exports: { types: './index.d.ts', import: './index.mjs' },
407
- }),
408
- );
409
- await writeFile(join(packageRoot, 'index.mjs'), 'export const packageMarker = 40;\n');
410
- await writeFile(
411
- join(packageRoot, 'index.d.ts'),
412
- 'export declare const packageMarker: number;\n',
413
- );
414
- await mkdir(sourceRoot, { recursive: true });
415
- await writeFile(join(sourceRoot, 'helper.ts'), 'export const helperMarker: number = 2;\n');
416
- const sourcePath = join(sourceRoot, 'portable.pack.ts');
417
- await writeFile(
418
- sourcePath,
419
- [
420
- "import { packageMarker } from 'scriptable-pack-fixture';",
421
- "import { helperMarker } from './helper.ts';",
422
- 'const guid = (last: number) => new Uint8Array([...Array(15).fill(0), last]);',
423
- 'export default {',
424
- "schemaVersion: '1.0.0', packageId: guid(1),",
425
- "assets: { mesh: { guid: guid(2), kind: 'mesh' } }, externalAssets: {},",
426
- "build: () => ({ ok: true, value: { mesh: { kind: 'mesh', marker: packageMarker + helperMarker } } }),",
427
- '};',
428
- ].join('\n'),
429
- );
430
-
431
- const loaded = await loadScriptablePack(sourcePath);
432
- expect(loaded.ok).toBe(true);
433
- if (!loaded.ok) return;
434
- const built = await loaded.value.build({} as never);
435
- expect(built.ok && (built.value.mesh as unknown as { marker: number }).marker).toBe(42);
436
- } finally {
437
- await rm(root, { recursive: true, force: true });
438
- }
439
- });
440
-
441
- it('preserves structured external-read failures across the worker boundary', async () => {
442
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-worker-read-error-'));
443
- try {
444
- const sourcePath = join(root, 'reader-error.pack.ts');
445
- await writeFile(
446
- sourcePath,
447
- [
448
- 'const packageId = new Uint8Array([1,159,250,151,0,0,112,0,128,0,0,0,0,0,0,0]);',
449
- 'const dependency = new Uint8Array([1,159,250,151,0,0,112,0,128,0,0,0,0,0,0,1]);',
450
- 'export default {',
451
- "schemaVersion: '1.0.0', packageId,",
452
- "assets: { mesh: { guid: new Uint8Array([1,159,250,151,0,0,112,0,128,0,0,0,0,0,0,2]), kind: 'mesh' } },",
453
- 'externalAssets: { dependency },',
454
- 'build: (reader: { readByGuid: (guid: Uint8Array) => Promise<unknown> }) => reader.readByGuid(dependency),',
455
- '};',
456
- ].join('\n'),
457
- );
458
- const loaded = await loadScriptablePack(sourcePath);
459
- expect(loaded.ok).toBe(true);
460
- if (!loaded.ok) return;
461
-
462
- const result = await loaded.value.build({
463
- readByGuid: async () =>
464
- err(
465
- new AssetError({
466
- code: 'asset-not-found',
467
- expected: 'the staged dependency snapshot',
468
- hint: 'publish the dependency before rebuilding the ScriptablePack',
469
- detail: { sourcePath: 'staged-dependency' },
470
- }),
471
- ),
472
- } as never);
473
- expect(result).toMatchObject({
474
- ok: false,
475
- error: {
476
- code: 'asset-not-found',
477
- expected: 'the staged dependency snapshot',
478
- hint: 'publish the dependency before rebuilding the ScriptablePack',
479
- },
480
- });
481
- } finally {
482
- await rm(root, { recursive: true, force: true });
483
- }
484
- });
485
-
486
- it('executes identity mutations through one GUID-allocating authoring gateway', async () => {
487
- const mutations: ScriptablePackAuthoringMutation[] = [];
488
- const rebuilds: string[] = [];
489
- const sourceMeta = projectScriptablePackMeta(definition(), 'house.pack.ts');
490
- const port = {
491
- async preflight() {
492
- return ok({
493
- revision: 'preflight:house.pack.ts',
494
- meta: sourceMeta,
495
- capabilities: { inspect: true, rebuild: true, coldCook: true, addOutput: true },
496
- incomingRefs: [],
497
- } as const);
498
- },
499
- async mutate(operation: ScriptablePackAuthoringMutation) {
500
- mutations.push(operation);
501
- return ok({
502
- sourcePath:
503
- operation.kind === 'clone-scriptable-pack'
504
- ? operation.targetPath
505
- : operation.sourcePath,
506
- revision: `write-${mutations.length}`,
507
- });
508
- },
509
- async inspect(sourcePath: string) {
510
- return ok({ revision: `inspect:${sourcePath}`, meta: sourceMeta });
511
- },
512
- async rebuild(sourcePath: string, mode: 'rebuild' | 'cold-cook') {
513
- rebuilds.push(`${mode}:${sourcePath}`);
514
- return ok({ revision: `cook:${mode}`, meta: sourceMeta });
515
- },
516
- };
517
- let nextGuid = 10;
518
- const gateway = createScriptablePackAuthoringGateway(port, () => {
519
- const value = new Uint8Array(16);
520
- value[15] = nextGuid++;
521
- return value as ReturnType<typeof AssetGuid.random>;
522
- });
523
-
524
- expect(
525
- (
526
- await gateway.execute({
527
- requestId: 'request-create',
528
- kind: 'create-scriptable-pack',
529
- sourcePath: 'created.pack.ts',
530
- initialOutput: { sourceKey: 'mesh', kind: 'mesh' },
531
- })
532
- ).ok,
533
- ).toBe(true);
534
- await gateway.execute({
535
- requestId: 'request-add',
536
- kind: 'add-output',
537
- sourcePath: 'created.pack.ts',
538
- sourceKey: 'scene',
539
- assetKind: 'scene',
540
- });
541
- await gateway.execute({
542
- requestId: 'request-remove',
543
- kind: 'remove-output',
544
- sourcePath: 'created.pack.ts',
545
- sourceKey: 'mesh',
546
- });
547
- await gateway.execute({
548
- requestId: 'request-clone',
549
- kind: 'clone-scriptable-pack',
550
- sourcePath: 'house.pack.ts',
551
- targetPath: 'house-copy.pack.ts',
552
- });
553
- const inspected = await gateway.execute({
554
- requestId: 'request-inspect',
555
- kind: 'inspect-meta',
556
- sourcePath: 'house.pack.ts',
557
- });
558
- const rebuilt = await gateway.execute({
559
- requestId: 'request-rebuild',
560
- kind: 'rebuild',
561
- sourcePath: 'house.pack.ts',
562
- expectedRevision: 'preflight:house.pack.ts',
563
- });
564
-
565
- expect(mutations.map((operation) => operation.kind)).toEqual([
566
- 'create-scriptable-pack',
567
- 'add-output',
568
- 'remove-output',
569
- 'clone-scriptable-pack',
570
- ]);
571
- expect(mutations[0]).toMatchObject({ packageId: expect.any(Uint8Array) });
572
- expect(mutations[1]).toMatchObject({ guid: expect.any(Uint8Array) });
573
- expect(mutations[3]).toMatchObject({
574
- packageId: expect.any(Uint8Array),
575
- outputGuids: {
576
- 'geometry/wall': expect.any(Uint8Array),
577
- 'scene/house': expect.any(Uint8Array),
578
- },
579
- });
580
- expect(inspected.ok && inspected.value.revision).toBe('inspect:house.pack.ts');
581
- expect(rebuilt.ok && rebuilt.value.revision).toBe('cook:rebuild');
582
- expect(rebuilds).toEqual(['rebuild:house.pack.ts']);
583
-
584
- const staleRebuild = await gateway.execute({
585
- requestId: 'request-rebuild-stale',
586
- kind: 'rebuild',
587
- sourcePath: 'house.pack.ts',
588
- expectedRevision: 'stale-revision',
589
- });
590
- expect(staleRebuild).toMatchObject({
591
- ok: false,
592
- error: { code: 'pack-source-revision-conflict', actual: 'preflight:house.pack.ts' },
593
- });
594
- expect(rebuilds).toEqual(['rebuild:house.pack.ts']);
595
- });
596
-
597
- it('authors canonical sources with confinement, CAS, and reference-aware removal', async () => {
598
- const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-authoring-'));
599
- const incoming = new Map<string, readonly string[]>();
600
- try {
601
- const port = createFileSystemScriptablePackAuthoringPort({
602
- gameRoot: root,
603
- incomingRefs: async (_sourcePath, sourceKey) => incoming.get(sourceKey ?? '*') ?? [],
604
- });
605
- let nextGuid = 40;
606
- const gateway = createScriptablePackAuthoringGateway(port, () => {
607
- const value = new Uint8Array(16);
608
- value[15] = nextGuid++;
609
- return value as ReturnType<typeof AssetGuid.random>;
610
- });
611
-
612
- const created = await gateway.execute({
613
- requestId: 'create-canonical',
614
- kind: 'create-scriptable-pack',
615
- sourcePath: 'assets/generated.pack.ts',
616
- name: 'Generated',
617
- initialOutput: { sourceKey: 'scene/main', kind: 'scene', name: 'Main' },
618
- });
619
- expect(created.ok).toBe(true);
620
- if (!created.ok) return;
621
- expect(created.value.meta.subAssets).toMatchObject([
622
- { sourceKey: 'scene/main', kind: 'scene', name: 'Main' },
623
- ]);
624
- expect(await readFile(join(root, 'assets/generated.pack.ts'), 'utf8')).toContain(
625
- '@forgeax-scriptable-pack canonical-v1',
626
- );
627
-
628
- const preflight = await gateway.execute({
629
- requestId: 'preflight-canonical',
630
- kind: 'preflight',
631
- sourcePath: 'assets/generated.pack.ts',
632
- });
633
- expect(preflight).toMatchObject({
634
- ok: true,
635
- value: { capabilities: { addOutput: true, removeOutput: true } },
636
- });
637
- if (!preflight.ok) return;
638
-
639
- const added = await gateway.execute({
640
- requestId: 'add-canonical-output',
641
- expectedRevision: preflight.value.revision,
642
- kind: 'add-output',
643
- sourcePath: 'assets/generated.pack.ts',
644
- sourceKey: 'material/accent',
645
- assetKind: 'material',
646
- name: 'Accent',
647
- });
648
- expect(added).toMatchObject({
649
- ok: true,
650
- value: {
651
- meta: { subAssets: [{ sourceKey: 'material/accent' }, { sourceKey: 'scene/main' }] },
652
- },
653
- });
654
-
655
- const stale = await gateway.execute({
656
- requestId: 'stale-canonical-edit',
657
- expectedRevision: preflight.value.revision,
658
- kind: 'rename-display',
659
- sourcePath: 'assets/generated.pack.ts',
660
- target: { kind: 'package' },
661
- name: 'Stale',
662
- });
663
- expect(stale).toMatchObject({ ok: false, error: { code: 'pack-source-revision-conflict' } });
664
-
665
- incoming.set('material/accent', ['scene.pack.json#/entities/0/components/MeshRenderer']);
666
- const current = await port.inspect('assets/generated.pack.ts');
667
- expect(current.ok).toBe(true);
668
- if (!current.ok) return;
669
- const blocked = await gateway.execute({
670
- requestId: 'remove-referenced-output',
671
- expectedRevision: current.value.revision,
672
- kind: 'remove-output',
673
- sourcePath: 'assets/generated.pack.ts',
674
- sourceKey: 'material/accent',
675
- });
676
- expect(blocked).toMatchObject({
677
- ok: false,
678
- error: {
679
- code: 'pack-source-reference-conflict',
680
- detail: { incomingRefs: ['scene.pack.json#/entities/0/components/MeshRenderer'] },
681
- },
682
- });
683
-
684
- const removed = await gateway.execute({
685
- requestId: 'confirm-remove-referenced-output',
686
- expectedRevision: current.value.revision,
687
- kind: 'remove-output',
688
- sourcePath: 'assets/generated.pack.ts',
689
- sourceKey: 'material/accent',
690
- confirmIncomingRefs: ['scene.pack.json#/entities/0/components/MeshRenderer'],
691
- });
692
- expect(removed).toMatchObject({ ok: true });
693
-
694
- const escaped = await gateway.execute({
695
- requestId: 'escape-game-root',
696
- kind: 'inspect-meta',
697
- sourcePath: '../outside.pack.ts',
698
- });
699
- expect(escaped).toMatchObject({ ok: false, error: { code: 'pack-source-path-invalid' } });
700
- } finally {
701
- await rm(root, { recursive: true, force: true });
702
- }
703
- });
704
-
705
- it('projects operation descriptors and makes caller request ids idempotent', async () => {
706
- const sourceMeta = projectScriptablePackMeta(definition(), 'house.pack.ts');
707
- let writes = 0;
708
- const gateway = createScriptablePackAuthoringGateway({
709
- async preflight() {
710
- return ok({
711
- revision: 'revision-1',
712
- meta: sourceMeta,
713
- capabilities: { inspect: true, rebuild: true, coldCook: true, addOutput: true },
714
- incomingRefs: ['019ffa97-0000-7000-8000-000000000001'],
715
- } as const);
716
- },
717
- async mutate(operation) {
718
- writes += 1;
719
- expect(operation.expectedRevision).toBe('revision-1');
720
- return ok({ sourcePath: operation.sourcePath, revision: 'revision-2' });
721
- },
722
- async inspect() {
723
- return ok({ revision: 'revision-2', meta: sourceMeta });
724
- },
725
- async rebuild() {
726
- return ok({ revision: 'revision-2', meta: sourceMeta });
727
- },
728
- });
729
-
730
- expect(SOURCE_AUTHORING_OPERATION_DESCRIPTORS.map((descriptor) => descriptor.id)).toContain(
731
- 'asset-source.create',
732
- );
733
- const addOutputDescriptor = SOURCE_AUTHORING_OPERATION_DESCRIPTORS.find(
734
- (descriptor) => descriptor.id === 'asset-source.add-output',
735
- );
736
- expect(addOutputDescriptor).toMatchObject({
737
- kind: 'add-output',
738
- domain: 'session',
739
- argsSchema: {
740
- type: 'object',
741
- required: ['requestId', 'sourceKey', 'assetKind', 'expectedRevision'],
742
- additionalProperties: false,
743
- anyOf: [
744
- { required: ['requestId', 'sourceKey', 'assetKind', 'expectedRevision', 'sourcePath'] },
745
- { required: ['requestId', 'sourceKey', 'assetKind', 'expectedRevision', 'ownerGuid'] },
746
- { required: ['requestId', 'sourceKey', 'assetKind', 'expectedRevision', 'guid'] },
747
- ],
748
- properties: {
749
- sourcePath: { pattern: '^[^/].*\\.pack\\.ts$' },
750
- ownerGuid: { type: 'string', format: 'uuid' },
751
- requestId: { minLength: 1 },
752
- expectedRevision: { minLength: 64, maxLength: 64 },
753
- },
754
- },
755
- });
756
- const preflight = await gateway.execute({
757
- requestId: 'request-preflight',
758
- kind: 'preflight',
759
- sourcePath: 'house.pack.ts',
760
- });
761
- expect(preflight).toMatchObject({
762
- ok: true,
763
- value: { revision: 'revision-1', capabilities: { addOutput: true } },
764
- });
765
-
766
- const operation = {
767
- requestId: 'request-idempotent-add',
768
- expectedRevision: 'revision-1',
769
- kind: 'add-output' as const,
770
- sourcePath: 'house.pack.ts',
771
- sourceKey: 'mesh/new',
772
- assetKind: 'mesh' as const,
773
- };
774
- const first = await gateway.execute(operation);
775
- const replay = await gateway.execute(operation);
776
- expect(first).toEqual(replay);
777
- expect(writes).toBe(1);
778
-
779
- const conflict = await gateway.execute({
780
- ...operation,
781
- kind: 'remove-output',
782
- sourceKey: 'mesh/other',
783
- });
784
- expect(conflict).toMatchObject({
785
- ok: false,
786
- error: { code: 'pack-source-operation-committed', retryable: false },
787
- });
788
- });
789
- });