@forgeax/engine-pack 0.1.21 → 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 (124) 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 +3314 -803
  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/source-inventory.d.ts.map +1 -1
  25. package/dist/guid.d.ts +15 -0
  26. package/dist/guid.d.ts.map +1 -1
  27. package/dist/guid.mjs +53 -5
  28. package/dist/guid.mjs.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.mjs +1252 -560
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/inventory/binding.d.ts +6 -0
  34. package/dist/inventory/binding.d.ts.map +1 -0
  35. package/dist/inventory/declaration.d.ts +24 -0
  36. package/dist/inventory/declaration.d.ts.map +1 -0
  37. package/dist/inventory/index.d.ts +3 -0
  38. package/dist/inventory/index.d.ts.map +1 -0
  39. package/dist/inventory/sync.d.ts +5 -0
  40. package/dist/inventory/sync.d.ts.map +1 -0
  41. package/dist/name.mjs +4 -7
  42. package/dist/name.mjs.map +1 -1
  43. package/dist/package-finalizer.d.ts +2 -20
  44. package/dist/package-finalizer.d.ts.map +1 -1
  45. package/dist/parameterized-pack-node.d.ts +26 -0
  46. package/dist/parameterized-pack-node.d.ts.map +1 -0
  47. package/dist/parameterized-pack-node.mjs +11093 -0
  48. package/dist/parameterized-pack-node.mjs.map +1 -0
  49. package/dist/parameterized-pack.d.ts +233 -0
  50. package/dist/parameterized-pack.d.ts.map +1 -0
  51. package/dist/resolve-asset-source.d.ts +1 -3
  52. package/dist/resolve-asset-source.d.ts.map +1 -1
  53. package/dist/resolve-asset-source.mjs +6 -81
  54. package/dist/resolve-asset-source.mjs.map +1 -1
  55. package/dist/runtime.mjs +77 -7
  56. package/dist/runtime.mjs.map +1 -1
  57. package/dist/scanner.d.ts +19 -8
  58. package/dist/scanner.d.ts.map +1 -1
  59. package/dist/scanner.mjs +1169 -690
  60. package/dist/scanner.mjs.map +1 -1
  61. package/dist/schema-compiled.d.ts.map +1 -1
  62. package/dist/schema.mjs +79 -7
  63. package/dist/schema.mjs.map +1 -1
  64. package/dist/scriptable-pack-node.d.ts +8 -32
  65. package/dist/scriptable-pack-node.d.ts.map +1 -1
  66. package/dist/scriptable-pack-node.mjs +576 -1021
  67. package/dist/scriptable-pack-node.mjs.map +1 -1
  68. package/dist/scriptable-pack-worker.mjs +29 -11
  69. package/dist/scriptable-pack-worker.mjs.map +1 -1
  70. package/dist/scriptable-pack.d.ts +19 -532
  71. package/dist/scriptable-pack.d.ts.map +1 -1
  72. package/dist/scriptable-pack.mjs +1042 -527
  73. package/dist/scriptable-pack.mjs.map +1 -1
  74. package/package.json +7 -7
  75. package/schema/meta.schema.json +1 -1
  76. package/schema/pack.schema.json +76 -6
  77. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  78. package/src/__tests__/inventory-schema.test.ts +109 -0
  79. package/src/__tests__/pack.unit.test.ts +1 -238
  80. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  81. package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
  82. package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
  83. package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
  84. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  85. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  86. package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
  87. package/src/__tests__/scanner-inventory.test.ts +24 -12
  88. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  89. package/src/__tests__/topology.unit.test.ts +23 -0
  90. package/src/build.ts +28 -18
  91. package/src/catalog-builder.ts +61 -34
  92. package/src/cli-asset.ts +70 -85
  93. package/src/deriveAssetName.ts +14 -13
  94. package/src/evidence/source-inventory.ts +34 -21
  95. package/src/guid.ts +65 -4
  96. package/src/index.ts +12 -11
  97. package/src/inventory/binding.ts +25 -0
  98. package/src/inventory/declaration.ts +168 -0
  99. package/src/inventory/index.ts +18 -0
  100. package/src/inventory/sync.ts +22 -0
  101. package/src/package-finalizer.ts +2 -42
  102. package/src/parameterized-pack-node.ts +1968 -0
  103. package/src/parameterized-pack.ts +1493 -0
  104. package/src/resolve-asset-source.ts +4 -76
  105. package/src/scanner.ts +241 -59
  106. package/src/schema-compiled.ts +2 -0
  107. package/src/scriptable-pack-node.ts +111 -722
  108. package/src/scriptable-pack-worker.ts +48 -16
  109. package/src/scriptable-pack.ts +27 -939
  110. package/dist/.tsbuildinfo +0 -1
  111. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  112. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  113. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  114. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  115. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  116. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  117. package/dist/config.d.ts +0 -6
  118. package/dist/config.d.ts.map +0 -1
  119. package/dist/config.mjs +0 -29
  120. package/dist/config.mjs.map +0 -1
  121. package/src/__tests__/load-asset-config.test.ts +0 -121
  122. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  123. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  124. package/src/config.ts +0 -36
@@ -2,9 +2,8 @@ import { createHash } from 'node:crypto';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
4
4
  import type { SourceDeclarationEvidence } from '@forgeax/engine-types';
5
+ import { parseParameterizedPackJson, projectDirectPackJson } from '../parameterized-pack.js';
5
6
  import { SCANNER_BLACKLIST, scan } from '../scanner.js';
6
- import { projectScriptablePackMeta } from '../scriptable-pack.js';
7
- import { loadScriptablePack } from '../scriptable-pack-node.js';
8
7
 
9
8
  /** Project root and GUID used to locate an authoritative source declaration. */
10
9
  export interface SourceInventoryRequest {
@@ -150,34 +149,43 @@ export async function readSourceInventory(
150
149
  let authored: SourceDeclarationEvidence | undefined;
151
150
  for (const path of paths) {
152
151
  if (path.endsWith('.pack.ts')) {
153
- const loaded = await loadScriptablePack(path, { metadataOnly: true });
154
- if (!loaded.ok) continue;
155
- const meta = projectScriptablePackMeta(loaded.value, path);
156
- if (meta.subAssets.some((asset) => asset.guid.toLowerCase() === guid)) {
157
- return { origin: 'sourceMeta', sourcePath: path };
158
- }
152
+ // Parameterized source identity is a build result, not a declaration
153
+ // table. The source index therefore records the source only after the
154
+ // producer publishes its materialized Pack output.
159
155
  continue;
160
156
  }
161
157
  const raw = await readFile(path, 'utf8').catch(() => undefined);
162
158
  if (raw === undefined) continue;
163
- let parsed: {
164
- readonly assets?: readonly { readonly guid?: unknown }[];
165
- readonly source?: unknown;
166
- readonly inputFingerprint?: unknown;
167
- readonly importSettings?: unknown;
168
- readonly subAssets?: readonly { readonly guid?: unknown }[];
169
- };
159
+ let parsed: Record<string, unknown>;
170
160
  try {
171
- parsed = JSON.parse(raw) as typeof parsed;
161
+ const value: unknown = JSON.parse(raw);
162
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) continue;
163
+ parsed = value as Record<string, unknown>;
172
164
  } catch {
173
165
  continue;
174
166
  }
175
167
  if (path.endsWith('.pack.json')) {
176
- if (
177
- parsed.assets?.some(
168
+ if (parsed.schemaVersion === '3.0.0') {
169
+ const direct = parseParameterizedPackJson(parsed);
170
+ if (direct.ok && direct.value.format === 'direct') {
171
+ const projected = projectDirectPackJson(direct.value);
172
+ if (
173
+ projected.ok &&
174
+ projected.value.assets.some((asset) => asset.guid.toLowerCase() === guid)
175
+ ) {
176
+ authored = { origin: 'authoredPack', sourcePath: path };
177
+ }
178
+ }
179
+ } else if (
180
+ Array.isArray(parsed.assets) &&
181
+ parsed.assets.some(
178
182
  (asset) =>
179
- typeof asset.guid === 'string' &&
180
- (asset.guid === request.guid || asset.guid.toLowerCase() === guid),
183
+ asset !== null &&
184
+ typeof asset === 'object' &&
185
+ !Array.isArray(asset) &&
186
+ typeof (asset as { readonly guid?: unknown }).guid === 'string' &&
187
+ ((asset as { readonly guid: string }).guid === request.guid ||
188
+ (asset as { readonly guid: string }).guid.toLowerCase() === guid),
181
189
  )
182
190
  ) {
183
191
  authored = { origin: 'authoredPack', sourcePath: path };
@@ -185,7 +193,12 @@ export async function readSourceInventory(
185
193
  continue;
186
194
  }
187
195
  if (path.endsWith('.meta.json')) {
188
- const source = sourceFromMeta(path, parsed, request.guid, raw);
196
+ const source = sourceFromMeta(
197
+ path,
198
+ parsed as Parameters<typeof sourceFromMeta>[1],
199
+ request.guid,
200
+ raw,
201
+ );
189
202
  if (source !== undefined) return source;
190
203
  }
191
204
  }
package/src/guid.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { sha1 } from '@noble/hashes/legacy.js';
1
2
  import { uuidv7obj } from 'uuidv7';
2
3
  import { PackError } from './errors.js';
3
4
 
@@ -8,6 +9,9 @@ import { PackError } from './errors.js';
8
9
  */
9
10
  export type AssetGuid = Uint8Array & { readonly __guidBrand: 'AssetGuid' };
10
11
 
12
+ /** Pack-level identity. It is intentionally not assignable to AssetGuid. */
13
+ export type PackageId = Uint8Array & { readonly __packageIdBrand: 'PackageId' };
14
+
11
15
  /** Minimal Result alias for this module (structurally compatible with ScanResult). */
12
16
  export type GuidResult<T, E> =
13
17
  | { readonly ok: true; readonly value: T }
@@ -27,6 +31,13 @@ function bytesToDashForm(bytes: Uint8Array): string {
27
31
 
28
32
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
33
 
34
+ /** Stable author-owned output identity. Paths and payloads are not identity inputs. */
35
+ export const PACK_SOURCE_KEY_RE = /^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*$/;
36
+
37
+ export function isValidPackSourceKey(value: unknown): value is string {
38
+ return typeof value === 'string' && PACK_SOURCE_KEY_RE.test(value);
39
+ }
40
+
30
41
  export function isValidAssetGuidString(value: unknown): value is string {
31
42
  return typeof value === 'string' && UUID_RE.test(value);
32
43
  }
@@ -40,6 +51,54 @@ function dashFormToBytes(dashForm: string): Uint8Array {
40
51
  return bytes;
41
52
  }
42
53
 
54
+ function randomUuidBytes(): Uint8Array {
55
+ const uuid = uuidv7obj();
56
+ const bytes = new Uint8Array(16);
57
+ bytes.set(uuid.bytes);
58
+ return bytes;
59
+ }
60
+
61
+ function packageId(value: Uint8Array): PackageId {
62
+ return value as PackageId;
63
+ }
64
+
65
+ /** Utilities for the pack-level UUID identity. */
66
+ export const PackageId = {
67
+ parse(dashForm: string): GuidResult<PackageId, PackError> {
68
+ const parsed = AssetGuid.parse(dashForm);
69
+ return parsed.ok ? { ok: true, value: packageId(parsed.value) } : parsed;
70
+ },
71
+ format(value: PackageId): string {
72
+ return bytesToDashForm(value);
73
+ },
74
+ random(): PackageId {
75
+ return packageId(randomUuidBytes());
76
+ },
77
+ } as const;
78
+
79
+ function derivedGuid(namespace: PackageId, sourceKey: string): AssetGuid {
80
+ if (!(namespace instanceof Uint8Array) || namespace.byteLength !== 16) {
81
+ throw new TypeError('AssetGuid.derive requires a 16-byte PackageId');
82
+ }
83
+ if (!isValidPackSourceKey(sourceKey)) {
84
+ const error = new TypeError(
85
+ `AssetGuid.derive received invalid sourceKey ${JSON.stringify(sourceKey)}`,
86
+ ) as TypeError & { readonly code?: string };
87
+ Object.defineProperty(error, 'code', { value: 'pack-source-key-invalid' });
88
+ throw error;
89
+ }
90
+ const name = new TextEncoder().encode(sourceKey);
91
+ const input = new Uint8Array(namespace.byteLength + name.byteLength);
92
+ input.set(namespace, 0);
93
+ input.set(name, namespace.byteLength);
94
+ const digest = sha1(input);
95
+ const result = digest.slice(0, 16);
96
+ // RFC 4122 §4.1.1 / §4.1.3: UUID version 5, RFC variant.
97
+ result[6] = ((result[6] ?? 0) & 0x0f) | 0x50;
98
+ result[8] = ((result[8] ?? 0) & 0x3f) | 0x80;
99
+ return brand(result);
100
+ }
101
+
43
102
  /**
44
103
  * Utilities for the AssetGuid branded 16-byte UUID type.
45
104
  * Declare the type via `import type { AssetGuid } from '@forgeax/engine-pack/guid'`
@@ -91,9 +150,11 @@ export const AssetGuid = {
91
150
  * Works in both Node.js and browser environments.
92
151
  */
93
152
  random(): AssetGuid {
94
- const uuid = uuidv7obj();
95
- const bytes = new Uint8Array(16);
96
- bytes.set(uuid.bytes);
97
- return brand(bytes);
153
+ return brand(randomUuidBytes());
154
+ },
155
+
156
+ /** Derive the stable UUIDv5 projection for one Pack subject and sourceKey. */
157
+ derive(namespace: PackageId, sourceKey: string): AssetGuid {
158
+ return derivedGuid(namespace, sourceKey);
98
159
  },
99
160
  } as const;
package/src/index.ts CHANGED
@@ -22,6 +22,13 @@ export {
22
22
  validateMaterialCookReceipt,
23
23
  } from './evidence/material-cook.js';
24
24
  export { buildOfflineAssetEvidence, packageVerification } from './evidence/offline-evidence.js';
25
+ export {
26
+ AssetGuid,
27
+ isValidAssetGuidString,
28
+ isValidPackSourceKey,
29
+ PACK_SOURCE_KEY_RE,
30
+ PackageId,
31
+ } from './guid.js';
25
32
  export {
26
33
  decodeMeshBinHeader,
27
34
  MESH_BIN_DIGEST_BYTES,
@@ -33,24 +40,18 @@ export {
33
40
  type MeshBinHeaderV4,
34
41
  writeMeshBinHeader,
35
42
  } from './mesh-bin-contract.js';
43
+ export * from './parameterized-pack.js';
36
44
  export { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
37
45
  export {
38
46
  type AssetReader,
39
47
  isScriptablePackAssetKind,
40
- projectScriptablePackMeta,
48
+ projectScriptablePackSceneComponents,
41
49
  SCRIPTABLE_PACK_ASSET_KINDS,
42
- type ScriptablePackAssetDeclaration,
43
- type ScriptablePackAssetDeclarations,
44
- type ScriptablePackAssetFor,
45
50
  type ScriptablePackAssetKind,
46
- type ScriptablePackDefinition,
47
- type ScriptablePackError,
48
- type ScriptablePackExternalAssets,
49
- type ScriptablePackMetaJson,
50
- type ScriptablePackOutputs,
51
- type ScriptablePackPublicationEnvelope,
52
51
  type ScriptablePackReadError,
53
- validateScriptablePackDefinition,
52
+ type ScriptablePackSceneComponent,
53
+ type ScriptablePackSceneComponentInput,
54
+ type ScriptablePackSourceClosureEntry,
54
55
  } from './scriptable-pack.js';
55
56
  export { calculateTopologyDiff, diffTopology } from './topology.js';
56
57
 
@@ -0,0 +1,25 @@
1
+ import type { AssetRef, SceneEntityRef } from '@forgeax/engine-types';
2
+ import type { AuthorInventory } from './declaration.js';
3
+
4
+ export function projectAssetRefs(inventory: AuthorInventory): readonly AssetRef[] {
5
+ return inventory.declarations.map(
6
+ (row) =>
7
+ ({
8
+ guid: row.guid,
9
+ sourceKey: row.sourceKey,
10
+ kind: row.kind,
11
+ }) as AssetRef,
12
+ );
13
+ }
14
+
15
+ /** Derive instance-relative scene refs from the same validated author rows. */
16
+ export function projectSceneEntityRefs(inventory: AuthorInventory): readonly SceneEntityRef[] {
17
+ return inventory.declarations.flatMap((row) =>
18
+ row.sceneBindings === undefined
19
+ ? []
20
+ : row.sceneBindings.map((bindingKey) => ({
21
+ sceneSourceKey: row.sourceKey,
22
+ bindingKey,
23
+ })),
24
+ );
25
+ }
@@ -0,0 +1,168 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+
3
+ export interface AuthorInventoryRow {
4
+ readonly guid: string;
5
+ readonly sourceKey: string;
6
+ readonly kind: string;
7
+ readonly payload: Readonly<Record<string, unknown>>;
8
+ readonly refs: readonly string[];
9
+ readonly sceneBindings?: readonly string[];
10
+ }
11
+
12
+ export interface AuthorInventory {
13
+ readonly declarations: readonly AuthorInventoryRow[];
14
+ }
15
+
16
+ export type InventoryErrorCode =
17
+ | 'inventory-source-key-missing'
18
+ | 'inventory-source-key-duplicate'
19
+ | 'inventory-guid-duplicate'
20
+ | 'inventory-scene-binding-missing'
21
+ | 'inventory-scene-binding-duplicate';
22
+
23
+ export type InventoryError = {
24
+ readonly code: InventoryErrorCode;
25
+ readonly expected: string;
26
+ readonly hint: string;
27
+ readonly detail: {
28
+ readonly guid?: string;
29
+ readonly sourceKey?: string;
30
+ };
31
+ };
32
+
33
+ function failure(
34
+ code: InventoryErrorCode,
35
+ expected: string,
36
+ hint: string,
37
+ detail: InventoryError['detail'],
38
+ ): Result<never, InventoryError> {
39
+ return err({ code, expected, hint, detail });
40
+ }
41
+
42
+ export function validateAuthorInventory(value: unknown): Result<AuthorInventory, InventoryError> {
43
+ if (
44
+ typeof value !== 'object' ||
45
+ value === null ||
46
+ !Array.isArray((value as { readonly declarations?: unknown }).declarations)
47
+ ) {
48
+ return failure(
49
+ 'inventory-source-key-missing',
50
+ 'an author inventory with declaration rows',
51
+ 'declare each author asset in the source inventory',
52
+ {},
53
+ );
54
+ }
55
+
56
+ const guids = new Set<string>();
57
+ const sourceKeys = new Set<string>();
58
+ const declarations: AuthorInventoryRow[] = [];
59
+ const rows = (value as { readonly declarations: readonly unknown[] }).declarations;
60
+ for (const raw of rows) {
61
+ if (typeof raw !== 'object' || raw === null) {
62
+ return failure(
63
+ 'inventory-source-key-missing',
64
+ 'each declaration has a non-empty sourceKey',
65
+ 'add sourceKey to the author declaration before projection',
66
+ {},
67
+ );
68
+ }
69
+ const row = raw as Record<string, unknown>;
70
+ const guid = typeof row.guid === 'string' ? row.guid : undefined;
71
+ const sourceKey = typeof row.sourceKey === 'string' ? row.sourceKey : undefined;
72
+ if (
73
+ guid === undefined ||
74
+ sourceKey === undefined ||
75
+ sourceKey.trim().length === 0 ||
76
+ sourceKey !== sourceKey.trim()
77
+ ) {
78
+ return failure(
79
+ 'inventory-source-key-missing',
80
+ 'each declaration has a non-empty sourceKey',
81
+ 'add sourceKey to the author declaration before projection',
82
+ {
83
+ ...(guid === undefined ? {} : { guid }),
84
+ ...(sourceKey === undefined ? {} : { sourceKey }),
85
+ },
86
+ );
87
+ }
88
+ const normalizedGuid = guid.toLowerCase();
89
+ if (guids.has(normalizedGuid)) {
90
+ return failure(
91
+ 'inventory-guid-duplicate',
92
+ 'every GUID identifies exactly one author declaration',
93
+ 'remove or rename the duplicate GUID before publishing the inventory',
94
+ { guid, sourceKey },
95
+ );
96
+ }
97
+ if (sourceKeys.has(sourceKey)) {
98
+ return failure(
99
+ 'inventory-source-key-duplicate',
100
+ 'every sourceKey identifies exactly one author declaration',
101
+ 'rename the duplicate semantic sourceKey before publishing the inventory',
102
+ { guid, sourceKey },
103
+ );
104
+ }
105
+ if (row.kind === 'scene') {
106
+ const payload = row.payload;
107
+ const payloadRecord =
108
+ typeof payload === 'object' && payload !== null
109
+ ? (payload as Record<string, unknown>)
110
+ : undefined;
111
+ const entities =
112
+ payloadRecord !== undefined && Array.isArray(payloadRecord.entities)
113
+ ? payloadRecord.entities
114
+ : [];
115
+ const bindings = new Set<string>();
116
+ for (const entity of entities) {
117
+ const bindingKey =
118
+ typeof entity === 'object' && entity !== null && typeof entity.bindingKey === 'string'
119
+ ? entity.bindingKey
120
+ : undefined;
121
+ if (bindingKey !== undefined && bindingKey.length === 0) {
122
+ return failure(
123
+ 'inventory-scene-binding-missing',
124
+ 'each declared scene bindingKey is non-empty',
125
+ 'remove the empty bindingKey or declare a stable key in the scene producer',
126
+ { sourceKey },
127
+ );
128
+ }
129
+ if (bindingKey === undefined) continue;
130
+ if (bindings.has(bindingKey)) {
131
+ return failure(
132
+ 'inventory-scene-binding-duplicate',
133
+ 'bindingKey values are unique within one scene',
134
+ 'rename the duplicate bindingKey in the scene producer',
135
+ { sourceKey },
136
+ );
137
+ }
138
+ bindings.add(bindingKey);
139
+ }
140
+ }
141
+ guids.add(normalizedGuid);
142
+ sourceKeys.add(sourceKey);
143
+ const sceneBindings =
144
+ row.kind === 'scene' && typeof row.payload === 'object' && row.payload !== null
145
+ ? (row.payload as { readonly entities?: readonly unknown[] }).entities?.flatMap(
146
+ (entity) => {
147
+ if (typeof entity !== 'object' || entity === null) return [];
148
+ const key = (entity as { readonly bindingKey?: unknown }).bindingKey;
149
+ return typeof key === 'string' && key.length > 0 ? [key] : [];
150
+ },
151
+ )
152
+ : undefined;
153
+ declarations.push({
154
+ guid,
155
+ sourceKey,
156
+ kind: typeof row.kind === 'string' ? row.kind : 'unknown',
157
+ payload:
158
+ typeof row.payload === 'object' && row.payload !== null
159
+ ? (row.payload as Readonly<Record<string, unknown>>)
160
+ : {},
161
+ refs: Array.isArray(row.refs)
162
+ ? row.refs.filter((ref): ref is string => typeof ref === 'string')
163
+ : [],
164
+ ...(sceneBindings === undefined ? {} : { sceneBindings }),
165
+ });
166
+ }
167
+ return ok({ declarations });
168
+ }
@@ -0,0 +1,18 @@
1
+ export { validateProducerContract, validateProducerOutputs } from '../producer-contract.js';
2
+ export {
3
+ type InventoryDeclaration,
4
+ type MetaInventoryDocument,
5
+ type MetaInventorySubAsset,
6
+ type PackInventoryAsset,
7
+ type PackInventoryDocument,
8
+ SCANNER_BLACKLIST,
9
+ type ScanInventory,
10
+ type ScanOptions,
11
+ type ScanResult,
12
+ type ScanSourceDeclaration,
13
+ type ScriptablePackInventoryDeclaration,
14
+ type ScriptablePackScanOptions,
15
+ STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS,
16
+ scan,
17
+ scanInventory,
18
+ } from '../scanner.js';
@@ -0,0 +1,22 @@
1
+ import type { AuthorInventory } from './declaration.js';
2
+
3
+ export type InventoryDigest = string;
4
+
5
+ export function inventoryDigest(inventory: AuthorInventory): InventoryDigest {
6
+ return inventory.declarations
7
+ .map(
8
+ (row) => `${row.guid}\0${row.sourceKey}\0${row.kind}\0${row.sceneBindings?.join('\0') ?? ''}`,
9
+ )
10
+ .sort()
11
+ .join('\n');
12
+ }
13
+
14
+ export function syncAuthorInventory(inventory: AuthorInventory): AuthorInventory {
15
+ return {
16
+ declarations: inventory.declarations.map((row) => ({
17
+ ...row,
18
+ refs: [...row.refs],
19
+ ...(row.sceneBindings === undefined ? {} : { sceneBindings: [...row.sceneBindings] }),
20
+ })),
21
+ };
22
+ }
@@ -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
  };