@forgeax/engine-pack 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 (95) hide show
  1. package/README.md +52 -4
  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__/package-finalizer.contract.test.d.ts +2 -0
  6. package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
  7. package/dist/__tests__/runtime.browser.test.d.ts +2 -0
  8. package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
  9. package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
  10. package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
  11. package/dist/artifact-path.mjs +53 -0
  12. package/dist/artifact-path.mjs.map +1 -0
  13. package/dist/atlas/shelf-pack.d.ts.map +1 -1
  14. package/dist/build.d.ts +19 -0
  15. package/dist/build.d.ts.map +1 -0
  16. package/dist/build.mjs +10168 -0
  17. package/dist/build.mjs.map +1 -0
  18. package/dist/builtin.d.ts +20 -0
  19. package/dist/builtin.d.ts.map +1 -1
  20. package/dist/builtin.mjs +141 -0
  21. package/dist/builtin.mjs.map +1 -0
  22. package/dist/catalog-builder.d.ts +43 -0
  23. package/dist/catalog-builder.d.ts.map +1 -0
  24. package/dist/catalog-delta.d.ts +11 -0
  25. package/dist/catalog-delta.d.ts.map +1 -0
  26. package/dist/catalog-projection.d.ts +91 -0
  27. package/dist/catalog-projection.d.ts.map +1 -0
  28. package/dist/cli-asset.mjs +231 -484
  29. package/dist/cli-asset.mjs.map +1 -1
  30. package/dist/index.mjs +83 -1
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/material-cook.d.ts +2 -0
  33. package/dist/material-cook.d.ts.map +1 -0
  34. package/dist/material-cook.mjs +193 -0
  35. package/dist/material-cook.mjs.map +1 -0
  36. package/dist/mesh-bin-contract.mjs +96 -0
  37. package/dist/mesh-bin-contract.mjs.map +1 -0
  38. package/dist/native-cooker-registry.d.ts +19 -0
  39. package/dist/native-cooker-registry.d.ts.map +1 -1
  40. package/dist/native-cooker.mjs +37 -0
  41. package/dist/native-cooker.mjs.map +1 -1
  42. package/dist/package-finalizer.d.ts +121 -0
  43. package/dist/package-finalizer.d.ts.map +1 -0
  44. package/dist/resolve-asset-source.d.ts.map +1 -1
  45. package/dist/resolve-asset-source.mjs +16 -6
  46. package/dist/resolve-asset-source.mjs.map +1 -1
  47. package/dist/runtime-publication.d.ts +44 -0
  48. package/dist/runtime-publication.d.ts.map +1 -0
  49. package/dist/runtime.d.ts +8 -0
  50. package/dist/runtime.d.ts.map +1 -0
  51. package/dist/runtime.mjs +7261 -0
  52. package/dist/runtime.mjs.map +1 -0
  53. package/dist/scanner.d.ts +87 -54
  54. package/dist/scanner.d.ts.map +1 -1
  55. package/dist/scanner.mjs +258 -454
  56. package/dist/scanner.mjs.map +1 -1
  57. package/dist/scriptable-pack-node.d.ts +3 -8
  58. package/dist/scriptable-pack-node.d.ts.map +1 -1
  59. package/dist/scriptable-pack-node.mjs +93 -9
  60. package/dist/scriptable-pack-node.mjs.map +1 -1
  61. package/dist/scriptable-pack-worker.mjs +1 -0
  62. package/dist/scriptable-pack-worker.mjs.map +1 -1
  63. package/dist/scriptable-pack.d.ts +33 -0
  64. package/dist/scriptable-pack.d.ts.map +1 -1
  65. package/dist/scriptable-pack.mjs +91 -2
  66. package/dist/scriptable-pack.mjs.map +1 -1
  67. package/dist/topology.d.ts +1 -1
  68. package/dist/topology.d.ts.map +1 -1
  69. package/package.json +27 -2
  70. package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +2 -1
  71. package/src/__tests__/owner-chain.integration.test.ts +78 -0
  72. package/src/__tests__/package-finalizer.contract.test.ts +105 -0
  73. package/src/__tests__/runtime.browser.test.ts +15 -0
  74. package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
  75. package/src/__tests__/scanner-inventory.test.ts +16 -12
  76. package/src/__tests__/scriptable-pack.unit.test.ts +42 -0
  77. package/src/atlas/shelf-pack.ts +20 -24
  78. package/src/build.ts +139 -0
  79. package/src/builtin.ts +38 -0
  80. package/src/catalog-builder.ts +313 -0
  81. package/src/catalog-delta.ts +189 -0
  82. package/src/catalog-projection.ts +344 -0
  83. package/src/cli-asset.ts +1 -5
  84. package/src/index.ts +2 -2
  85. package/src/material-cook.ts +13 -0
  86. package/src/native-cooker-registry.ts +72 -0
  87. package/src/package-finalizer.ts +394 -0
  88. package/src/resolve-asset-source.ts +2 -7
  89. package/src/runtime-publication.ts +190 -0
  90. package/src/runtime.ts +23 -0
  91. package/src/scanner.ts +289 -290
  92. package/src/scriptable-pack-node.ts +27 -23
  93. package/src/scriptable-pack-worker.ts +2 -0
  94. package/src/scriptable-pack.ts +123 -1
  95. package/src/topology.ts +2 -1
package/src/scanner.ts CHANGED
@@ -1,6 +1,13 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readdir, readFile, stat } from 'node:fs/promises';
2
3
  import { basename, join } from 'node:path';
3
- import type { ImportedOutputDeclaration, PackErrorCode } from '@forgeax/engine-types';
4
+ import type {
5
+ CatalogDiagnostic,
6
+ PackErrorCode,
7
+ ProviderProvenance,
8
+ ResourceRevision,
9
+ SourceOverrideDescriptor,
10
+ } from '@forgeax/engine-types';
4
11
  import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
5
12
  import { loadAssetConfig } from './config.js';
6
13
  import { PackError } from './errors.js';
@@ -8,13 +15,16 @@ import { isValidAssetGuidString } from './guid.js';
8
15
  import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
9
16
  import { resolveAssetSource } from './resolve-asset-source.js';
10
17
  import { validateMeta, validatePack } from './schema-compiled.js';
11
- import { projectScriptablePackMeta, type ScriptablePackMetaJson } from './scriptable-pack.js';
12
18
  import {
13
- createScriptablePackModuleExecutorPool,
19
+ projectScriptablePackMeta,
20
+ type ScriptablePackDefinition,
21
+ type ScriptablePackMetaJson,
22
+ type ScriptablePackSourceClosureEntry,
23
+ } from './scriptable-pack.js';
24
+ import {
14
25
  inventoryScriptablePackSource,
15
- type LoadScriptablePackOptions,
16
26
  loadScriptablePack,
17
- type ScriptablePackSourceClosureEntry,
27
+ type ScriptablePackModuleExecutor,
18
28
  } from './scriptable-pack-node.js';
19
29
 
20
30
  // Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
@@ -34,58 +44,117 @@ function packErr<E>(error: E): ScanResult<never, E> {
34
44
  /** Host-owned source paths that should not enter the Pack catalog. */
35
45
  export interface ScanOptions {
36
46
  readonly ignorePath?: (path: string) => boolean;
37
- /** Timeout/executor owner for ScriptablePack metadata loading. */
38
- readonly scriptablePackLoad?: LoadScriptablePackOptions;
47
+ readonly scriptablePack?: ScriptablePackScanOptions;
48
+ }
49
+
50
+ /** One bounded executor policy shared by ScriptablePack inventory and production. */
51
+ export interface ScriptablePackScanOptions {
52
+ readonly timeoutMs?: number;
53
+ readonly buildTimeoutMs?: number;
54
+ readonly executor?: ScriptablePackModuleExecutor;
55
+ /** Path-only scans must release the isolated loader before returning. */
56
+ readonly metadataOnly?: boolean;
39
57
  }
40
58
 
41
- export type AssetInventoryProducerKind = 'direct-pack' | 'source-meta' | 'scriptable-pack';
59
+ /** Stable default policy shared by inventory and production owners. */
60
+ export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze(
61
+ {},
62
+ ) satisfies ScriptablePackScanOptions;
42
63
 
43
- export interface AssetInventoryOutput {
64
+ export interface InventoryDeclaration {
44
65
  readonly guid: string;
66
+ readonly kind: string;
67
+ readonly sourcePath: string;
68
+ readonly sourceRevision: string;
45
69
  readonly sourceKey?: string;
46
70
  readonly sourceIndex?: number;
47
- readonly kind: string;
48
- readonly name?: string;
49
- readonly execution?: 'direct' | 'cooked';
50
- readonly refs: readonly string[];
51
71
  }
52
72
 
53
- export interface AssetInventoryProjectedMeta {
54
- readonly schemaVersion?: string;
73
+ export interface ScanInventory {
74
+ readonly paths: readonly string[];
75
+ readonly inventory: readonly InventoryDeclaration[];
76
+ /** Complete parsed source declarations captured by the validated scan pass. */
77
+ readonly declarations: ReadonlyMap<string, ScanSourceDeclaration>;
78
+ }
79
+
80
+ export interface ScriptablePackInventoryDeclaration {
81
+ readonly sourcePath: string;
82
+ readonly sourceRevision: string;
83
+ readonly meta: ScriptablePackMetaJson;
84
+ readonly definition: Readonly<ScriptablePackDefinition>;
85
+ readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
86
+ }
87
+
88
+ export interface MetaInventoryDocument {
89
+ readonly schemaVersion: string | number;
90
+ readonly kind: 'external-asset-package';
55
91
  readonly packageId?: string;
56
- readonly importer?: string;
57
- readonly source?: string;
58
92
  readonly name?: string;
59
- readonly outputs: readonly AssetInventoryOutput[];
60
- readonly externalGuids: readonly string[];
93
+ readonly provenance?: ProviderProvenance;
94
+ readonly revision?: ResourceRevision;
95
+ readonly diagnostics?: readonly CatalogDiagnostic[];
96
+ readonly importer: string;
97
+ readonly source?: string;
98
+ readonly importSettings: Readonly<Record<string, unknown>>;
99
+ readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
100
+ readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];
101
+ readonly paramSchema?: readonly Readonly<Record<string, unknown>>[];
102
+ readonly subAssets: readonly MetaInventorySubAsset[];
61
103
  }
62
104
 
63
- export interface AssetInventoryDiagnostic {
64
- readonly code: string;
65
- readonly message: string;
105
+ export interface MetaInventorySubAsset {
106
+ readonly guid: string;
107
+ readonly sourceIndex: number;
108
+ readonly sourceKey?: string;
109
+ readonly name?: string;
110
+ readonly kind: string;
66
111
  }
67
112
 
68
- export interface AssetInventoryDeclaration {
69
- readonly sourcePath: string;
70
- readonly canonicalSourcePath: string;
71
- readonly producer: AssetInventoryProducerKind;
113
+ export interface PackInventoryDocument {
114
+ readonly schemaVersion: '1.0.0' | '2.0.0';
115
+ readonly kind: 'internal-text-package';
72
116
  readonly packageId?: string;
73
- readonly projectedMeta: AssetInventoryProjectedMeta;
74
- readonly outputs: readonly AssetInventoryOutput[];
75
- readonly externalGuids: readonly string[];
76
- readonly sourceClosure?: readonly ScriptablePackSourceClosureEntry[];
77
- readonly diagnostics: readonly AssetInventoryDiagnostic[];
117
+ readonly provenance?: ProviderProvenance;
118
+ readonly revision?: ResourceRevision;
119
+ readonly diagnostics?: readonly CatalogDiagnostic[];
120
+ readonly assets: readonly PackInventoryAsset[];
78
121
  }
79
122
 
80
- export interface AssetInventory {
81
- readonly paths: readonly string[];
82
- readonly scriptablePackMeta: ReadonlyMap<string, ScriptablePackMetaJson>;
83
- /** One declaration record per authored package/source, keyed by source path. */
84
- readonly declarations: readonly AssetInventoryDeclaration[];
85
- readonly declarationsByPath: ReadonlyMap<string, AssetInventoryDeclaration>;
86
- /** Producer ownership map used by Catalog, watcher, and demand cook. */
87
- readonly sourceByGuid: ReadonlyMap<string, string>;
88
- readonly metaByGuid: ReadonlyMap<string, string>;
123
+ export interface PackInventoryAsset {
124
+ readonly guid: string;
125
+ readonly kind: string;
126
+ readonly name?: string;
127
+ readonly execution?: 'direct' | 'cooked';
128
+ readonly sourceKey?: string;
129
+ readonly sourceIndex?: number;
130
+ readonly payload: Readonly<Record<string, unknown>>;
131
+ readonly refs: readonly string[];
132
+ readonly artifacts?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
133
+ }
134
+
135
+ export type ScanSourceDeclaration =
136
+ | {
137
+ readonly format: 'meta.json';
138
+ readonly sourcePath: string;
139
+ readonly sourceRevision: string;
140
+ readonly value: MetaInventoryDocument;
141
+ }
142
+ | {
143
+ readonly format: 'pack.json';
144
+ readonly sourcePath: string;
145
+ readonly sourceRevision: string;
146
+ readonly sourceText: string;
147
+ readonly value: PackInventoryDocument;
148
+ }
149
+ | ({
150
+ readonly format: 'pack.ts';
151
+ readonly sourcePath: string;
152
+ readonly sourceRevision: string;
153
+ readonly value: ScriptablePackMetaJson;
154
+ } & ScriptablePackInventoryDeclaration);
155
+
156
+ interface ScanCapture {
157
+ readonly declarations: Map<string, ScanSourceDeclaration>;
89
158
  }
90
159
 
91
160
  /**
@@ -111,6 +180,45 @@ const BLACKLIST = new Set([
111
180
 
112
181
  export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
113
182
 
183
+ type MalformedFileCode = Extract<PackErrorCode, 'pack-malformed-pack' | 'pack-malformed-meta'>;
184
+
185
+ type JsonValidator = {
186
+ (value: unknown): boolean;
187
+ errors?: readonly { readonly instancePath?: string; readonly message?: string }[] | null;
188
+ };
189
+
190
+ async function readValidatedJson(
191
+ path: string,
192
+ code: MalformedFileCode,
193
+ validate: JsonValidator,
194
+ ): Promise<ScanResult<{ readonly raw: string; readonly parsed: unknown }, PackError>> {
195
+ let raw: string;
196
+ let parsed: unknown;
197
+ try {
198
+ raw = await readFile(path, 'utf-8');
199
+ parsed = JSON.parse(raw);
200
+ } catch {
201
+ return packErr(
202
+ makePackError(code, {
203
+ path,
204
+ ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
205
+ }),
206
+ );
207
+ }
208
+ if (!validate(parsed)) {
209
+ return packErr(
210
+ makePackError(code, {
211
+ path,
212
+ ajvErrors: (validate.errors ?? []).map((error) => ({
213
+ instancePath: error.instancePath ?? '',
214
+ message: error.message ?? 'unknown ajv error',
215
+ })),
216
+ }),
217
+ );
218
+ }
219
+ return ok({ raw, parsed });
220
+ }
221
+
114
222
  function makePackError(
115
223
  code: PackErrorCode,
116
224
  detail: ConstructorParameters<typeof PackError>[0]['detail'],
@@ -153,7 +261,7 @@ function* extractMountSourceGuids(asset: {
153
261
  /**
154
262
  * Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
155
263
  * Runs a 7-step fail-fast validation chain (w17 + M7-T01):
156
- * Step 1 - collect all .meta.json + .pack.json + .pack.ts paths (blacklist skipped)
264
+ * Step 1 - collect all .meta.json + .pack.json paths (blacklist skipped)
157
265
  * Step 2 - schema validation (ajv strict)
158
266
  * Step 3 - GUID string format validation
159
267
  * Step 4 - GUID collision detection
@@ -161,17 +269,18 @@ function* extractMountSourceGuids(asset: {
161
269
  * Step 6 - cyclic reference detection (hand-written DFS)
162
270
  * Step 7 - complete pack and source closure validation
163
271
  *
164
- * Returns one reusable `AssetInventory` or `Err(PackError)` on the first violation.
272
+ * Returns `Ok(paths)` or `Err(PackError)` on the first violation.
165
273
  *
166
274
  * NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
167
275
  */
168
- export async function scanInventory(
276
+ async function scanValidated(
169
277
  roots: readonly string[],
170
278
  opts: ScanOptions = {},
171
- ): Promise<ScanResult<AssetInventory, PackError>> {
172
- // Step 1: collect all authored package declarations. ScriptablePack metadata
173
- // is projected once through the trusted loader; the resulting inventory is
174
- // shared by CLI, Catalog, watcher, and producer adapters.
279
+ capture?: ScanCapture,
280
+ ): Promise<ScanResult<string[], PackError>> {
281
+ // Step 1: collect all authored package declarations. ScriptablePack runtime
282
+ // validation belongs to its trusted module loader; scanner only inventories
283
+ // the source path so CLI/Vite share one discovery set.
175
284
  const rawPaths: string[] = [];
176
285
  const explicitRootSet = new Set(roots);
177
286
 
@@ -240,36 +349,11 @@ export async function scanInventory(
240
349
  // kind stays in the path/detail evidence; identity is the normalized GUID.
241
350
  const guidToPath = new Map<string, string>();
242
351
  const packRefs = new Map<string, string[]>(); // guid -> refs[]
243
- const scriptablePackMeta = new Map<string, ScriptablePackMetaJson>();
244
- const metaByGuid = new Map<string, string>();
245
- const declarations: AssetInventoryDeclaration[] = [];
246
352
 
247
353
  for (const packPath of packPaths) {
248
- let parsed: unknown;
249
- try {
250
- const raw = await readFile(packPath, 'utf-8');
251
- parsed = JSON.parse(raw);
252
- } catch {
253
- return packErr(
254
- makePackError('pack-malformed-pack', {
255
- path: packPath,
256
- ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
257
- }),
258
- );
259
- }
260
-
261
- const valid = validatePack(parsed);
262
- if (!valid) {
263
- return packErr(
264
- makePackError('pack-malformed-pack', {
265
- path: packPath,
266
- ajvErrors: (validatePack.errors ?? []).map((e) => ({
267
- instancePath: e.instancePath,
268
- message: e.message ?? 'unknown ajv error',
269
- })),
270
- }),
271
- );
272
- }
354
+ const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
355
+ if (!loaded.ok) return loaded;
356
+ const { raw, parsed } = loaded.value;
273
357
 
274
358
  const packageContract = validateProducerContract(parsed);
275
359
  if (!packageContract.ok) {
@@ -282,20 +366,7 @@ export async function scanInventory(
282
366
  }
283
367
 
284
368
  // Step 3: validate GUIDs in pack
285
- const packObj = parsed as {
286
- packageId?: string;
287
- assets: {
288
- guid: string;
289
- kind: string;
290
- execution?: 'direct' | 'cooked';
291
- name?: string;
292
- payload: unknown;
293
- refs: string[];
294
- artifacts?: Readonly<Record<string, unknown>>;
295
- sourceKey?: string;
296
- sourceIndex?: number;
297
- }[];
298
- };
369
+ const packObj = parsed as unknown as PackInventoryDocument;
299
370
  for (const asset of packObj.assets) {
300
371
  if (
301
372
  asset.kind === 'particle-effect' &&
@@ -332,7 +403,12 @@ export async function scanInventory(
332
403
  }
333
404
  }
334
405
  const topologyContract = validateProducerOutputs(
335
- producerAssets as unknown as readonly ImportedOutputDeclaration[],
406
+ producerAssets.map((asset, sourceIndex) => ({
407
+ guid: asset.guid,
408
+ kind: asset.kind,
409
+ sourceIndex: asset.sourceIndex ?? sourceIndex,
410
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
411
+ })),
336
412
  );
337
413
  if (!topologyContract.ok) {
338
414
  return packErr(
@@ -376,12 +452,6 @@ export async function scanInventory(
376
452
  }
377
453
  guidToPath.set(normalizedGuid, packPath);
378
454
 
379
- // Accumulate refs for cycle detection
380
- const existingRefs = packRefs.get(normalizedGuid) ?? [];
381
- for (const ref of asset.refs) {
382
- existingRefs.push(ref.toLowerCase());
383
- }
384
-
385
455
  // feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
386
456
  // mount-payload-extract — for scene assets, redundantly inject the
387
457
  // mount.source -> resolved GUID edge into the cycle graph alongside
@@ -393,65 +463,26 @@ export async function scanInventory(
393
463
  // refs[]. The `kind: 'mount-asset'` tag on the resulting
394
464
  // pack-cyclic-reference detail is set by the cycle producer below
395
465
  // (R10).
396
- for (const guid of extractMountSourceGuids(asset)) {
397
- existingRefs.push(guid);
398
- }
399
- packRefs.set(normalizedGuid, existingRefs);
466
+ packRefs.set(normalizedGuid, [
467
+ ...asset.refs.map((ref) => ref.toLowerCase()),
468
+ ...extractMountSourceGuids(asset),
469
+ ]);
400
470
  }
401
- const outputs = packObj.assets.map((asset) => ({
402
- guid: asset.guid,
403
- ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
404
- ...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
405
- kind: asset.kind,
406
- ...(asset.name === undefined ? {} : { name: asset.name }),
407
- ...(asset.execution === undefined ? {} : { execution: asset.execution }),
408
- refs: asset.refs,
409
- }));
410
- declarations.push({
471
+ capture?.declarations.set(packPath, {
472
+ format: 'pack.json',
411
473
  sourcePath: packPath,
412
- canonicalSourcePath: packPath.replace(/\\/g, '/'),
413
- producer: 'direct-pack',
414
- ...(packObj.packageId === undefined ? {} : { packageId: packObj.packageId }),
415
- projectedMeta: {
416
- schemaVersion: '2.0.0',
417
- ...(packObj.packageId === undefined ? {} : { packageId: packObj.packageId }),
418
- outputs,
419
- externalGuids: outputs.flatMap((output) => output.refs),
420
- },
421
- outputs,
422
- externalGuids: outputs.flatMap((output) => output.refs),
423
- diagnostics: [],
474
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
475
+ sourceText: raw,
476
+ value: packObj,
424
477
  });
425
478
  }
426
479
 
427
480
  // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
428
481
  const { paths: assetPaths } = loadAssetConfig(process.cwd());
429
482
  for (const metaPath of metaPaths) {
430
- let parsed: unknown;
431
- try {
432
- const raw = await readFile(metaPath, 'utf-8');
433
- parsed = JSON.parse(raw);
434
- } catch {
435
- return packErr(
436
- makePackError('pack-malformed-meta', {
437
- path: metaPath,
438
- ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
439
- }),
440
- );
441
- }
442
-
443
- const valid = validateMeta(parsed);
444
- if (!valid) {
445
- return packErr(
446
- makePackError('pack-malformed-meta', {
447
- path: metaPath,
448
- ajvErrors: (validateMeta.errors ?? []).map((e) => ({
449
- instancePath: e.instancePath,
450
- message: e.message ?? 'unknown ajv error',
451
- })),
452
- }),
453
- );
454
- }
483
+ const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
484
+ if (!loaded.ok) return loaded;
485
+ const { raw, parsed } = loaded.value;
455
486
 
456
487
  const metaContract = validateProducerContract(parsed);
457
488
  if (!metaContract.ok) {
@@ -464,23 +495,13 @@ export async function scanInventory(
464
495
  }
465
496
 
466
497
  // Step 3: validate GUIDs in meta subAssets
467
- const metaObj = parsed as {
468
- packageId?: string;
469
- importer?: string;
470
- source?: string;
471
- name?: string;
472
- importSettings?: {
473
- readonly externalAssets?: readonly { readonly guid?: unknown }[];
474
- };
475
- subAssets: {
476
- guid: string;
477
- sourceIndex: number;
478
- sourceKey?: string;
479
- kind: string;
480
- name?: string;
481
- }[];
482
- sourceOverrides?: unknown;
483
- };
498
+ const metaObj = parsed as unknown as MetaInventoryDocument;
499
+ capture?.declarations.set(metaPath, {
500
+ format: 'meta.json',
501
+ sourcePath: metaPath,
502
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
503
+ value: metaObj,
504
+ });
484
505
  for (const sub of metaObj.subAssets) {
485
506
  if (!isValidAssetGuidString(sub.guid)) {
486
507
  return packErr(
@@ -501,13 +522,10 @@ export async function scanInventory(
501
522
  );
502
523
  }
503
524
  guidToPath.set(normalizedGuid, metaPath);
504
- metaByGuid.set(normalizedGuid, metaPath);
505
525
  }
506
526
  const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
507
527
  if (producerSubAssets.length > 0) {
508
- const topologyContract = validateProducerOutputs(
509
- producerSubAssets as unknown as readonly ImportedOutputDeclaration[],
510
- );
528
+ const topologyContract = validateProducerOutputs(producerSubAssets);
511
529
  if (!topologyContract.ok) {
512
530
  return packErr(
513
531
  makePackError('pack-malformed-meta', {
@@ -536,122 +554,86 @@ export async function scanInventory(
536
554
  }),
537
555
  );
538
556
  }
539
- const outputs = metaObj.subAssets.map((sub) => ({
540
- guid: sub.guid,
541
- ...(sub.sourceKey === undefined ? {} : { sourceKey: sub.sourceKey }),
542
- sourceIndex: sub.sourceIndex,
543
- kind: sub.kind,
544
- ...(sub.name === undefined ? {} : { name: sub.name }),
545
- refs: [],
546
- }));
547
- const externalGuids = (metaObj.importSettings?.externalAssets ?? []).flatMap((entry) =>
548
- typeof entry.guid === 'string' ? [entry.guid] : [],
549
- );
550
- declarations.push({
551
- sourcePath: metaPath,
552
- canonicalSourcePath: metaPath.replace(/\\/g, '/'),
553
- producer: 'source-meta',
554
- ...(metaObj.packageId === undefined ? {} : { packageId: metaObj.packageId }),
555
- projectedMeta: {
556
- ...(metaObj.packageId === undefined ? {} : { packageId: metaObj.packageId }),
557
- ...(metaObj.importer === undefined ? {} : { importer: metaObj.importer }),
558
- ...(metaObj.source === undefined ? {} : { source: metaObj.source }),
559
- ...(metaObj.name === undefined ? {} : { name: metaObj.name }),
560
- outputs,
561
- externalGuids,
562
- },
563
- outputs,
564
- externalGuids,
565
- diagnostics: [],
566
- });
567
557
  }
568
558
 
569
559
  // ScriptablePack identity enters the same topology and collision authority as JSON declarations.
570
- const scriptablePackExecutorPool =
571
- scriptablePaths.length === 0 || opts.scriptablePackLoad?.executor !== undefined
572
- ? undefined
573
- : createScriptablePackModuleExecutorPool();
574
- try {
575
- for (const sourcePath of scriptablePaths) {
576
- const executor = await scriptablePackExecutorPool?.acquire();
577
- const loaded = await loadScriptablePack(sourcePath, {
578
- ...opts.scriptablePackLoad,
579
- metadataOnly: true,
580
- ...(executor === undefined ? {} : { executor }),
581
- });
582
- if (!loaded.ok) {
583
- const diagnostic =
584
- 'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
585
- return packErr(
586
- makePackError('pack-malformed-meta', {
587
- path: sourcePath,
588
- ajvErrors: [
589
- {
590
- instancePath: '',
591
- message:
592
- diagnostic === undefined
593
- ? loaded.error.code
594
- : `${loaded.error.code}: ${diagnostic}`,
595
- },
596
- ],
597
- }),
598
- );
599
- }
600
- const meta = projectScriptablePackMeta(loaded.value, sourcePath);
601
- scriptablePackMeta.set(sourcePath, meta);
602
- const sourceClosure = await inventoryScriptablePackSource(sourcePath);
603
- const topology = validateProducerOutputs(
604
- meta.subAssets as unknown as readonly ImportedOutputDeclaration[],
560
+ for (const sourcePath of scriptablePaths) {
561
+ let source: string;
562
+ try {
563
+ source = await readFile(sourcePath, 'utf8');
564
+ } catch {
565
+ return packErr(
566
+ makePackError('pack-malformed-meta', {
567
+ path: sourcePath,
568
+ ajvErrors: [{ instancePath: '', message: 'ScriptablePack source read failed' }],
569
+ }),
570
+ );
571
+ }
572
+ const loaded = await loadScriptablePack(sourcePath, {
573
+ ...(opts.scriptablePack ?? {}),
574
+ ...(capture === undefined ? { metadataOnly: true } : {}),
575
+ });
576
+ if (!loaded.ok) {
577
+ const diagnostic =
578
+ 'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
579
+ return packErr(
580
+ makePackError('pack-malformed-meta', {
581
+ path: sourcePath,
582
+ ajvErrors: [
583
+ {
584
+ instancePath: '',
585
+ message:
586
+ diagnostic === undefined
587
+ ? loaded.error.code
588
+ : `${loaded.error.code}: ${diagnostic}`,
589
+ },
590
+ ],
591
+ }),
592
+ );
593
+ }
594
+ let sourceClosure: readonly ScriptablePackSourceClosureEntry[];
595
+ try {
596
+ sourceClosure = await inventoryScriptablePackSource(sourcePath, source);
597
+ } catch {
598
+ return packErr(
599
+ makePackError('pack-malformed-meta', {
600
+ path: sourcePath,
601
+ ajvErrors: [{ instancePath: '', message: 'ScriptablePack source closure read failed' }],
602
+ }),
605
603
  );
606
- if (!topology.ok) {
604
+ }
605
+ const meta = projectScriptablePackMeta(loaded.value, sourcePath);
606
+ const topology = validateProducerOutputs(meta.subAssets);
607
+ if (!topology.ok) {
608
+ return packErr(
609
+ makePackError('pack-malformed-meta', {
610
+ path: sourcePath,
611
+ ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
612
+ }),
613
+ );
614
+ }
615
+ const declaration = {
616
+ sourcePath,
617
+ sourceRevision: `sha256:${createHash('sha256').update(source).digest('hex')}`,
618
+ meta,
619
+ definition: loaded.value,
620
+ sourceClosure,
621
+ } satisfies ScriptablePackInventoryDeclaration;
622
+ capture?.declarations.set(sourcePath, {
623
+ format: 'pack.ts',
624
+ ...declaration,
625
+ value: meta,
626
+ });
627
+ for (const sub of meta.subAssets) {
628
+ const guid = sub.guid.toLowerCase();
629
+ const existing = guidToPath.get(guid);
630
+ if (existing !== undefined) {
607
631
  return packErr(
608
- makePackError('pack-malformed-meta', {
609
- path: sourcePath,
610
- ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
611
- }),
632
+ makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
612
633
  );
613
634
  }
614
- for (const sub of meta.subAssets) {
615
- const guid = sub.guid.toLowerCase();
616
- const existing = guidToPath.get(guid);
617
- if (existing !== undefined) {
618
- return packErr(
619
- makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
620
- );
621
- }
622
- guidToPath.set(guid, sourcePath);
623
- }
624
- const outputs = meta.subAssets.map((sub) => ({
625
- guid: sub.guid,
626
- sourceKey: sub.sourceKey,
627
- sourceIndex: sub.sourceIndex,
628
- kind: sub.kind,
629
- ...(sub.name === undefined ? {} : { name: sub.name }),
630
- refs: [],
631
- }));
632
- const externalGuids = meta.importSettings.externalAssets.map((entry) => entry.guid);
633
- declarations.push({
634
- sourcePath,
635
- canonicalSourcePath: sourcePath.replace(/\\/g, '/'),
636
- producer: 'scriptable-pack',
637
- packageId: meta.packageId,
638
- projectedMeta: {
639
- schemaVersion: meta.schemaVersion,
640
- packageId: meta.packageId,
641
- importer: meta.importer,
642
- source: meta.source,
643
- ...(meta.name === undefined ? {} : { name: meta.name }),
644
- outputs,
645
- externalGuids,
646
- },
647
- outputs,
648
- externalGuids,
649
- sourceClosure,
650
- diagnostics: [],
651
- });
635
+ guidToPath.set(guid, sourcePath);
652
636
  }
653
- } finally {
654
- await scriptablePackExecutorPool?.dispose();
655
637
  }
656
638
 
657
639
  // Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
@@ -693,24 +675,41 @@ export async function scanInventory(
693
675
  }
694
676
  }
695
677
 
696
- const declarationsByPath = new Map(
697
- declarations.map((declaration) => [declaration.sourcePath, declaration]),
698
- );
699
- return ok({
700
- paths: rawPaths,
701
- scriptablePackMeta,
702
- declarations,
703
- declarationsByPath,
704
- sourceByGuid: new Map(guidToPath),
705
- metaByGuid,
706
- });
678
+ return ok(rawPaths);
707
679
  }
708
680
 
709
- /** Compatibility projection for callers that only need discovered paths. */
681
+ /** Scan one complete Pack inventory while preserving the public path-only API. */
710
682
  export async function scan(
711
683
  roots: readonly string[],
712
684
  opts: ScanOptions = {},
713
685
  ): Promise<ScanResult<string[], PackError>> {
714
- const inventory = await scanInventory(roots, opts);
715
- return inventory.ok ? ok([...inventory.value.paths]) : inventory;
686
+ return scanValidated(roots, opts);
687
+ }
688
+
689
+ /** Return the validated source inventory without interpreting producer output kinds. */
690
+ export async function scanInventory(
691
+ roots: readonly string[],
692
+ opts: ScanOptions = {},
693
+ ): Promise<ScanResult<ScanInventory, PackError>> {
694
+ const declarations = new Map<string, ScanSourceDeclaration>();
695
+ const scanned = await scanValidated(roots, opts, { declarations });
696
+ if (!scanned.ok) return scanned;
697
+ const inventory: InventoryDeclaration[] = [];
698
+ for (const sourcePath of scanned.value) {
699
+ const declaration = declarations.get(sourcePath);
700
+ if (declaration?.format !== 'pack.json') continue;
701
+ for (const [index, asset] of declaration.value.assets.entries()) {
702
+ inventory.push({
703
+ guid: asset.guid,
704
+ kind: asset.kind,
705
+ sourcePath,
706
+ sourceRevision: declaration.sourceRevision,
707
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
708
+ ...(asset.sourceIndex === undefined
709
+ ? { sourceIndex: index }
710
+ : { sourceIndex: asset.sourceIndex }),
711
+ });
712
+ }
713
+ }
714
+ return ok({ paths: scanned.value, inventory, declarations });
716
715
  }