@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
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import type { AssetCodec, AssetRelation, CookReceipt } from '@forgeax/engine-types';
2
+ import type { AssetCodec, CookReceipt } from '@forgeax/engine-types';
3
3
 
4
4
  export interface PackageArtifactBody {
5
5
  readonly mediaType: string;
@@ -24,25 +24,6 @@ export interface PackageProduct {
24
24
  readonly sourceKey?: string;
25
25
  }
26
26
 
27
- /** Minimal authored Pack v1/v2 shape consumed by the file transport seam. */
28
- export interface AuthoredPackAssetInput {
29
- readonly guid: string;
30
- readonly kind: string;
31
- readonly name?: string;
32
- readonly execution?: 'direct' | 'cooked';
33
- readonly sourceKey?: string;
34
- readonly sourceIndex?: number;
35
- readonly relations?: readonly AssetRelation[];
36
- readonly payload: Record<string, unknown>;
37
- readonly refs?: readonly string[];
38
- readonly artifacts?: Readonly<Record<string, unknown>>;
39
- }
40
-
41
- export interface AuthoredPackInput {
42
- readonly schemaVersion?: string;
43
- readonly assets?: readonly AuthoredPackAssetInput[];
44
- }
45
-
46
27
  export interface PackageDocumentArtifact {
47
28
  readonly path: string;
48
29
  readonly mediaType: string;
@@ -66,28 +47,6 @@ export interface PackageDocument {
66
47
  readonly assets: readonly PackageDocumentAsset[];
67
48
  }
68
49
 
69
- /** Upgrade the legacy authored Pack envelope at the Pack schema boundary. */
70
- export function upgradeLegacyAuthoredPack(pack: AuthoredPackInput): AuthoredPackInput {
71
- if (pack.schemaVersion !== '1.0.0' || pack.assets === undefined) return pack;
72
- return {
73
- ...pack,
74
- schemaVersion: '2.0.0',
75
- assets: pack.assets.map((asset) => {
76
- const fontRefs =
77
- asset.kind === 'font'
78
- ? [asset.payload.atlasGuid, asset.payload.samplerGuid].filter(
79
- (guid): guid is string => typeof guid === 'string',
80
- )
81
- : [];
82
- return {
83
- ...asset,
84
- refs: asset.refs ?? fontRefs,
85
- artifacts: asset.artifacts ?? {},
86
- };
87
- }),
88
- };
89
- }
90
-
91
50
  export interface PackageFinalizePolicy {
92
51
  readonly base: string;
93
52
  readonly packagePath: string;
@@ -364,6 +323,7 @@ export async function finalizePackageTransportSource(
364
323
  packageUrl: finalized.value.packageUrl,
365
324
  artifacts: transportArtifacts(product.assets, policy.artifactPath),
366
325
  digest: finalized.value.digest,
326
+ receipts: finalized.value.receipts,
367
327
  sourceRevision,
368
328
  semantic: JSON.stringify(sorted(finalized.value.pack)),
369
329
  };
@@ -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,18 +9,19 @@ 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 AnyScriptablePackDefinition,
16
+ parsePackSourceJson,
17
+ projectDirectPackJson,
18
+ projectScriptablePackMeta,
19
+ type ScriptablePackSourceMeta,
20
+ } from './pack-authoring.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
27
  loadScriptablePack,
@@ -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: ScriptablePackSourceMeta;
89
+ readonly definition: Readonly<AnyScriptablePackDefinition>;
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,16 @@ export interface PackInventoryDocument {
120
125
  readonly assets: readonly PackInventoryAsset[];
121
126
  }
122
127
 
128
+ export interface PackSourceInventoryDocument {
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 = LegacyPackInventoryDocument | PackSourceInventoryDocument;
137
+
123
138
  export interface PackInventoryAsset {
124
139
  readonly guid: string;
125
140
  readonly kind: string;
@@ -146,12 +161,14 @@ export type ScanSourceDeclaration =
146
161
  readonly sourceText: string;
147
162
  readonly value: PackInventoryDocument;
148
163
  }
149
- | ({
164
+ | {
150
165
  readonly format: 'pack.ts';
151
166
  readonly sourcePath: string;
152
167
  readonly sourceRevision: string;
153
- readonly value: ScriptablePackMetaJson;
154
- } & ScriptablePackInventoryDeclaration);
168
+ readonly value: ScriptablePackSourceMeta;
169
+ readonly definition: AnyScriptablePackDefinition;
170
+ readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
171
+ };
155
172
 
156
173
  interface ScanCapture {
157
174
  readonly declarations: Map<string, ScanSourceDeclaration>;
@@ -169,6 +186,7 @@ interface ScanCapture {
169
186
  */
170
187
  const BLACKLIST = new Set([
171
188
  'node_modules',
189
+ '__tests__',
172
190
  '.forgeax-harness',
173
191
  '.forgeax',
174
192
  '.git',
@@ -349,12 +367,99 @@ async function scanValidated(
349
367
  // kind stays in the path/detail evidence; identity is the normalized GUID.
350
368
  const guidToPath = new Map<string, string>();
351
369
  const packRefs = new Map<string, string[]>(); // guid -> refs[]
370
+ const packageIdToPath = new Map<string, string>();
371
+ const packageKind = new Map<string, 'legacy' | 'direct' | 'instance' | 'scriptable'>();
372
+ const instanceParents: {
373
+ readonly path: string;
374
+ readonly packageId: string;
375
+ readonly parent: string;
376
+ }[] = [];
377
+
378
+ function registerPackage(
379
+ packageId: string | undefined,
380
+ path: string,
381
+ kind: 'legacy' | 'direct' | 'instance' | 'scriptable',
382
+ ): PackError | undefined {
383
+ if (packageId === undefined) return undefined;
384
+ const normalized = packageId.toLowerCase();
385
+ const existing = packageIdToPath.get(normalized);
386
+ if (existing !== undefined) {
387
+ return makePackError('pack-guid-collision', {
388
+ paths: [existing, path],
389
+ guid: normalized,
390
+ });
391
+ }
392
+ packageIdToPath.set(normalized, path);
393
+ packageKind.set(normalized, kind);
394
+ return undefined;
395
+ }
352
396
 
353
397
  for (const packPath of packPaths) {
354
398
  const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
355
399
  if (!loaded.ok) return loaded;
356
400
  const { raw, parsed } = loaded.value;
357
401
 
402
+ if (record(parsed) && parsed.schemaVersion === '3.0.0') {
403
+ const authoring = parsePackSourceJson(parsed);
404
+ if (!authoring.ok) {
405
+ return packErr(
406
+ makePackError('pack-malformed-pack', {
407
+ path: packPath,
408
+ ajvErrors: [{ instancePath: '', message: authoring.error.code }],
409
+ }),
410
+ );
411
+ }
412
+ const packageId = authoring.value.packageId;
413
+ const packageCollision = registerPackage(
414
+ PackageId.format(packageId),
415
+ packPath,
416
+ authoring.value.format,
417
+ );
418
+ if (packageCollision !== undefined) return packErr(packageCollision);
419
+ if (authoring.value.format === 'instance') {
420
+ instanceParents.push({
421
+ path: packPath,
422
+ packageId: PackageId.format(authoring.value.packageId),
423
+ parent: PackageId.format(authoring.value.parent),
424
+ });
425
+ } else {
426
+ const projected = projectDirectPackJson(authoring.value);
427
+ if (!projected.ok) {
428
+ return packErr(
429
+ makePackError('pack-malformed-pack', {
430
+ path: packPath,
431
+ ajvErrors: [{ instancePath: '/assets', message: projected.error.code }],
432
+ }),
433
+ );
434
+ }
435
+ for (const asset of projected.value.assets) {
436
+ const normalizedGuid = asset.guid.toLowerCase();
437
+ const existing = guidToPath.get(normalizedGuid);
438
+ if (existing !== undefined) {
439
+ return packErr(
440
+ makePackError('pack-guid-collision', {
441
+ paths: [existing, packPath],
442
+ guid: normalizedGuid,
443
+ }),
444
+ );
445
+ }
446
+ guidToPath.set(normalizedGuid, packPath);
447
+ packRefs.set(normalizedGuid, [
448
+ ...asset.refs.map((ref) => ref.toLowerCase()),
449
+ ...extractMountSourceGuids(asset),
450
+ ]);
451
+ }
452
+ }
453
+ capture?.declarations.set(packPath, {
454
+ format: 'pack.json',
455
+ sourcePath: packPath,
456
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
457
+ sourceText: raw,
458
+ value: parsed as unknown as PackSourceInventoryDocument,
459
+ });
460
+ continue;
461
+ }
462
+
358
463
  const packageContract = validateProducerContract(parsed);
359
464
  if (!packageContract.ok) {
360
465
  return packErr(
@@ -366,7 +471,9 @@ async function scanValidated(
366
471
  }
367
472
 
368
473
  // Step 3: validate GUIDs in pack
369
- const packObj = parsed as unknown as PackInventoryDocument;
474
+ const packObj = parsed as unknown as LegacyPackInventoryDocument;
475
+ const packageCollision = registerPackage(packObj.packageId, packPath, 'legacy');
476
+ if (packageCollision !== undefined) return packErr(packageCollision);
370
477
  for (const asset of packObj.assets) {
371
478
  if (
372
479
  asset.kind === 'particle-effect' &&
@@ -477,8 +584,9 @@ async function scanValidated(
477
584
  });
478
585
  }
479
586
 
480
- // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
481
- const { paths: assetPaths } = loadAssetConfig(process.cwd());
587
+ // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files.
588
+ // Meta source paths are local to their sidecar: an omitted source names the
589
+ // companion file and an explicit source is resolved relative to the sidecar.
482
590
  for (const metaPath of metaPaths) {
483
591
  const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
484
592
  if (!loaded.ok) return loaded;
@@ -496,6 +604,8 @@ async function scanValidated(
496
604
 
497
605
  // Step 3: validate GUIDs in meta subAssets
498
606
  const metaObj = parsed as unknown as MetaInventoryDocument;
607
+ const packageCollision = registerPackage(metaObj.packageId, metaPath, 'legacy');
608
+ if (packageCollision !== undefined) return packErr(packageCollision);
499
609
  capture?.declarations.set(metaPath, {
500
610
  format: 'meta.json',
501
611
  sourcePath: metaPath,
@@ -537,13 +647,7 @@ async function scanValidated(
537
647
  }
538
648
 
539
649
  // 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;
650
+ const expectedSourcePath = resolveAssetSource(metaPath, metaObj.source);
547
651
  try {
548
652
  await stat(expectedSourcePath);
549
653
  } catch {
@@ -556,7 +660,8 @@ async function scanValidated(
556
660
  }
557
661
  }
558
662
 
559
- // ScriptablePack identity enters the same topology and collision authority as JSON declarations.
663
+ // ScriptablePack source identity enters the package registry. Its outputs are
664
+ // intentionally absent until the dynamic build worklist evaluates it.
560
665
  for (const sourcePath of scriptablePaths) {
561
666
  let source: string;
562
667
  try {
@@ -565,17 +670,17 @@ async function scanValidated(
565
670
  return packErr(
566
671
  makePackError('pack-malformed-meta', {
567
672
  path: sourcePath,
568
- ajvErrors: [{ instancePath: '', message: 'ScriptablePack source read failed' }],
673
+ ajvErrors: [{ instancePath: '', message: 'Pack source read failed' }],
569
674
  }),
570
675
  );
571
676
  }
572
- const loaded = await loadScriptablePack(sourcePath, {
677
+ const loaderOptions = {
573
678
  ...(opts.scriptablePack ?? {}),
574
679
  ...(capture === undefined ? { metadataOnly: true } : {}),
575
- });
680
+ };
681
+ const loaded = await loadScriptablePack(sourcePath, loaderOptions);
576
682
  if (!loaded.ok) {
577
- const diagnostic =
578
- 'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
683
+ const diagnostic = loaded.error.detail.diagnostic;
579
684
  return packErr(
580
685
  makePackError('pack-malformed-meta', {
581
686
  path: sourcePath,
@@ -583,9 +688,9 @@ async function scanValidated(
583
688
  {
584
689
  instancePath: '',
585
690
  message:
586
- diagnostic === undefined
587
- ? loaded.error.code
588
- : `${loaded.error.code}: ${diagnostic}`,
691
+ typeof diagnostic === 'string'
692
+ ? `${loaded.error.code}: ${diagnostic}`
693
+ : loaded.error.code,
589
694
  },
590
695
  ],
591
696
  }),
@@ -598,41 +703,84 @@ async function scanValidated(
598
703
  return packErr(
599
704
  makePackError('pack-malformed-meta', {
600
705
  path: sourcePath,
601
- ajvErrors: [{ instancePath: '', message: 'ScriptablePack source closure read failed' }],
706
+ ajvErrors: [{ instancePath: '', message: 'Pack source closure read failed' }],
602
707
  }),
603
708
  );
604
709
  }
710
+ const packageCollision = registerPackage(
711
+ PackageId.format(loaded.value.packageId),
712
+ sourcePath,
713
+ 'scriptable',
714
+ );
715
+ if (packageCollision !== undefined) return packErr(packageCollision);
605
716
  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 = {
717
+ capture?.declarations.set(sourcePath, {
718
+ format: 'pack.ts',
616
719
  sourcePath,
617
720
  sourceRevision: `sha256:${createHash('sha256').update(source).digest('hex')}`,
618
- meta,
721
+ value: meta,
619
722
  definition: loaded.value,
620
723
  sourceClosure,
621
- } satisfies ScriptablePackInventoryDeclaration;
622
- capture?.declarations.set(sourcePath, {
623
- format: 'pack.ts',
624
- ...declaration,
625
- value: meta,
626
724
  });
627
- for (const sub of meta.subAssets) {
628
- const guid = sub.guid.toLowerCase();
629
- const existing = guidToPath.get(guid);
630
- if (existing !== undefined) {
725
+ }
726
+
727
+ // v3 instance parent validation happens after all declarations have been
728
+ // registered, so scan order cannot change missing-parent or cycle results.
729
+ const parentByPackageId = new Map(
730
+ instanceParents.map((instance) => [instance.packageId.toLowerCase(), instance]),
731
+ );
732
+ for (const instance of instanceParents) {
733
+ const parentId = instance.parent.toLowerCase();
734
+ const parentPath = packageIdToPath.get(parentId);
735
+ if (parentPath === undefined) {
736
+ return packErr(
737
+ makePackError('pack-malformed-pack', {
738
+ path: instance.path,
739
+ reason: 'pack-parent-not-found',
740
+ ajvErrors: [
741
+ {
742
+ instancePath: '/parent',
743
+ message: `parent packageId ${instance.parent} was not found`,
744
+ },
745
+ ],
746
+ }),
747
+ );
748
+ }
749
+ const kind = packageKind.get(parentId);
750
+ if (kind !== 'scriptable' && kind !== 'instance') {
751
+ return packErr(
752
+ makePackError('pack-malformed-pack', {
753
+ path: instance.path,
754
+ reason: 'pack-parent-has-no-parameters',
755
+ ajvErrors: [
756
+ {
757
+ instancePath: '/parent',
758
+ message: `parent ${instance.parent} is not a ScriptablePack source with parameters`,
759
+ },
760
+ ],
761
+ }),
762
+ );
763
+ }
764
+ }
765
+ for (const instance of instanceParents) {
766
+ const chain = new Set<string>();
767
+ let current = instance.packageId.toLowerCase();
768
+ while (true) {
769
+ if (chain.has(current)) {
631
770
  return packErr(
632
- makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
771
+ makePackError('pack-malformed-pack', {
772
+ path: instance.path,
773
+ reason: 'pack-parent-cycle',
774
+ ajvErrors: [
775
+ { instancePath: '/parent', message: `parent chain repeats packageId ${current}` },
776
+ ],
777
+ }),
633
778
  );
634
779
  }
635
- guidToPath.set(guid, sourcePath);
780
+ chain.add(current);
781
+ const next = parentByPackageId.get(current);
782
+ if (next === undefined) break;
783
+ current = next.parent.toLowerCase();
636
784
  }
637
785
  }
638
786
 
@@ -698,6 +846,38 @@ export async function scanInventory(
698
846
  for (const sourcePath of scanned.value) {
699
847
  const declaration = declarations.get(sourcePath);
700
848
  if (declaration?.format !== 'pack.json') continue;
849
+ if (declaration.value.schemaVersion === '3.0.0') {
850
+ const parsed = parsePackSourceJson(declaration.value);
851
+ if (!parsed.ok) {
852
+ return packErr(
853
+ makePackError('pack-malformed-pack', {
854
+ path: sourcePath,
855
+ ajvErrors: [{ instancePath: '', message: parsed.error.code }],
856
+ }),
857
+ );
858
+ }
859
+ if (parsed.value.format !== 'direct') continue;
860
+ const projected = projectDirectPackJson(parsed.value);
861
+ if (!projected.ok) {
862
+ return packErr(
863
+ makePackError('pack-malformed-pack', {
864
+ path: sourcePath,
865
+ ajvErrors: [{ instancePath: '/assets', message: projected.error.code }],
866
+ }),
867
+ );
868
+ }
869
+ for (const [index, asset] of projected.value.assets.entries()) {
870
+ inventory.push({
871
+ guid: asset.guid,
872
+ kind: asset.kind,
873
+ sourcePath,
874
+ sourceRevision: declaration.sourceRevision,
875
+ sourceKey: asset.sourceKey,
876
+ sourceIndex: index,
877
+ });
878
+ }
879
+ continue;
880
+ }
701
881
  for (const [index, asset] of declaration.value.assets.entries()) {
702
882
  inventory.push({
703
883
  guid: asset.guid,
@@ -107,7 +107,10 @@
107
107
  "derivedInterface": {
108
108
  "type": "object",
109
109
  "required": ["layoutIdentity"],
110
- "properties": { "layoutIdentity": { "type": "string", "minLength": 1 } },
110
+ "properties": {
111
+ "layoutIdentity": { "type": "string", "minLength": 1 },
112
+ "layerPlanIdentity": { "type": "string", "minLength": 1 }
113
+ },
111
114
  "additionalProperties": false
112
115
  },
113
116
  "publicationGeneration": { "type": "integer", "minimum": 1 }
@@ -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,