@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,88 +1,16 @@
1
- import { dirname, posix, resolve } from 'node:path';
2
- import type { Result } from '@forgeax/engine-types';
3
- import { PACK_ERROR_HINTS, err as resultErr, ok as resultOk } from '@forgeax/engine-types';
1
+ import { dirname, resolve } from 'node:path';
4
2
  import { deriveAssetName } from './deriveAssetName.js';
5
- import { PackError } from './errors.js';
6
3
 
7
4
  function deriveSourceName(metaFileName: string): string {
8
5
  return metaFileName.replace(/\.meta\.json$/, '');
9
6
  }
10
7
 
11
- const PATH_REF_RE = /^@([^/]+)\/(.+)$/;
12
-
13
- function isPathEscape(pathDir: string, resolved: string): boolean {
14
- const rel = posix.relative(pathDir, resolved);
15
- return rel.startsWith('..') || posix.isAbsolute(rel);
16
- }
17
-
18
- export function resolveAssetSource(
19
- metaPath: string,
20
- source: string | undefined,
21
- paths: Record<string, string>,
22
- ): Result<string, PackError> {
8
+ export function resolveAssetSource(metaPath: string, source: string | undefined): string {
23
9
  if (source === undefined) {
24
10
  const metaDir = dirname(metaPath);
25
11
  const metaName = deriveAssetName(metaPath, 1);
26
12
  const derived = deriveSourceName(metaName);
27
- return resultOk(resolve(metaDir, derived));
13
+ return resolve(metaDir, derived);
28
14
  }
29
-
30
- if (source.startsWith('@')) {
31
- const match = source.match(PATH_REF_RE);
32
- if (!match) {
33
- return resultErr(
34
- new PackError({
35
- code: 'pack-malformed-path-ref',
36
- expected: 'source in format @<name>/<rest>',
37
- hint: PACK_ERROR_HINTS['pack-malformed-path-ref'],
38
- detail: {
39
- code: 'pack-malformed-path-ref',
40
- reason: 'format',
41
- rawSource: source,
42
- expectedFormat: '@<name>/<rest>',
43
- },
44
- }),
45
- );
46
- }
47
-
48
- const [, name, rest] = match as [string, string, string];
49
- const pathDir = paths[name];
50
-
51
- if (pathDir === undefined) {
52
- return resultErr(
53
- new PackError({
54
- code: 'pack-unknown-path',
55
- expected: 'path name found in package.json#forgeax.assets.paths',
56
- hint: PACK_ERROR_HINTS['pack-unknown-path'],
57
- detail: {
58
- code: 'pack-unknown-path',
59
- pathName: name,
60
- knownNames: Object.keys(paths),
61
- },
62
- }),
63
- );
64
- }
65
-
66
- const resolved = resolve(pathDir, rest);
67
-
68
- if (isPathEscape(pathDir, resolved)) {
69
- return resultErr(
70
- new PackError({
71
- code: 'pack-malformed-path-ref',
72
- expected: 'rest path must not escape the declared path directory',
73
- hint: PACK_ERROR_HINTS['pack-malformed-path-ref'],
74
- detail: {
75
- code: 'pack-malformed-path-ref',
76
- reason: 'escape',
77
- rawSource: source,
78
- expectedFormat: '@<name>/<rest>',
79
- },
80
- }),
81
- );
82
- }
83
-
84
- return resultOk(resolved);
85
- }
86
-
87
- return resultOk(resolve(dirname(metaPath), source));
15
+ return resolve(dirname(metaPath), source);
88
16
  }
package/src/scanner.ts CHANGED
@@ -9,21 +9,22 @@ import type {
9
9
  SourceOverrideDescriptor,
10
10
  } from '@forgeax/engine-types';
11
11
  import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
12
- import { loadAssetConfig } from './config.js';
13
12
  import { PackError } from './errors.js';
14
- import { isValidAssetGuidString } from './guid.js';
13
+ import { isValidAssetGuidString, PackageId } from './guid.js';
14
+ import {
15
+ type AnyParameterizedPackDefinition,
16
+ type ParameterizedPackSourceMeta,
17
+ parseParameterizedPackJson,
18
+ projectDirectPackJson,
19
+ projectParameterizedPackMeta,
20
+ } from './parameterized-pack.js';
15
21
  import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
16
22
  import { resolveAssetSource } from './resolve-asset-source.js';
17
23
  import { validateMeta, validatePack } from './schema-compiled.js';
18
- import {
19
- projectScriptablePackMeta,
20
- type ScriptablePackDefinition,
21
- type ScriptablePackMetaJson,
22
- type ScriptablePackSourceClosureEntry,
23
- } from './scriptable-pack.js';
24
+ import type { ScriptablePackSourceClosureEntry } from './scriptable-pack.js';
24
25
  import {
25
26
  inventoryScriptablePackSource,
26
- loadScriptablePack,
27
+ loadParameterizedScriptablePack,
27
28
  type ScriptablePackModuleExecutor,
28
29
  } from './scriptable-pack-node.js';
29
30
 
@@ -41,6 +42,10 @@ function packErr<E>(error: E): ScanResult<never, E> {
41
42
  return { ok: false, error };
42
43
  }
43
44
 
45
+ function record(value: unknown): value is Record<string, unknown> {
46
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
47
+ }
48
+
44
49
  /** Host-owned source paths that should not enter the Pack catalog. */
45
50
  export interface ScanOptions {
46
51
  readonly ignorePath?: (path: string) => boolean;
@@ -80,8 +85,8 @@ export interface ScanInventory {
80
85
  export interface ScriptablePackInventoryDeclaration {
81
86
  readonly sourcePath: string;
82
87
  readonly sourceRevision: string;
83
- readonly meta: ScriptablePackMetaJson;
84
- readonly definition: Readonly<ScriptablePackDefinition>;
88
+ readonly meta: ParameterizedPackSourceMeta;
89
+ readonly definition: Readonly<AnyParameterizedPackDefinition>;
85
90
  readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
86
91
  }
87
92
 
@@ -110,7 +115,7 @@ export interface MetaInventorySubAsset {
110
115
  readonly kind: string;
111
116
  }
112
117
 
113
- export interface PackInventoryDocument {
118
+ export interface LegacyPackInventoryDocument {
114
119
  readonly schemaVersion: '1.0.0' | '2.0.0';
115
120
  readonly kind: 'internal-text-package';
116
121
  readonly packageId?: string;
@@ -120,6 +125,18 @@ export interface PackInventoryDocument {
120
125
  readonly assets: readonly PackInventoryAsset[];
121
126
  }
122
127
 
128
+ export interface ParameterizedPackInventoryDocument {
129
+ readonly schemaVersion: '3.0.0';
130
+ readonly packageId: string;
131
+ readonly assets?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
132
+ readonly parent?: string;
133
+ readonly values?: Readonly<Record<string, unknown>>;
134
+ }
135
+
136
+ export type PackInventoryDocument =
137
+ | LegacyPackInventoryDocument
138
+ | ParameterizedPackInventoryDocument;
139
+
123
140
  export interface PackInventoryAsset {
124
141
  readonly guid: string;
125
142
  readonly kind: string;
@@ -146,12 +163,14 @@ export type ScanSourceDeclaration =
146
163
  readonly sourceText: string;
147
164
  readonly value: PackInventoryDocument;
148
165
  }
149
- | ({
150
- readonly format: 'pack.ts';
166
+ | {
167
+ readonly format: 'pack.ts-parameterized';
151
168
  readonly sourcePath: string;
152
169
  readonly sourceRevision: string;
153
- readonly value: ScriptablePackMetaJson;
154
- } & ScriptablePackInventoryDeclaration);
170
+ readonly value: ParameterizedPackSourceMeta;
171
+ readonly definition: AnyParameterizedPackDefinition;
172
+ readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
173
+ };
155
174
 
156
175
  interface ScanCapture {
157
176
  readonly declarations: Map<string, ScanSourceDeclaration>;
@@ -169,6 +188,7 @@ interface ScanCapture {
169
188
  */
170
189
  const BLACKLIST = new Set([
171
190
  'node_modules',
191
+ '__tests__',
172
192
  '.forgeax-harness',
173
193
  '.forgeax',
174
194
  '.git',
@@ -349,12 +369,99 @@ async function scanValidated(
349
369
  // kind stays in the path/detail evidence; identity is the normalized GUID.
350
370
  const guidToPath = new Map<string, string>();
351
371
  const packRefs = new Map<string, string[]>(); // guid -> refs[]
372
+ const packageIdToPath = new Map<string, string>();
373
+ const packageKind = new Map<string, 'legacy' | 'direct' | 'instance' | 'parameterized'>();
374
+ const instanceParents: {
375
+ readonly path: string;
376
+ readonly packageId: string;
377
+ readonly parent: string;
378
+ }[] = [];
379
+
380
+ function registerPackage(
381
+ packageId: string | undefined,
382
+ path: string,
383
+ kind: 'legacy' | 'direct' | 'instance' | 'parameterized',
384
+ ): PackError | undefined {
385
+ if (packageId === undefined) return undefined;
386
+ const normalized = packageId.toLowerCase();
387
+ const existing = packageIdToPath.get(normalized);
388
+ if (existing !== undefined) {
389
+ return makePackError('pack-guid-collision', {
390
+ paths: [existing, path],
391
+ guid: normalized,
392
+ });
393
+ }
394
+ packageIdToPath.set(normalized, path);
395
+ packageKind.set(normalized, kind);
396
+ return undefined;
397
+ }
352
398
 
353
399
  for (const packPath of packPaths) {
354
400
  const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
355
401
  if (!loaded.ok) return loaded;
356
402
  const { raw, parsed } = loaded.value;
357
403
 
404
+ if (record(parsed) && parsed.schemaVersion === '3.0.0') {
405
+ const authoring = parseParameterizedPackJson(parsed);
406
+ if (!authoring.ok) {
407
+ return packErr(
408
+ makePackError('pack-malformed-pack', {
409
+ path: packPath,
410
+ ajvErrors: [{ instancePath: '', message: authoring.error.code }],
411
+ }),
412
+ );
413
+ }
414
+ const packageId = authoring.value.packageId;
415
+ const packageCollision = registerPackage(
416
+ PackageId.format(packageId),
417
+ packPath,
418
+ authoring.value.format,
419
+ );
420
+ if (packageCollision !== undefined) return packErr(packageCollision);
421
+ if (authoring.value.format === 'instance') {
422
+ instanceParents.push({
423
+ path: packPath,
424
+ packageId: PackageId.format(authoring.value.packageId),
425
+ parent: PackageId.format(authoring.value.parent),
426
+ });
427
+ } else {
428
+ const projected = projectDirectPackJson(authoring.value);
429
+ if (!projected.ok) {
430
+ return packErr(
431
+ makePackError('pack-malformed-pack', {
432
+ path: packPath,
433
+ ajvErrors: [{ instancePath: '/assets', message: projected.error.code }],
434
+ }),
435
+ );
436
+ }
437
+ for (const asset of projected.value.assets) {
438
+ const normalizedGuid = asset.guid.toLowerCase();
439
+ const existing = guidToPath.get(normalizedGuid);
440
+ if (existing !== undefined) {
441
+ return packErr(
442
+ makePackError('pack-guid-collision', {
443
+ paths: [existing, packPath],
444
+ guid: normalizedGuid,
445
+ }),
446
+ );
447
+ }
448
+ guidToPath.set(normalizedGuid, packPath);
449
+ packRefs.set(normalizedGuid, [
450
+ ...asset.refs.map((ref) => ref.toLowerCase()),
451
+ ...extractMountSourceGuids(asset),
452
+ ]);
453
+ }
454
+ }
455
+ capture?.declarations.set(packPath, {
456
+ format: 'pack.json',
457
+ sourcePath: packPath,
458
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
459
+ sourceText: raw,
460
+ value: parsed as unknown as ParameterizedPackInventoryDocument,
461
+ });
462
+ continue;
463
+ }
464
+
358
465
  const packageContract = validateProducerContract(parsed);
359
466
  if (!packageContract.ok) {
360
467
  return packErr(
@@ -366,7 +473,9 @@ async function scanValidated(
366
473
  }
367
474
 
368
475
  // Step 3: validate GUIDs in pack
369
- const packObj = parsed as unknown as PackInventoryDocument;
476
+ const packObj = parsed as unknown as LegacyPackInventoryDocument;
477
+ const packageCollision = registerPackage(packObj.packageId, packPath, 'legacy');
478
+ if (packageCollision !== undefined) return packErr(packageCollision);
370
479
  for (const asset of packObj.assets) {
371
480
  if (
372
481
  asset.kind === 'particle-effect' &&
@@ -477,8 +586,9 @@ async function scanValidated(
477
586
  });
478
587
  }
479
588
 
480
- // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
481
- const { paths: assetPaths } = loadAssetConfig(process.cwd());
589
+ // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files.
590
+ // Meta source paths are local to their sidecar: an omitted source names the
591
+ // companion file and an explicit source is resolved relative to the sidecar.
482
592
  for (const metaPath of metaPaths) {
483
593
  const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
484
594
  if (!loaded.ok) return loaded;
@@ -496,6 +606,8 @@ async function scanValidated(
496
606
 
497
607
  // Step 3: validate GUIDs in meta subAssets
498
608
  const metaObj = parsed as unknown as MetaInventoryDocument;
609
+ const packageCollision = registerPackage(metaObj.packageId, metaPath, 'legacy');
610
+ if (packageCollision !== undefined) return packErr(packageCollision);
499
611
  capture?.declarations.set(metaPath, {
500
612
  format: 'meta.json',
501
613
  sourcePath: metaPath,
@@ -537,13 +649,7 @@ async function scanValidated(
537
649
  }
538
650
 
539
651
  // Step 5: orphan .meta.json check — the source file declared in meta.source must exist.
540
- // Uses resolveAssetSource for three-mode dispatch (undefined derivation / @name/ path table /
541
- // relative path), replacing the former hardcoded join(metaDir, metaObj.source).
542
- const sourceResolved = resolveAssetSource(metaPath, metaObj.source, assetPaths);
543
- if (!sourceResolved.ok) {
544
- return packErr(sourceResolved.error);
545
- }
546
- const expectedSourcePath = sourceResolved.value;
652
+ const expectedSourcePath = resolveAssetSource(metaPath, metaObj.source);
547
653
  try {
548
654
  await stat(expectedSourcePath);
549
655
  } catch {
@@ -556,7 +662,8 @@ async function scanValidated(
556
662
  }
557
663
  }
558
664
 
559
- // ScriptablePack identity enters the same topology and collision authority as JSON declarations.
665
+ // Parameterized source identity enters the package registry. Its outputs are
666
+ // intentionally absent until the dynamic build worklist evaluates it.
560
667
  for (const sourcePath of scriptablePaths) {
561
668
  let source: string;
562
669
  try {
@@ -565,17 +672,17 @@ async function scanValidated(
565
672
  return packErr(
566
673
  makePackError('pack-malformed-meta', {
567
674
  path: sourcePath,
568
- ajvErrors: [{ instancePath: '', message: 'ScriptablePack source read failed' }],
675
+ ajvErrors: [{ instancePath: '', message: 'Pack source read failed' }],
569
676
  }),
570
677
  );
571
678
  }
572
- const loaded = await loadScriptablePack(sourcePath, {
679
+ const loaderOptions = {
573
680
  ...(opts.scriptablePack ?? {}),
574
681
  ...(capture === undefined ? { metadataOnly: true } : {}),
575
- });
682
+ };
683
+ const loaded = await loadParameterizedScriptablePack(sourcePath, loaderOptions);
576
684
  if (!loaded.ok) {
577
- const diagnostic =
578
- 'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
685
+ const diagnostic = loaded.error.detail.diagnostic;
579
686
  return packErr(
580
687
  makePackError('pack-malformed-meta', {
581
688
  path: sourcePath,
@@ -583,9 +690,9 @@ async function scanValidated(
583
690
  {
584
691
  instancePath: '',
585
692
  message:
586
- diagnostic === undefined
587
- ? loaded.error.code
588
- : `${loaded.error.code}: ${diagnostic}`,
693
+ typeof diagnostic === 'string'
694
+ ? `${loaded.error.code}: ${diagnostic}`
695
+ : loaded.error.code,
589
696
  },
590
697
  ],
591
698
  }),
@@ -598,41 +705,84 @@ async function scanValidated(
598
705
  return packErr(
599
706
  makePackError('pack-malformed-meta', {
600
707
  path: sourcePath,
601
- ajvErrors: [{ instancePath: '', message: 'ScriptablePack source closure read failed' }],
708
+ ajvErrors: [{ instancePath: '', message: 'Pack source closure read failed' }],
602
709
  }),
603
710
  );
604
711
  }
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 = {
712
+ const packageCollision = registerPackage(
713
+ PackageId.format(loaded.value.packageId),
714
+ sourcePath,
715
+ 'parameterized',
716
+ );
717
+ if (packageCollision !== undefined) return packErr(packageCollision);
718
+ const meta = projectParameterizedPackMeta(loaded.value, sourcePath);
719
+ capture?.declarations.set(sourcePath, {
720
+ format: 'pack.ts-parameterized',
616
721
  sourcePath,
617
722
  sourceRevision: `sha256:${createHash('sha256').update(source).digest('hex')}`,
618
- meta,
723
+ value: meta,
619
724
  definition: loaded.value,
620
725
  sourceClosure,
621
- } satisfies ScriptablePackInventoryDeclaration;
622
- capture?.declarations.set(sourcePath, {
623
- format: 'pack.ts',
624
- ...declaration,
625
- value: meta,
626
726
  });
627
- for (const sub of meta.subAssets) {
628
- const guid = sub.guid.toLowerCase();
629
- const existing = guidToPath.get(guid);
630
- if (existing !== undefined) {
727
+ }
728
+
729
+ // v3 instance parent validation happens after all declarations have been
730
+ // registered, so scan order cannot change missing-parent or cycle results.
731
+ const parentByPackageId = new Map(
732
+ instanceParents.map((instance) => [instance.packageId.toLowerCase(), instance]),
733
+ );
734
+ for (const instance of instanceParents) {
735
+ const parentId = instance.parent.toLowerCase();
736
+ const parentPath = packageIdToPath.get(parentId);
737
+ if (parentPath === undefined) {
738
+ return packErr(
739
+ makePackError('pack-malformed-pack', {
740
+ path: instance.path,
741
+ reason: 'pack-parent-not-found',
742
+ ajvErrors: [
743
+ {
744
+ instancePath: '/parent',
745
+ message: `parent packageId ${instance.parent} was not found`,
746
+ },
747
+ ],
748
+ }),
749
+ );
750
+ }
751
+ const kind = packageKind.get(parentId);
752
+ if (kind !== 'parameterized' && kind !== 'instance') {
753
+ return packErr(
754
+ makePackError('pack-malformed-pack', {
755
+ path: instance.path,
756
+ reason: 'pack-parent-not-parameterized',
757
+ ajvErrors: [
758
+ {
759
+ instancePath: '/parent',
760
+ message: `parent ${instance.parent} is not a parameterized Pack`,
761
+ },
762
+ ],
763
+ }),
764
+ );
765
+ }
766
+ }
767
+ for (const instance of instanceParents) {
768
+ const chain = new Set<string>();
769
+ let current = instance.packageId.toLowerCase();
770
+ while (true) {
771
+ if (chain.has(current)) {
631
772
  return packErr(
632
- makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
773
+ makePackError('pack-malformed-pack', {
774
+ path: instance.path,
775
+ reason: 'pack-parent-cycle',
776
+ ajvErrors: [
777
+ { instancePath: '/parent', message: `parent chain repeats packageId ${current}` },
778
+ ],
779
+ }),
633
780
  );
634
781
  }
635
- guidToPath.set(guid, sourcePath);
782
+ chain.add(current);
783
+ const next = parentByPackageId.get(current);
784
+ if (next === undefined) break;
785
+ current = next.parent.toLowerCase();
636
786
  }
637
787
  }
638
788
 
@@ -698,6 +848,38 @@ export async function scanInventory(
698
848
  for (const sourcePath of scanned.value) {
699
849
  const declaration = declarations.get(sourcePath);
700
850
  if (declaration?.format !== 'pack.json') continue;
851
+ if (declaration.value.schemaVersion === '3.0.0') {
852
+ const parsed = parseParameterizedPackJson(declaration.value);
853
+ if (!parsed.ok) {
854
+ return packErr(
855
+ makePackError('pack-malformed-pack', {
856
+ path: sourcePath,
857
+ ajvErrors: [{ instancePath: '', message: parsed.error.code }],
858
+ }),
859
+ );
860
+ }
861
+ if (parsed.value.format !== 'direct') continue;
862
+ const projected = projectDirectPackJson(parsed.value);
863
+ if (!projected.ok) {
864
+ return packErr(
865
+ makePackError('pack-malformed-pack', {
866
+ path: sourcePath,
867
+ ajvErrors: [{ instancePath: '/assets', message: projected.error.code }],
868
+ }),
869
+ );
870
+ }
871
+ for (const [index, asset] of projected.value.assets.entries()) {
872
+ inventory.push({
873
+ guid: asset.guid,
874
+ kind: asset.kind,
875
+ sourcePath,
876
+ sourceRevision: declaration.sourceRevision,
877
+ sourceKey: asset.sourceKey,
878
+ sourceIndex: index,
879
+ });
880
+ }
881
+ continue;
882
+ }
701
883
  for (const [index, asset] of declaration.value.assets.entries()) {
702
884
  inventory.push({
703
885
  guid: asset.guid,
@@ -152,6 +152,7 @@ export function buildSceneAssetValidator(
152
152
  required: ['kind', 'entities'],
153
153
  properties: {
154
154
  kind: { type: 'string', const: 'scene' },
155
+ sourceKey: { type: 'string', minLength: 1 },
155
156
  entities: {
156
157
  type: 'array',
157
158
  items: {
@@ -160,6 +161,7 @@ export function buildSceneAssetValidator(
160
161
  required: ['localId', 'components'],
161
162
  properties: {
162
163
  localId: { type: 'integer', minimum: 0 },
164
+ bindingKey: { type: 'string', minLength: 1 },
163
165
  components: {
164
166
  type: 'object',
165
167
  additionalProperties: false,