@forgeax/engine-pack 0.1.27 → 0.1.29

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 (52) hide show
  1. package/README.md +30 -17
  2. package/dist/build.mjs +35 -52
  3. package/dist/build.mjs.map +1 -1
  4. package/dist/cli-asset.d.ts +1 -1
  5. package/dist/cli-asset.d.ts.map +1 -1
  6. package/dist/cli-asset.mjs +40 -33
  7. package/dist/cli-asset.mjs.map +1 -1
  8. package/dist/index.mjs +1 -1
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/inventory/declaration.d.ts +1 -1
  11. package/dist/inventory/declaration.d.ts.map +1 -1
  12. package/dist/inventory/sync.d.ts.map +1 -1
  13. package/dist/native-cooker-registry.d.ts +1 -0
  14. package/dist/native-cooker-registry.d.ts.map +1 -1
  15. package/dist/native-cooker.mjs.map +1 -1
  16. package/dist/pack-authoring-node.mjs +22 -13
  17. package/dist/pack-authoring-node.mjs.map +1 -1
  18. package/dist/runtime.mjs +1 -1
  19. package/dist/runtime.mjs.map +1 -1
  20. package/dist/scanner.mjs +22 -13
  21. package/dist/scanner.mjs.map +1 -1
  22. package/dist/schema-compiled.d.ts.map +1 -1
  23. package/dist/schema.mjs +41 -63
  24. package/dist/schema.mjs.map +1 -1
  25. package/dist/scriptable-pack-node.d.ts.map +1 -1
  26. package/dist/scriptable-pack-node.mjs +11 -3
  27. package/dist/scriptable-pack-node.mjs.map +1 -1
  28. package/dist/scriptable-pack-worker.mjs +5 -1
  29. package/dist/scriptable-pack-worker.mjs.map +1 -1
  30. package/package.json +3 -6
  31. package/schema/meta.schema.json +1 -1
  32. package/src/__tests__/atlas-cli-region-mismatch.test.ts +1 -1
  33. package/src/__tests__/cli.unit.test.ts +1 -1
  34. package/src/__tests__/inventory-schema.test.ts +10 -10
  35. package/src/__tests__/pack-authoring.unit.test.ts +2 -4
  36. package/src/__tests__/pack.unit.test.ts +89 -127
  37. package/src/__tests__/scanner-instance-cycle.test.ts +92 -0
  38. package/src/__tests__/scanner-inventory.contract.test.ts +2 -2
  39. package/src/__tests__/scriptable-pack-diagnostic.integration.test.ts +45 -0
  40. package/src/atlas/run-atlas.ts +3 -3
  41. package/src/atlas/shelf-pack.ts +1 -1
  42. package/src/atlas/upng-ambient.d.ts +1 -1
  43. package/src/cli-asset.ts +22 -23
  44. package/src/inventory/binding.ts +3 -3
  45. package/src/inventory/declaration.ts +10 -48
  46. package/src/inventory/sync.ts +3 -2
  47. package/src/native-cooker-registry.ts +1 -0
  48. package/src/scanner.ts +29 -29
  49. package/src/schema-compiled.ts +40 -79
  50. package/src/scriptable-pack-node.ts +11 -3
  51. package/src/scriptable-pack-worker.ts +8 -1
  52. package/src/__tests__/scanner-mount-cycle.test.ts +0 -232
@@ -75,108 +75,69 @@ export function buildSceneAssetValidator(
75
75
  componentsProperties[name] = sub;
76
76
  }
77
77
 
78
- // MountOverride schema (feat-20260608-scene-nesting-ecs-fication M1 / w11;
79
- // feat-20260713-mount-override-component-add-and-shared-ref-round M1 / w3;
80
- // requirements §S-10 / AC-03): validates each entry in mounts[].overrides[].
81
- // `field` is optional — it is a component-granular add-or-patch discriminant
82
- // (present -> patch one field with `value`; absent -> add/upsert the whole
83
- // `comp` with `value` as its per-field map). `localId` / `comp` / `value`
84
- // stay required; `additionalProperties: false` rejects any `op`-tag
85
- // discriminant so downstream consumers never branch on it. Field-level type
86
- // validation (matching the schema vocab) is enforced at runtime via
87
- // setSceneOverride (D-9 / EcsErrorCode 'scene-override-type-mismatch').
88
- const mountOverrideSchema = {
78
+ const componentsSchema = {
89
79
  type: 'object',
90
80
  additionalProperties: false,
91
- required: ['localId', 'comp', 'value'],
92
- properties: {
93
- localId: { type: 'integer', minimum: 0 },
94
- comp: { type: 'string', minLength: 1 },
95
- field: { type: 'string', minLength: 1 },
96
- value: {},
97
- },
81
+ properties: componentsProperties,
98
82
  };
99
-
100
- // SceneInstanceMount schema (feat-20260608-scene-nesting-ecs-fication
101
- // M1 / w11; requirements §S-10): validates each mounts[] entry. parent /
102
- // components / overrides are optional; cross-mount window collision and
103
- // mount.source / mount.memberCount agreement with referenced child
104
- // SceneAsset are checked at the build-time scanner (D-1, w14), not here.
105
- const mountSchema = {
83
+ const instanceSchema = {
106
84
  type: 'object',
107
85
  additionalProperties: false,
108
- required: ['localId', 'source', 'memberFirst', 'memberCount'],
86
+ required: ['source'],
109
87
  properties: {
110
- localId: { type: 'integer', minimum: 0 },
111
- source: { type: 'integer', minimum: 0 },
112
- memberFirst: { type: 'integer', minimum: 0 },
113
- memberCount: { type: 'integer', minimum: 0 },
114
- parent: { type: 'integer', minimum: 0 },
115
- components: {
116
- type: 'object',
117
- additionalProperties: false,
118
- properties: componentsProperties,
88
+ // Authored scenes use a GUID string. Pack output may carry a refs[]
89
+ // index, which is resolved by the Scene decoder before instantiation.
90
+ source: {
91
+ oneOf: [
92
+ { type: 'string', minLength: 1 },
93
+ { type: 'integer', minimum: 0 },
94
+ ],
119
95
  },
120
96
  overrides: {
121
97
  type: 'array',
122
- items: mountOverrideSchema,
123
- },
124
- publicationFence: {
125
- type: 'object',
126
- additionalProperties: false,
127
- required: [
128
- 'schemaVersion',
129
- 'sourcePath',
130
- 'sourceRevision',
131
- 'publicationGeneration',
132
- 'outputDigest',
133
- 'outputSetDigest',
134
- 'receiptIdentity',
135
- ],
136
- properties: {
137
- schemaVersion: { type: 'string', const: 'scene-publication-fence/1' },
138
- sourcePath: { type: 'string', minLength: 1 },
139
- sourceRevision: { type: 'string', minLength: 1 },
140
- publicationGeneration: { type: 'integer', minimum: 1 },
141
- outputDigest: { type: 'string', minLength: 1 },
142
- outputSetDigest: { type: 'string', minLength: 1 },
143
- receiptIdentity: { type: 'string', minLength: 1 },
98
+ items: {
99
+ type: 'object',
100
+ additionalProperties: false,
101
+ required: ['target', 'components'],
102
+ properties: {
103
+ target: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
104
+ components: componentsSchema,
105
+ },
144
106
  },
145
107
  },
146
108
  },
147
109
  };
148
-
110
+ const entitySchema = {
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ required: ['components'],
114
+ properties: {
115
+ components: componentsSchema,
116
+ instance: instanceSchema,
117
+ },
118
+ };
149
119
  const sceneSchema = {
150
120
  type: 'object',
151
121
  additionalProperties: false,
152
122
  required: ['kind', 'entities'],
153
123
  properties: {
154
124
  kind: { type: 'string', const: 'scene' },
155
- sourceKey: { type: 'string', minLength: 1 },
156
- entities: {
125
+ // Skin assets are explicit scene dependencies so async catalog loading
126
+ // can finish the joint-wiring path before the Scene is instantiated.
127
+ // Authored producers use GUID strings; Pack output may use refs[] indices.
128
+ skinGuids: {
157
129
  type: 'array',
158
130
  items: {
159
- type: 'object',
160
- additionalProperties: false,
161
- required: ['localId', 'components'],
162
- properties: {
163
- localId: { type: 'integer', minimum: 0 },
164
- bindingKey: { type: 'string', minLength: 1 },
165
- components: {
166
- type: 'object',
167
- additionalProperties: false,
168
- properties: componentsProperties,
169
- },
170
- },
131
+ oneOf: [
132
+ { type: 'string', minLength: 1 },
133
+ { type: 'integer', minimum: 0 },
134
+ ],
171
135
  },
172
136
  },
173
- // Optional top-level mounts[] (feat-20260608-scene-nesting-ecs-fication
174
- // M1 / w11). Missing mounts is semantically equivalent to mounts: []
175
- // (plan-strategy §6.3 back-compat); ajv default produces the empty
176
- // array on absent input.
177
- mounts: {
178
- type: 'array',
179
- items: mountSchema,
137
+ entities: {
138
+ type: 'object',
139
+ propertyNames: { type: 'string', minLength: 1 },
140
+ additionalProperties: entitySchema,
180
141
  },
181
142
  },
182
143
  };
@@ -266,9 +266,17 @@ class WorkerScriptablePackExecutor
266
266
  }
267
267
  };
268
268
  worker.on('message', onMessage);
269
- worker.once('error', rejectLoad);
269
+ const fail = (error: Error): void => {
270
+ rejectLoad(error);
271
+ const active = this.build;
272
+ this.build = undefined;
273
+ active?.reject(error);
274
+ void this.dispose('failure');
275
+ };
276
+ worker.once('error', fail);
270
277
  worker.once('exit', (code) => {
271
- if (code !== 0) rejectLoad(new Error(`ScriptablePack worker exited with code ${code}`));
278
+ if (this.disposal !== undefined) return;
279
+ fail(new Error(`ScriptablePack worker exited with code ${code}`));
272
280
  });
273
281
  });
274
282
  }
@@ -393,7 +401,7 @@ class ReusableWorkerScriptablePackExecutor
393
401
  void this.dispose('failure');
394
402
  });
395
403
  this.worker.on('exit', (code) => {
396
- if (code === 0 || this.disposal !== undefined) return;
404
+ if (this.disposal !== undefined) return;
397
405
  this.failed = true;
398
406
  this.rejectActive(new Error(`ScriptablePack worker exited with code ${code}`));
399
407
  void this.dispose('failure');
@@ -215,7 +215,14 @@ function compileModuleClosure(entryPath: string, outputRoot: string, loadId?: nu
215
215
  if (errors.length > 0) {
216
216
  throw new Error(
217
217
  `ScriptablePack TypeScript transpile failed for ${path}: ${errors
218
- .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
218
+ .map((diagnostic) => {
219
+ const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
220
+ const location =
221
+ position === undefined
222
+ ? path
223
+ : `${path}:${position.line + 1}:${position.character + 1}`;
224
+ return `${location}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`;
225
+ })
219
226
  .join('; ')}`,
220
227
  );
221
228
  }
@@ -1,232 +0,0 @@
1
- // scanner-mount-cycle.test - scanner step-6 mount-asset cycle coverage
2
- // (feat-20260608-scene-nesting-ecs-fication M1 / w13).
3
- //
4
- // Coverage (AC-12; plan-strategy D-1):
5
- // (a) acyclic mount chain A -> B -> C: scanner returns Ok (no cycle).
6
- // (b) two-asset cycle A -> B -> A: scanner returns
7
- // Err(PackError{ code: 'pack-cyclic-reference', detail.kind: 'mount-asset',
8
- // detail.cycle: GUID[] }) with the GUIDs in cycle order (first === last).
9
- // (c) three-asset cycle A -> B -> C -> A: same, longer cycle.
10
- // (d) self-mount A -> A: same, single-element cycle.
11
- //
12
- // The fixtures are hand-authored scene .pack.json files in a tmpdir;
13
- // each scene asset uses `payload.mounts[].source` as an integer index into
14
- // the parent scene's `refs[]` (D-1 + research Finding 11). The mount window
15
- // (memberFirst / memberCount) is set conservatively so the scanner step-6
16
- // cycle pass triggers before any mount-window collision check.
17
- //
18
- // TDD red signal: until w14 wires `payload.mounts[].source` -> `refsByGuid`
19
- // edge into scanner step-6, the scanner does NOT see the mount edge in the
20
- // DFS and the cycle assertions miss. With w14 the cycle producer surfaces
21
- // `pack-cyclic-reference + detail.kind: 'mount-asset'` (w8 landed the
22
- // detail.kind tagging at the producer).
23
-
24
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
25
- import { tmpdir } from 'node:os';
26
- import { join } from 'node:path';
27
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
28
- import { scan } from '../scanner.js';
29
-
30
- const GUID_A = 'aa000000-0000-4000-8000-000000000aaa';
31
- const GUID_B = 'bb000000-0000-4000-8000-000000000bbb';
32
- const GUID_C = 'cc000000-0000-4000-8000-000000000ccc';
33
-
34
- interface MountSpec {
35
- readonly localId: number;
36
- readonly source: number;
37
- readonly memberFirst: number;
38
- readonly memberCount: number;
39
- }
40
-
41
- interface SceneAssetSpec {
42
- readonly guid: string;
43
- /** GUIDs the scene asset's `payload.mounts[].source` integers index into. */
44
- readonly refs: readonly string[];
45
- readonly mounts: readonly MountSpec[];
46
- }
47
-
48
- function sceneAssetPack(spec: SceneAssetSpec): unknown {
49
- return {
50
- schemaVersion: '1.0.0',
51
- kind: 'internal-text-package',
52
- assets: [
53
- {
54
- guid: spec.guid,
55
- kind: 'scene',
56
- refs: [...spec.refs],
57
- payload: {
58
- kind: 'scene',
59
- entities: [{ localId: 0, components: {} }],
60
- mounts: spec.mounts.map((m) => ({
61
- localId: m.localId,
62
- source: m.source,
63
- memberFirst: m.memberFirst,
64
- memberCount: m.memberCount,
65
- })),
66
- },
67
- },
68
- ],
69
- };
70
- }
71
-
72
- interface CycleErrorShape {
73
- readonly code: string;
74
- readonly detail: {
75
- readonly code?: string;
76
- readonly kind?: string;
77
- readonly cycle?: readonly string[];
78
- };
79
- }
80
-
81
- describe('scanner step-6 mount-asset cycle detection (AC-12; D-1)', () => {
82
- let dir: string;
83
-
84
- beforeEach(async () => {
85
- dir = await mkdtemp(join(tmpdir(), 'pack-mount-cycle-'));
86
- });
87
-
88
- afterEach(async () => {
89
- await rm(dir, { recursive: true, force: true });
90
- });
91
-
92
- it('(a) accepts an acyclic mount chain A -> B -> C', async () => {
93
- // A mounts B (source=0 in A.refs) -> B mounts C (source=0 in B.refs) ->
94
- // C mounts nothing.
95
- await writeFile(
96
- join(dir, 'a.pack.json'),
97
- JSON.stringify(
98
- sceneAssetPack({
99
- guid: GUID_A,
100
- refs: [GUID_B],
101
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
102
- }),
103
- ),
104
- 'utf-8',
105
- );
106
- await writeFile(
107
- join(dir, 'b.pack.json'),
108
- JSON.stringify(
109
- sceneAssetPack({
110
- guid: GUID_B,
111
- refs: [GUID_C],
112
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
113
- }),
114
- ),
115
- 'utf-8',
116
- );
117
- await writeFile(
118
- join(dir, 'c.pack.json'),
119
- JSON.stringify(sceneAssetPack({ guid: GUID_C, refs: [], mounts: [] })),
120
- 'utf-8',
121
- );
122
-
123
- const result = await scan([dir]);
124
- expect(result.ok).toBe(true);
125
- });
126
-
127
- it('(b) rejects a two-asset cycle A -> B -> A with kind: mount-asset', async () => {
128
- await writeFile(
129
- join(dir, 'a.pack.json'),
130
- JSON.stringify(
131
- sceneAssetPack({
132
- guid: GUID_A,
133
- refs: [GUID_B],
134
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
135
- }),
136
- ),
137
- 'utf-8',
138
- );
139
- await writeFile(
140
- join(dir, 'b.pack.json'),
141
- JSON.stringify(
142
- sceneAssetPack({
143
- guid: GUID_B,
144
- refs: [GUID_A],
145
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
146
- }),
147
- ),
148
- 'utf-8',
149
- );
150
-
151
- const result = await scan([dir]);
152
- expect(result.ok).toBe(false);
153
- if (result.ok) return;
154
- const err = result.error as unknown as CycleErrorShape;
155
- expect(err.code).toBe('pack-cyclic-reference');
156
- expect(err.detail.kind).toBe('mount-asset');
157
- const cycle = err.detail.cycle ?? [];
158
- expect(cycle.length).toBeGreaterThanOrEqual(2);
159
- expect(cycle[0]).toBe(cycle[cycle.length - 1]);
160
- // Both GUIDs participate in the cycle (case-insensitive compare; scanner
161
- // lowercases before storing).
162
- const lower = cycle.map((g) => g.toLowerCase());
163
- expect(lower).toContain(GUID_A.toLowerCase());
164
- expect(lower).toContain(GUID_B.toLowerCase());
165
- });
166
-
167
- it('(c) rejects a three-asset cycle A -> B -> C -> A with kind: mount-asset', async () => {
168
- await writeFile(
169
- join(dir, 'a.pack.json'),
170
- JSON.stringify(
171
- sceneAssetPack({
172
- guid: GUID_A,
173
- refs: [GUID_B],
174
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
175
- }),
176
- ),
177
- 'utf-8',
178
- );
179
- await writeFile(
180
- join(dir, 'b.pack.json'),
181
- JSON.stringify(
182
- sceneAssetPack({
183
- guid: GUID_B,
184
- refs: [GUID_C],
185
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
186
- }),
187
- ),
188
- 'utf-8',
189
- );
190
- await writeFile(
191
- join(dir, 'c.pack.json'),
192
- JSON.stringify(
193
- sceneAssetPack({
194
- guid: GUID_C,
195
- refs: [GUID_A],
196
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
197
- }),
198
- ),
199
- 'utf-8',
200
- );
201
-
202
- const result = await scan([dir]);
203
- expect(result.ok).toBe(false);
204
- if (result.ok) return;
205
- const err = result.error as unknown as CycleErrorShape;
206
- expect(err.code).toBe('pack-cyclic-reference');
207
- expect(err.detail.kind).toBe('mount-asset');
208
- const cycle = err.detail.cycle ?? [];
209
- expect(cycle.length).toBeGreaterThanOrEqual(3);
210
- expect(cycle[0]).toBe(cycle[cycle.length - 1]);
211
- });
212
-
213
- it('(d) rejects a self-mount A -> A', async () => {
214
- await writeFile(
215
- join(dir, 'a.pack.json'),
216
- JSON.stringify(
217
- sceneAssetPack({
218
- guid: GUID_A,
219
- refs: [GUID_A],
220
- mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
221
- }),
222
- ),
223
- 'utf-8',
224
- );
225
- const result = await scan([dir]);
226
- expect(result.ok).toBe(false);
227
- if (result.ok) return;
228
- const err = result.error as unknown as CycleErrorShape;
229
- expect(err.code).toBe('pack-cyclic-reference');
230
- expect(err.detail.kind).toBe('mount-asset');
231
- });
232
- });