@forgeax/engine-pack 0.1.21 → 0.1.24

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 (139) hide show
  1. package/README.md +167 -48
  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__/material-cook-receipt-layer-plan.unit.test.d.ts +2 -0
  5. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +2 -0
  7. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/pack-authoring.unit.test.d.ts +2 -0
  9. package/dist/__tests__/pack-authoring.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/pack-scanner.unit.test.d.ts +2 -0
  11. package/dist/__tests__/pack-scanner.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
  13. package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
  14. package/dist/build.d.ts +9 -4
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.mjs +3449 -897
  17. package/dist/build.mjs.map +1 -1
  18. package/dist/builtin.mjs +37 -4
  19. package/dist/builtin.mjs.map +1 -1
  20. package/dist/catalog-builder.d.ts.map +1 -1
  21. package/dist/cli-asset.d.ts.map +1 -1
  22. package/dist/cli-asset.mjs +1361 -845
  23. package/dist/cli-asset.mjs.map +1 -1
  24. package/dist/deriveAssetName.d.ts +11 -7
  25. package/dist/deriveAssetName.d.ts.map +1 -1
  26. package/dist/evidence/material-cook.d.ts +11 -0
  27. package/dist/evidence/material-cook.d.ts.map +1 -1
  28. package/dist/evidence/source-inventory.d.ts.map +1 -1
  29. package/dist/guid.d.ts +15 -0
  30. package/dist/guid.d.ts.map +1 -1
  31. package/dist/guid.mjs +53 -5
  32. package/dist/guid.mjs.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.mjs +1297 -562
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/inventory/binding.d.ts +6 -0
  38. package/dist/inventory/binding.d.ts.map +1 -0
  39. package/dist/inventory/declaration.d.ts +24 -0
  40. package/dist/inventory/declaration.d.ts.map +1 -0
  41. package/dist/inventory/index.d.ts +3 -0
  42. package/dist/inventory/index.d.ts.map +1 -0
  43. package/dist/inventory/sync.d.ts +5 -0
  44. package/dist/inventory/sync.d.ts.map +1 -0
  45. package/dist/material-cook.d.ts +1 -1
  46. package/dist/material-cook.d.ts.map +1 -1
  47. package/dist/material-cook.mjs +50 -3
  48. package/dist/material-cook.mjs.map +1 -1
  49. package/dist/name.mjs +4 -7
  50. package/dist/name.mjs.map +1 -1
  51. package/dist/pack-authoring-node.d.ts +26 -0
  52. package/dist/pack-authoring-node.d.ts.map +1 -0
  53. package/dist/pack-authoring-node.mjs +11089 -0
  54. package/dist/pack-authoring-node.mjs.map +1 -0
  55. package/dist/pack-authoring.d.ts +230 -0
  56. package/dist/pack-authoring.d.ts.map +1 -0
  57. package/dist/pack-authoring.mjs +1137 -0
  58. package/dist/pack-authoring.mjs.map +1 -0
  59. package/dist/package-finalizer.d.ts +2 -20
  60. package/dist/package-finalizer.d.ts.map +1 -1
  61. package/dist/resolve-asset-source.d.ts +1 -3
  62. package/dist/resolve-asset-source.d.ts.map +1 -1
  63. package/dist/resolve-asset-source.mjs +6 -81
  64. package/dist/resolve-asset-source.mjs.map +1 -1
  65. package/dist/runtime.mjs +77 -7
  66. package/dist/runtime.mjs.map +1 -1
  67. package/dist/scanner.d.ts +18 -7
  68. package/dist/scanner.d.ts.map +1 -1
  69. package/dist/scanner.mjs +1164 -687
  70. package/dist/scanner.mjs.map +1 -1
  71. package/dist/schema-compiled.d.ts.map +1 -1
  72. package/dist/schema.mjs +79 -7
  73. package/dist/schema.mjs.map +1 -1
  74. package/dist/scriptable-pack-node.d.ts +7 -31
  75. package/dist/scriptable-pack-node.d.ts.map +1 -1
  76. package/dist/scriptable-pack-node.mjs +572 -1019
  77. package/dist/scriptable-pack-node.mjs.map +1 -1
  78. package/dist/scriptable-pack-worker.mjs +29 -11
  79. package/dist/scriptable-pack-worker.mjs.map +1 -1
  80. package/dist/scriptable-pack.d.ts +20 -532
  81. package/dist/scriptable-pack.d.ts.map +1 -1
  82. package/dist/scriptable-pack.mjs +1038 -527
  83. package/dist/scriptable-pack.mjs.map +1 -1
  84. package/package.json +2 -7
  85. package/schema/meta.schema.json +1 -1
  86. package/schema/pack.schema.json +76 -6
  87. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  88. package/src/__tests__/inventory-schema.test.ts +109 -0
  89. package/src/__tests__/material-cook-receipt-layer-plan.unit.test.ts +16 -0
  90. package/src/__tests__/material-cook-schema.unit.test.ts +62 -0
  91. package/src/__tests__/pack-authoring-gateway.unit.test.ts +249 -0
  92. package/src/__tests__/pack-authoring.unit.test.ts +266 -0
  93. package/src/__tests__/pack-scanner.unit.test.ts +123 -0
  94. package/src/__tests__/pack.unit.test.ts +1 -238
  95. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  96. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  97. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  98. package/src/__tests__/scanner-inventory.contract.test.ts +11 -14
  99. package/src/__tests__/scanner-inventory.test.ts +23 -10
  100. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  101. package/src/__tests__/topology.unit.test.ts +23 -0
  102. package/src/build.ts +28 -18
  103. package/src/catalog-builder.ts +60 -33
  104. package/src/cli-asset.ts +68 -83
  105. package/src/deriveAssetName.ts +14 -13
  106. package/src/evidence/material-cook.ts +79 -3
  107. package/src/evidence/source-inventory.ts +34 -21
  108. package/src/guid.ts +65 -4
  109. package/src/index.ts +15 -11
  110. package/src/inventory/binding.ts +25 -0
  111. package/src/inventory/declaration.ts +168 -0
  112. package/src/inventory/index.ts +18 -0
  113. package/src/inventory/sync.ts +22 -0
  114. package/src/material-cook.ts +1 -0
  115. package/src/pack-authoring-node.ts +1966 -0
  116. package/src/pack-authoring.ts +1478 -0
  117. package/src/package-finalizer.ts +2 -42
  118. package/src/resolve-asset-source.ts +4 -76
  119. package/src/scanner.ts +236 -56
  120. package/src/schema/material-cook.schema.json +4 -1
  121. package/src/schema-compiled.ts +2 -0
  122. package/src/scriptable-pack-node.ts +108 -719
  123. package/src/scriptable-pack-worker.ts +48 -16
  124. package/src/scriptable-pack.ts +28 -939
  125. package/dist/.tsbuildinfo +0 -1
  126. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  127. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  128. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  129. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  130. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  131. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  132. package/dist/config.d.ts +0 -6
  133. package/dist/config.d.ts.map +0 -1
  134. package/dist/config.mjs +0 -29
  135. package/dist/config.mjs.map +0 -1
  136. package/src/__tests__/load-asset-config.test.ts +0 -121
  137. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  138. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  139. package/src/config.ts +0 -36
@@ -0,0 +1,266 @@
1
+ import { ok } from '@forgeax/engine-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { AssetGuid, PackageId } from '../guid.js';
4
+ import {
5
+ definePack,
6
+ definePackageId,
7
+ type PackParameterInheritanceSubject,
8
+ parsePackSourceJson,
9
+ projectDirectPackJson,
10
+ resolvePackParameterInheritance,
11
+ resolvePackParameterValues,
12
+ validatePackDefinition,
13
+ } from '../pack-authoring.js';
14
+
15
+ const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
16
+
17
+ function packageId(value: string) {
18
+ const result = PackageId.parse(value);
19
+ if (!result.ok) throw result.error;
20
+ return result.value;
21
+ }
22
+
23
+ describe('ScriptablePack and Pack authoring', () => {
24
+ it('derives the RFC 4122 UUIDv5 vector and keeps PackageId branded separately', () => {
25
+ const namespace = packageId(NAMESPACE);
26
+ expect(AssetGuid.format(AssetGuid.derive(namespace, 'www.widgets.com'))).toBe(
27
+ '21f7f8de-8051-5b89-8680-0195ef798b6a',
28
+ );
29
+ expect(PackageId.format(namespace)).toBe(NAMESPACE);
30
+ expect(AssetGuid.derive(namespace, 'mesh/a')).not.toBe(namespace);
31
+ });
32
+
33
+ it('derives an explicit context shape for zero-parameter and parameter-bearing ScriptablePacks', async () => {
34
+ const zero = definePack({
35
+ schemaVersion: '2.0.0',
36
+ packageId: definePackageId('01900000-0000-7000-8000-000000000001'),
37
+ build: ({ packageId: subjectId }) =>
38
+ ok({
39
+ 'scene/main': {
40
+ kind: 'scene',
41
+ entities: [],
42
+ mounts: [],
43
+ source: AssetGuid.format(AssetGuid.derive(subjectId, 'mesh/main')),
44
+ },
45
+ }),
46
+ });
47
+ expect('parameters' in zero).toBe(false);
48
+ const zeroResult = await zero.build({
49
+ packageId: zero.packageId,
50
+ readByGuid: async () => ok(null as never),
51
+ });
52
+ expect(zeroResult.ok).toBe(true);
53
+
54
+ const withParameters = definePack({
55
+ schemaVersion: '2.0.0',
56
+ packageId: definePackageId('01900000-0000-7000-8000-000000000002'),
57
+ parameters: [
58
+ { name: 'segments', type: 'u32', default: 12, minimum: 3, maximum: 128 },
59
+ { name: 'style', type: 'enum', values: ['oak', 'pine'], default: 'oak' },
60
+ ] as const,
61
+ build: ({ values }) =>
62
+ ok({
63
+ 'scene/main': {
64
+ kind: 'scene',
65
+ entities: values.segments >= 0 ? [] : [],
66
+ mounts: [],
67
+ },
68
+ }),
69
+ });
70
+ const values = resolvePackParameterValues(withParameters, { segments: 24 });
71
+ expect(values).toMatchObject({ ok: true, value: { segments: 24, style: 'oak' } });
72
+ expect(resolvePackParameterValues(withParameters, { unknown: true })).toMatchObject({
73
+ ok: false,
74
+ error: { code: 'pack-parameter-invalid' },
75
+ });
76
+
77
+ const bounded = definePack({
78
+ schemaVersion: '2.0.0',
79
+ packageId: definePackageId('01900000-0000-7000-8000-000000000003'),
80
+ parameters: [
81
+ { name: 'unsigned', type: 'u32', default: 0 },
82
+ { name: 'signed', type: 'i32', default: 0 },
83
+ { name: 'float', type: 'f32', default: 0 },
84
+ ] as const,
85
+ build: () => ok({}),
86
+ });
87
+ expect(resolvePackParameterValues(bounded, { unsigned: 0x1_0000_0000 })).toMatchObject({
88
+ ok: false,
89
+ error: { code: 'pack-parameter-invalid' },
90
+ });
91
+ expect(resolvePackParameterValues(bounded, { signed: 0x8000_0000 })).toMatchObject({
92
+ ok: false,
93
+ error: { code: 'pack-parameter-invalid' },
94
+ });
95
+ expect(resolvePackParameterValues(bounded, { float: Number.MAX_VALUE })).toMatchObject({
96
+ ok: false,
97
+ error: { code: 'pack-parameter-invalid' },
98
+ });
99
+ });
100
+
101
+ it('validates and preserves optional scene component schemas without exposing output identity', () => {
102
+ const definition = definePack({
103
+ schemaVersion: '2.0.0',
104
+ packageId: definePackageId('01900000-0000-7000-8000-000000000004'),
105
+ sceneComponents: [
106
+ { name: 'Transform', fields: { pos: { type: 'vec3' }, quat: 'quat' } },
107
+ { name: 'Name', fields: { value: 'string' } },
108
+ ],
109
+ build: () => ok({}),
110
+ });
111
+ expect(definition.sceneComponents).toEqual([
112
+ { name: 'Transform', fields: { pos: { type: 'vec3' }, quat: 'quat' } },
113
+ { name: 'Name', fields: { value: 'string' } },
114
+ ]);
115
+ expect(Object.isFrozen(definition.sceneComponents)).toBe(true);
116
+ expect(Object.isFrozen(definition.sceneComponents?.[0])).toBe(true);
117
+ expect(Object.isFrozen(definition.sceneComponents?.[0]?.fields)).toBe(true);
118
+
119
+ const duplicate = validatePackDefinition({
120
+ schemaVersion: '2.0.0',
121
+ packageId: definePackageId('01900000-0000-7000-8000-000000000005'),
122
+ sceneComponents: [
123
+ { name: 'Transform', fields: {} },
124
+ { name: 'Transform', fields: {} },
125
+ ],
126
+ build: () => ok({}),
127
+ });
128
+ expect(duplicate).toMatchObject({
129
+ ok: false,
130
+ error: {
131
+ code: 'pack-parameter-invalid',
132
+ detail: { reason: 'scene component names must be unique' },
133
+ },
134
+ });
135
+ const malformed = validatePackDefinition({
136
+ schemaVersion: '2.0.0',
137
+ packageId: definePackageId('01900000-0000-7000-8000-000000000006'),
138
+ sceneComponents: [{ name: 'Transform', fields: { pos: { type: '' } } }],
139
+ build: () => ok({}),
140
+ });
141
+ expect(malformed).toMatchObject({
142
+ ok: false,
143
+ error: {
144
+ code: 'pack-parameter-invalid',
145
+ detail: { reason: 'scene component field schema is malformed' },
146
+ },
147
+ });
148
+ });
149
+
150
+ it('parses direct and instance JSON without accepting GUID fields', () => {
151
+ const direct = parsePackSourceJson({
152
+ schemaVersion: '3.0.0',
153
+ packageId: '01900000-0000-7000-8000-000000000010',
154
+ assets: {
155
+ 'scene/main': { kind: 'scene', payload: {}, refs: [] },
156
+ },
157
+ });
158
+ expect(direct.ok).toBe(true);
159
+ if (direct.ok && direct.value.format === 'direct') {
160
+ const projected = projectDirectPackJson(direct.value);
161
+ expect(projected).toMatchObject({
162
+ ok: true,
163
+ value: {
164
+ packageId: '01900000-0000-7000-8000-000000000010',
165
+ assets: [{ sourceKey: 'scene/main', guid: expect.any(String) }],
166
+ },
167
+ });
168
+ }
169
+ expect(
170
+ parsePackSourceJson({
171
+ schemaVersion: '3.0.0',
172
+ packageId: '01900000-0000-7000-8000-000000000010',
173
+ assets: { 'scene/main': { guid: NAMESPACE, kind: 'scene', payload: {}, refs: [] } },
174
+ }),
175
+ ).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
176
+ expect(
177
+ parsePackSourceJson({
178
+ schemaVersion: '3.0.0',
179
+ packageId: '01900000-0000-7000-8000-000000000020',
180
+ parent: '01900000-0000-7000-8000-000000000002',
181
+ values: { segments: 24 },
182
+ }),
183
+ ).toMatchObject({ ok: true, value: { format: 'instance' } });
184
+ });
185
+
186
+ it('rejects legacy static output fields in the v2 source definition', () => {
187
+ expect(() =>
188
+ definePack({
189
+ schemaVersion: '2.0.0',
190
+ packageId: definePackageId('01900000-0000-7000-8000-000000000021'),
191
+ assets: [],
192
+ build: () => ok({}),
193
+ } as never),
194
+ ).toThrow(/repair the parameter declaration|authoring/);
195
+ expect(() =>
196
+ definePack({
197
+ schemaVersion: '2.0.0',
198
+ packageId: definePackageId('01900000-0000-7000-8000-000000000022'),
199
+ externalAssets: [],
200
+ build: () => ok({}),
201
+ } as never),
202
+ ).toThrow();
203
+ });
204
+
205
+ it('rejects unknown parameter descriptor fields instead of carrying a second schema', () => {
206
+ expect(() =>
207
+ definePack({
208
+ schemaVersion: '2.0.0',
209
+ packageId: definePackageId('01900000-0000-7000-8000-000000000023'),
210
+ parameters: [{ name: 'count', type: 'u32', default: 1, editor: 'slider' }] as never,
211
+ build: () => ok({}),
212
+ }),
213
+ ).toThrow();
214
+ });
215
+
216
+ it('resolves finite shallow inheritance and reports missing parent and cycle', async () => {
217
+ const rootId = packageId('01900000-0000-7000-8000-000000000030');
218
+ const middleId = packageId('01900000-0000-7000-8000-000000000031');
219
+ const leafId = packageId('01900000-0000-7000-8000-000000000032');
220
+ const root: PackParameterInheritanceSubject = {
221
+ format: 'source',
222
+ packageId: rootId,
223
+ parameters: [{ name: 'segments', type: 'u32', default: 12, minimum: 3 }],
224
+ };
225
+ const middle: PackParameterInheritanceSubject = {
226
+ format: 'instance',
227
+ packageId: middleId,
228
+ parent: rootId,
229
+ values: { segments: 24 },
230
+ };
231
+ const leaf: PackParameterInheritanceSubject = {
232
+ format: 'instance',
233
+ packageId: leafId,
234
+ parent: middleId,
235
+ values: {},
236
+ };
237
+ const subjects = new Map<string, PackParameterInheritanceSubject>([
238
+ [PackageId.format(rootId), root],
239
+ [PackageId.format(middleId), middle],
240
+ ]);
241
+ const resolved = await resolvePackParameterInheritance(leaf, (id) =>
242
+ subjects.get(PackageId.format(id)),
243
+ );
244
+ expect(resolved).toMatchObject({
245
+ ok: true,
246
+ value: {
247
+ values: { segments: 24 },
248
+ parentChain: [PackageId.format(middleId), PackageId.format(rootId)],
249
+ },
250
+ });
251
+
252
+ const missing = await resolvePackParameterInheritance(leaf, () => undefined);
253
+ expect(missing).toMatchObject({ ok: false, error: { code: 'pack-parent-not-found' } });
254
+
255
+ const cycleA: PackParameterInheritanceSubject = {
256
+ format: 'instance',
257
+ packageId: middleId,
258
+ parent: leafId,
259
+ values: {},
260
+ };
261
+ const cycle = await resolvePackParameterInheritance(cycleA, (id) =>
262
+ PackageId.format(id) === PackageId.format(leafId) ? leaf : cycleA,
263
+ );
264
+ expect(cycle).toMatchObject({ ok: false, error: { code: 'pack-parent-cycle' } });
265
+ });
266
+ });
@@ -0,0 +1,123 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { scanEntries } from '../cli-asset.js';
6
+ import { scanInventory } from '../scanner.js';
7
+
8
+ const SOURCE_PACKAGE = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
9
+ const INSTANCE_PACKAGE = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
10
+ const DIRECT_PACKAGE = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
11
+
12
+ const roots: string[] = [];
13
+
14
+ afterEach(async () => {
15
+ const root = roots.pop();
16
+ if (root !== undefined) await rm(root, { recursive: true, force: true });
17
+ });
18
+
19
+ describe('ScriptablePack and Pack source scanner', () => {
20
+ it('indexes direct v3 output keys as derived GUIDs without requiring entry GUIDs', async () => {
21
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-scan-'));
22
+ roots.push(root);
23
+ const path = join(root, 'direct.pack.json');
24
+ await writeFile(
25
+ path,
26
+ JSON.stringify({
27
+ schemaVersion: '3.0.0',
28
+ packageId: DIRECT_PACKAGE,
29
+ assets: {
30
+ 'scene/main': {
31
+ kind: 'scene',
32
+ payload: { kind: 'scene', nodes: [] },
33
+ refs: [],
34
+ },
35
+ },
36
+ }),
37
+ );
38
+
39
+ const result = await scanInventory([root]);
40
+
41
+ expect(result.ok).toBe(true);
42
+ if (!result.ok) return;
43
+ expect(result.value.inventory).toMatchObject([
44
+ {
45
+ kind: 'scene',
46
+ sourcePath: path,
47
+ sourceKey: 'scene/main',
48
+ sourceIndex: 0,
49
+ },
50
+ ]);
51
+ expect(result.value.inventory[0]?.guid).toMatch(
52
+ /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
53
+ );
54
+
55
+ const stdout: string[] = [];
56
+ const stderr: string[] = [];
57
+ const listed = await scanEntries([root], {
58
+ stdoutWrite: (line) => stdout.push(line),
59
+ stderrWrite: (line) => stderr.push(line),
60
+ });
61
+ expect(listed).toMatchObject({
62
+ ok: true,
63
+ value: [{ guid: result.value.inventory[0]?.guid, name: 'scene/main' }],
64
+ });
65
+ expect(stderr).toEqual([]);
66
+ });
67
+
68
+ it('captures a ScriptablePack source as metadata and resolves an instance parent', async () => {
69
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-source-scan-'));
70
+ roots.push(root);
71
+ await writeFile(
72
+ join(root, 'source.pack.ts'),
73
+ [
74
+ 'const packageId = new Uint8Array(16);',
75
+ 'packageId.set([170, 170, 170, 170, 170, 170, 74, 170, 138, 170, 170, 170, 170, 170, 170, 170]);',
76
+ 'export default {',
77
+ " schemaVersion: '2.0.0',",
78
+ ' packageId,',
79
+ " parameters: [{ name: 'count', type: 'u32', default: 1 }],",
80
+ " build: ({ values }) => ({ ok: true, value: { ['mesh/' + values.count]: { kind: 'mesh', materialSlots: [] } } }),",
81
+ '};',
82
+ ].join('\n'),
83
+ );
84
+ await writeFile(
85
+ join(root, 'instance.pack.json'),
86
+ JSON.stringify({
87
+ schemaVersion: '3.0.0',
88
+ packageId: INSTANCE_PACKAGE,
89
+ parent: SOURCE_PACKAGE,
90
+ values: { count: 2 },
91
+ }),
92
+ );
93
+
94
+ const result = await scanInventory([root]);
95
+
96
+ expect(result.ok).toBe(true);
97
+ if (!result.ok) return;
98
+ const declaration = result.value.declarations.get(join(root, 'source.pack.ts'));
99
+ expect(declaration?.format).toBe('pack.ts');
100
+ expect(result.value.inventory).toEqual([]);
101
+ });
102
+
103
+ it('fails closed when an instance parent is missing', async () => {
104
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-parent-scan-'));
105
+ roots.push(root);
106
+ await writeFile(
107
+ join(root, 'missing.pack.json'),
108
+ JSON.stringify({
109
+ schemaVersion: '3.0.0',
110
+ packageId: INSTANCE_PACKAGE,
111
+ parent: SOURCE_PACKAGE,
112
+ values: { count: 2 },
113
+ }),
114
+ );
115
+
116
+ const result = await scanInventory([root]);
117
+
118
+ expect(result.ok).toBe(false);
119
+ if (result.ok) return;
120
+ expect(result.error.code).toBe('pack-malformed-pack');
121
+ expect(result.error.detail).toMatchObject({ reason: 'pack-parent-not-found' });
122
+ });
123
+ });
@@ -1970,7 +1970,7 @@ const V1_WHITELIST = new Set([
1970
1970
  expect(result.ok).toBe(true);
1971
1971
  });
1972
1972
 
1973
- it('AC-17: explicit source=hero.png + companion file → scan passes (backward compat)', async () => {
1973
+ it('AC-17: explicit source=hero.png + companion file → scan passes', async () => {
1974
1974
  await writeFile(join(tmpDir, 'hero.png.meta.json'), makeMeta({ source: 'hero.png' }));
1975
1975
  await writeFile(join(tmpDir, 'hero.png'), Buffer.from('fake png'));
1976
1976
 
@@ -1978,241 +1978,4 @@ const V1_WHITELIST = new Set([
1978
1978
  expect(result.ok).toBe(true);
1979
1979
  });
1980
1980
  });
1981
-
1982
- describe('w15-scanner-path-integration.test.ts — AC-5+AC-10', () => {
1983
- let tmpDir: string;
1984
-
1985
- beforeEach(async () => {
1986
- tmpDir = await mkdtemp(join(tmpdir(), 'forgeax-ac5-'));
1987
- });
1988
- afterEach(async () => {
1989
- await rm(tmpDir, { recursive: true, force: true });
1990
- });
1991
-
1992
- it('AC-5: @name/ path resolves correctly when file exists', async () => {
1993
- const libDir = join(tmpDir, 'lib');
1994
- await mkdir(libDir);
1995
- await writeFile(join(libDir, 'shared.png'), Buffer.from('fake png'));
1996
- const metaContent = JSON.stringify({
1997
- schemaVersion: '1.0.0',
1998
- kind: 'external-asset-package',
1999
- importer: 'image',
2000
- source: '@shared/shared.png',
2001
- importSettings: {},
2002
- subAssets: [
2003
- { guid: 'aaaaaaaa-bbbb-4000-8000-000000000001', sourceIndex: 0, kind: 'texture' },
2004
- ],
2005
- });
2006
- await writeFile(join(tmpDir, 'ref.meta.json'), metaContent);
2007
-
2008
- // The scan runs inside its own process.cwd() — paths table resolution uses cwd
2009
- // The tmpDir is not the cwd, so @shared/ will report pack-unknown-path.
2010
- // AC-5: we need the path table to be configured at the scan's cwd.
2011
- // For a unit test, we verify instead via resolveAssetSource directly:
2012
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2013
- const paths = { shared: libDir };
2014
- const result = resolveAS(join(tmpDir, 'ref.meta.json'), '@shared/shared.png', paths);
2015
- expect(result.ok).toBe(true);
2016
- if (result.ok) {
2017
- expect(result.value).toBe(join(libDir, 'shared.png'));
2018
- }
2019
- });
2020
-
2021
- it('AC-10: path configured but file missing → pack-orphan-meta', async () => {
2022
- const libDir = join(tmpDir, 'lib');
2023
- await mkdir(libDir);
2024
- // Do NOT create missing.png in libDir
2025
- const metaContent = JSON.stringify({
2026
- schemaVersion: '1.0.0',
2027
- kind: 'external-asset-package',
2028
- importer: 'image',
2029
- source: '@shared/missing.png',
2030
- importSettings: {},
2031
- subAssets: [
2032
- { guid: 'aaaaaaaa-bbbb-4000-8000-000000000001', sourceIndex: 0, kind: 'texture' },
2033
- ],
2034
- });
2035
- await writeFile(join(tmpDir, 'ref.meta.json'), metaContent);
2036
-
2037
- // resolveAssetSource resolves the path but does not stat — the resolved path will be returned ok
2038
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2039
- const paths = { shared: libDir };
2040
- const result = resolveAS(join(tmpDir, 'ref.meta.json'), '@shared/missing.png', paths);
2041
- expect(result.ok).toBe(true);
2042
- if (result.ok) {
2043
- expect(result.value).toBe(join(libDir, 'missing.png'));
2044
- // The actual orphan check (stat) is done by the scanner caller
2045
- }
2046
- });
2047
-
2048
- it('AC-10: scanner reports pack-orphan-meta when resolved path file does not exist', async () => {
2049
- const metaContent = makeMeta({ source: 'nowhere.png' });
2050
- await writeFile(join(tmpDir, 'orphan.meta.json'), metaContent);
2051
- // Do NOT create nowhere.png
2052
-
2053
- const result = await scan([tmpDir]);
2054
- expect(result.ok).toBe(false);
2055
- if (!result.ok) {
2056
- expect(result.error.code).toBe('pack-orphan-meta');
2057
- if (result.error.code === 'pack-orphan-meta') {
2058
- expect(result.error.detail.expectedFile).toContain('nowhere.png');
2059
- }
2060
- }
2061
- });
2062
- });
2063
-
2064
- describe('scanner @name/ end-to-end via cwd package.json — AC-5/AC-8', () => {
2065
- let tmpDir: string;
2066
- let originalCwd: string;
2067
-
2068
- beforeEach(async () => {
2069
- originalCwd = process.cwd();
2070
- tmpDir = await mkdtemp(join(tmpdir(), 'forgeax-ac5e2e-'));
2071
- process.chdir(tmpDir);
2072
- });
2073
- afterEach(async () => {
2074
- process.chdir(originalCwd);
2075
- await rm(tmpDir, { recursive: true, force: true });
2076
- });
2077
-
2078
- it('AC-5: scan() passes orphan check for @name/ meta when cwd package.json path table resolves to an existing file', async () => {
2079
- const sharedDir = join(tmpDir, 'shared-lib', 'assets');
2080
- await mkdir(sharedDir, { recursive: true });
2081
- await writeFile(join(sharedDir, 'cross.png'), Buffer.from('fake png'));
2082
- await writeFile(
2083
- join(tmpDir, 'package.json'),
2084
- JSON.stringify({
2085
- name: 'ac5-e2e-tmp',
2086
- forgeax: { assets: { paths: { shared: 'shared-lib/assets' } } },
2087
- }),
2088
- );
2089
- const scanRoot = join(tmpDir, 'metas');
2090
- await mkdir(scanRoot, { recursive: true });
2091
- await writeFile(
2092
- join(scanRoot, 'cross.png.meta.json'),
2093
- JSON.stringify({
2094
- schemaVersion: '1.0.0',
2095
- kind: 'external-asset-package',
2096
- importer: 'image',
2097
- source: '@shared/cross.png',
2098
- importSettings: {},
2099
- subAssets: [
2100
- { guid: 'aaaaaaaa-bbbb-4000-8000-000000000001', sourceIndex: 0, kind: 'texture' },
2101
- ],
2102
- }),
2103
- );
2104
-
2105
- const result = await scan([scanRoot]);
2106
- expect(result.ok).toBe(true);
2107
- });
2108
-
2109
- it('AC-8: scan() reports pack-unknown-path when @name/ references an undeclared path name', async () => {
2110
- await writeFile(
2111
- join(tmpDir, 'package.json'),
2112
- JSON.stringify({ name: 'ac8-e2e-tmp', forgeax: { assets: { paths: {} } } }),
2113
- );
2114
- const scanRoot = join(tmpDir, 'metas');
2115
- await mkdir(scanRoot, { recursive: true });
2116
- await writeFile(
2117
- join(scanRoot, 'x.png.meta.json'),
2118
- JSON.stringify({
2119
- schemaVersion: '1.0.0',
2120
- kind: 'external-asset-package',
2121
- importer: 'image',
2122
- source: '@nope/x.png',
2123
- importSettings: {},
2124
- subAssets: [
2125
- { guid: 'aaaaaaaa-bbbb-4000-8000-000000000001', sourceIndex: 0, kind: 'texture' },
2126
- ],
2127
- }),
2128
- );
2129
-
2130
- const result = await scan([scanRoot]);
2131
- expect(result.ok).toBe(false);
2132
- if (!result.ok) {
2133
- expect(result.error.code).toBe('pack-unknown-path');
2134
- }
2135
- });
2136
- });
2137
- }
2138
-
2139
- {
2140
- // ─── AC-1 regression w15 — resolveAssetSource explicit source backwards compat ───
2141
-
2142
- describe('w15-AC-1-regression.test.ts', () => {
2143
- it('AC-1: explicit source=foo.png with resolveAssetSource == resolve(metaDir, foo.png)', async () => {
2144
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2145
- const { resolve } = await import('node:path');
2146
- const metaPath = '/home/user/assets/foo.png.meta.json';
2147
- const result = resolveAS(metaPath, 'foo.png', {});
2148
- expect(result.ok).toBe(true);
2149
- if (result.ok) {
2150
- expect(result.value).toBe(resolve('/home/user/assets', 'foo.png'));
2151
- }
2152
- });
2153
-
2154
- it('AC-1: explicit source=sub/deep.png with resolveAssetSource == resolve(metaDir, sub/deep.png)', async () => {
2155
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2156
- const { resolve } = await import('node:path');
2157
- const metaPath = '/home/user/assets/main.meta.json';
2158
- const result = resolveAS(metaPath, 'sub/deep.png', {});
2159
- expect(result.ok).toBe(true);
2160
- if (result.ok) {
2161
- expect(result.value).toBe(resolve('/home/user/assets', 'sub/deep.png'));
2162
- }
2163
- });
2164
-
2165
- it('AC-1: omitted source derives from meta filename', async () => {
2166
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2167
- const { resolve } = await import('node:path');
2168
- const metaPath = '/home/user/assets/foo.png.meta.json';
2169
- const result = resolveAS(metaPath, undefined, {});
2170
- expect(result.ok).toBe(true);
2171
- if (result.ok) {
2172
- expect(result.value).toBe(resolve('/home/user/assets', 'foo.png'));
2173
- }
2174
- });
2175
- });
2176
-
2177
- // ─── AC-5/AC-10 source path table scanner integration (w15) ───
2178
-
2179
- describe('w15-AC-5-AC-10-path-table.test.ts', () => {
2180
- it('AC-5: @shared/ resource resolves correctly when path table configured', async () => {
2181
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2182
- const { resolve } = await import('node:path');
2183
- const metaPath = '/project/assets/ref.meta.json';
2184
- const paths = { shared: '/project/lib' };
2185
- const result = resolveAS(metaPath, '@shared/tex.png', paths);
2186
- expect(result.ok).toBe(true);
2187
- if (result.ok) {
2188
- expect(result.value).toBe(resolve('/project/lib', 'tex.png'));
2189
- }
2190
- });
2191
-
2192
- it('AC-5: @shared/ with nested rest path', async () => {
2193
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2194
- const { resolve } = await import('node:path');
2195
- const metaPath = '/project/assets/ref.meta.json';
2196
- const paths = { shared: '/project/lib' };
2197
- const result = resolveAS(metaPath, '@shared/sub/dir/tex.png', paths);
2198
- expect(result.ok).toBe(true);
2199
- if (result.ok) {
2200
- expect(result.value).toBe(resolve('/project/lib', 'sub/dir/tex.png'));
2201
- }
2202
- });
2203
-
2204
- it('AC-10: @unknown/ path name → pack-unknown-path error', async () => {
2205
- const { resolveAssetSource: resolveAS } = await import('../resolve-asset-source.js');
2206
- const metaPath = '/project/assets/ref.meta.json';
2207
- const result = resolveAS(metaPath, '@nope/tex.png', {});
2208
- expect(result.ok).toBe(false);
2209
- if (!result.ok) {
2210
- expect(result.error.code).toBe('pack-unknown-path');
2211
- if (result.error.code === 'pack-unknown-path') {
2212
- expect(result.error.detail.pathName).toBe('nope');
2213
- expect(result.error.detail.knownNames).toEqual([]);
2214
- }
2215
- }
2216
- });
2217
- });
2218
1981
  }