@forgeax/engine-pack 0.1.2

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 (228) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +370 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/ai-recovery.unit.test.d.ts +2 -0
  5. package/dist/__tests__/ai-recovery.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/artifact-path.test.d.ts +2 -0
  7. package/dist/__tests__/artifact-path.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-evidence-error-hints.test.d.ts +2 -0
  9. package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +1 -0
  10. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +2 -0
  11. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +1 -0
  12. package/dist/__tests__/asset-evidence-schema.test.d.ts +2 -0
  13. package/dist/__tests__/asset-evidence-schema.test.d.ts.map +1 -0
  14. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +2 -0
  15. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +1 -0
  16. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +2 -0
  17. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +2 -0
  19. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/cli.unit.test.d.ts +2 -0
  21. package/dist/__tests__/cli.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +2 -0
  23. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/guid-collision.unit.test.d.ts +2 -0
  25. package/dist/__tests__/guid-collision.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/guid-validator-surface.unit.test.d.ts +2 -0
  27. package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/load-asset-config.test.d.ts +2 -0
  29. package/dist/__tests__/load-asset-config.test.d.ts.map +1 -0
  30. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +2 -0
  31. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/material-cook-receipt.unit.test.d.ts +2 -0
  33. package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/material-cook-schema.unit.test.d.ts +2 -0
  35. package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +2 -0
  37. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +2 -0
  39. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/meta-schema-optional.test.d.ts +2 -0
  41. package/dist/__tests__/meta-schema-optional.test.d.ts.map +1 -0
  42. package/dist/__tests__/meta-source-overrides.unit.test.d.ts +2 -0
  43. package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/native-cooker-registry.test.d.ts +2 -0
  45. package/dist/__tests__/native-cooker-registry.test.d.ts.map +1 -0
  46. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +2 -0
  47. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +1 -0
  48. package/dist/__tests__/offline-evidence.integration.test.d.ts +2 -0
  49. package/dist/__tests__/offline-evidence.integration.test.d.ts.map +1 -0
  50. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +2 -0
  51. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +1 -0
  52. package/dist/__tests__/pack-v2-schema.test.d.ts +2 -0
  53. package/dist/__tests__/pack-v2-schema.test.d.ts.map +1 -0
  54. package/dist/__tests__/pack.unit.test.d.ts +2 -0
  55. package/dist/__tests__/pack.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/producer-contract-schema.unit.test.d.ts +2 -0
  57. package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/resolve-asset-source.test.d.ts +2 -0
  59. package/dist/__tests__/resolve-asset-source.test.d.ts.map +1 -0
  60. package/dist/__tests__/runtime-projection.unit.test.d.ts +2 -0
  61. package/dist/__tests__/runtime-projection.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/scanner-inventory.test.d.ts +2 -0
  63. package/dist/__tests__/scanner-inventory.test.d.ts.map +1 -0
  64. package/dist/__tests__/scanner-mount-cycle.test.d.ts +2 -0
  65. package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +1 -0
  66. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +2 -0
  67. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +1 -0
  68. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +2 -0
  69. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/scriptable-pack.test-d.d.ts +2 -0
  71. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +1 -0
  72. package/dist/__tests__/scriptable-pack.unit.test.d.ts +2 -0
  73. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/topology-actions.unit.test.d.ts +2 -0
  75. package/dist/__tests__/topology-actions.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/topology.unit.test.d.ts +2 -0
  77. package/dist/__tests__/topology.unit.test.d.ts.map +1 -0
  78. package/dist/artifact-path.d.ts +9 -0
  79. package/dist/artifact-path.d.ts.map +1 -0
  80. package/dist/atlas/run-atlas.d.ts +8 -0
  81. package/dist/atlas/run-atlas.d.ts.map +1 -0
  82. package/dist/atlas/shelf-pack.d.ts +55 -0
  83. package/dist/atlas/shelf-pack.d.ts.map +1 -0
  84. package/dist/bridge.d.ts +2 -0
  85. package/dist/bridge.d.ts.map +1 -0
  86. package/dist/bridge.mjs +3 -0
  87. package/dist/bridge.mjs.map +1 -0
  88. package/dist/builtin.d.ts +24 -0
  89. package/dist/builtin.d.ts.map +1 -0
  90. package/dist/cli-asset.d.ts +22 -0
  91. package/dist/cli-asset.d.ts.map +1 -0
  92. package/dist/cli-asset.mjs +9950 -0
  93. package/dist/cli-asset.mjs.map +1 -0
  94. package/dist/config.d.ts +6 -0
  95. package/dist/config.d.ts.map +1 -0
  96. package/dist/config.mjs +29 -0
  97. package/dist/config.mjs.map +1 -0
  98. package/dist/deriveAssetName.d.ts +15 -0
  99. package/dist/deriveAssetName.d.ts.map +1 -0
  100. package/dist/errors.d.ts +30 -0
  101. package/dist/errors.d.ts.map +1 -0
  102. package/dist/errors.mjs +19 -0
  103. package/dist/errors.mjs.map +1 -0
  104. package/dist/evidence/material-cook.d.ts +70 -0
  105. package/dist/evidence/material-cook.d.ts.map +1 -0
  106. package/dist/evidence/offline-evidence.d.ts +25 -0
  107. package/dist/evidence/offline-evidence.d.ts.map +1 -0
  108. package/dist/evidence/source-inventory.d.ts +29 -0
  109. package/dist/evidence/source-inventory.d.ts.map +1 -0
  110. package/dist/guid.d.ts +45 -0
  111. package/dist/guid.d.ts.map +1 -0
  112. package/dist/guid.mjs +94 -0
  113. package/dist/guid.mjs.map +1 -0
  114. package/dist/index.d.ts +15 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.mjs +8475 -0
  117. package/dist/index.mjs.map +1 -0
  118. package/dist/material/artifact-writer.d.ts +14 -0
  119. package/dist/material/artifact-writer.d.ts.map +1 -0
  120. package/dist/mesh-bin-contract.d.ts +36 -0
  121. package/dist/mesh-bin-contract.d.ts.map +1 -0
  122. package/dist/name.mjs +22 -0
  123. package/dist/name.mjs.map +1 -0
  124. package/dist/native-cooker-registry.d.ts +28 -0
  125. package/dist/native-cooker-registry.d.ts.map +1 -0
  126. package/dist/native-cooker.d.ts +3 -0
  127. package/dist/native-cooker.d.ts.map +1 -0
  128. package/dist/native-cooker.mjs +105 -0
  129. package/dist/native-cooker.mjs.map +1 -0
  130. package/dist/producer-contract.d.ts +19 -0
  131. package/dist/producer-contract.d.ts.map +1 -0
  132. package/dist/resolve-asset-source.d.ts +4 -0
  133. package/dist/resolve-asset-source.d.ts.map +1 -0
  134. package/dist/resolve-asset-source.mjs +99 -0
  135. package/dist/resolve-asset-source.mjs.map +1 -0
  136. package/dist/runtime-projection.d.ts +14 -0
  137. package/dist/runtime-projection.d.ts.map +1 -0
  138. package/dist/scanner.d.ts +80 -0
  139. package/dist/scanner.d.ts.map +1 -0
  140. package/dist/scanner.mjs +9015 -0
  141. package/dist/scanner.mjs.map +1 -0
  142. package/dist/schema-compiled.d.ts +12 -0
  143. package/dist/schema-compiled.d.ts.map +1 -0
  144. package/dist/schema.mjs +7369 -0
  145. package/dist/schema.mjs.map +1 -0
  146. package/dist/scriptable-pack-node.d.ts +67 -0
  147. package/dist/scriptable-pack-node.d.ts.map +1 -0
  148. package/dist/scriptable-pack-node.mjs +1590 -0
  149. package/dist/scriptable-pack-node.mjs.map +1 -0
  150. package/dist/scriptable-pack-worker.d.ts +2 -0
  151. package/dist/scriptable-pack-worker.d.ts.map +1 -0
  152. package/dist/scriptable-pack-worker.mjs +223 -0
  153. package/dist/scriptable-pack-worker.mjs.map +1 -0
  154. package/dist/scriptable-pack.d.ts +525 -0
  155. package/dist/scriptable-pack.d.ts.map +1 -0
  156. package/dist/scriptable-pack.mjs +583 -0
  157. package/dist/scriptable-pack.mjs.map +1 -0
  158. package/dist/topology.d.ts +10 -0
  159. package/dist/topology.d.ts.map +1 -0
  160. package/package.json +129 -0
  161. package/schema/meta.schema.json +173 -0
  162. package/schema/pack.schema.json +232 -0
  163. package/src/__tests__/ai-recovery.unit.test.ts +114 -0
  164. package/src/__tests__/artifact-path.test.ts +41 -0
  165. package/src/__tests__/asset-evidence-error-hints.test.ts +46 -0
  166. package/src/__tests__/asset-evidence-lifecycle.test.ts +104 -0
  167. package/src/__tests__/asset-evidence-schema.test.ts +86 -0
  168. package/src/__tests__/atlas-cli-region-mismatch.test.ts +134 -0
  169. package/src/__tests__/atlas-error-code-owner.test-d.ts +23 -0
  170. package/src/__tests__/cli-asset-evidence.integration.test.ts +172 -0
  171. package/src/__tests__/cli.unit.test.ts +448 -0
  172. package/src/__tests__/evidence-source-inventory.unit.test.ts +37 -0
  173. package/src/__tests__/guid-collision.unit.test.ts +112 -0
  174. package/src/__tests__/guid-validator-surface.unit.test.ts +20 -0
  175. package/src/__tests__/load-asset-config.test.ts +121 -0
  176. package/src/__tests__/material-cook-dependencies.unit.test.ts +45 -0
  177. package/src/__tests__/material-cook-receipt.unit.test.ts +91 -0
  178. package/src/__tests__/material-cook-schema.unit.test.ts +98 -0
  179. package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +21 -0
  180. package/src/__tests__/mesh-bin-contract.unit.test.ts +32 -0
  181. package/src/__tests__/meta-schema-optional.test.ts +116 -0
  182. package/src/__tests__/meta-source-overrides.unit.test.ts +71 -0
  183. package/src/__tests__/native-cooker-registry.test.ts +64 -0
  184. package/src/__tests__/offline-evidence-artifact-verification-owner.test-d.ts +64 -0
  185. package/src/__tests__/offline-evidence.integration.test.ts +106 -0
  186. package/src/__tests__/pack-v2-fixture-migration.test.ts +64 -0
  187. package/src/__tests__/pack-v2-schema.test.ts +142 -0
  188. package/src/__tests__/pack.unit.test.ts +2218 -0
  189. package/src/__tests__/producer-contract-schema.unit.test.ts +92 -0
  190. package/src/__tests__/resolve-asset-source.test.ts +121 -0
  191. package/src/__tests__/runtime-projection.unit.test.ts +87 -0
  192. package/src/__tests__/scanner-inventory.test.ts +72 -0
  193. package/src/__tests__/scanner-mount-cycle.test.ts +232 -0
  194. package/src/__tests__/scriptable-pack-cli.integration.test.ts +47 -0
  195. package/src/__tests__/scriptable-pack-ddc-key.unit.test.ts +63 -0
  196. package/src/__tests__/scriptable-pack.test-d.ts +131 -0
  197. package/src/__tests__/scriptable-pack.unit.test.ts +747 -0
  198. package/src/__tests__/topology-actions.unit.test.ts +94 -0
  199. package/src/__tests__/topology.unit.test.ts +85 -0
  200. package/src/artifact-path.ts +69 -0
  201. package/src/atlas/run-atlas.ts +358 -0
  202. package/src/atlas/shelf-pack.ts +176 -0
  203. package/src/atlas/upng-ambient.d.ts +13 -0
  204. package/src/bridge.ts +2 -0
  205. package/src/builtin.ts +79 -0
  206. package/src/cli-asset.ts +590 -0
  207. package/src/config.ts +36 -0
  208. package/src/deriveAssetName.ts +42 -0
  209. package/src/errors.ts +47 -0
  210. package/src/evidence/material-cook.ts +332 -0
  211. package/src/evidence/offline-evidence.ts +90 -0
  212. package/src/evidence/source-inventory.ts +193 -0
  213. package/src/guid.ts +99 -0
  214. package/src/index.ts +104 -0
  215. package/src/material/artifact-writer.ts +26 -0
  216. package/src/mesh-bin-contract.ts +150 -0
  217. package/src/native-cooker-registry.ts +163 -0
  218. package/src/native-cooker.ts +2 -0
  219. package/src/producer-contract.ts +277 -0
  220. package/src/resolve-asset-source.ts +93 -0
  221. package/src/runtime-projection.ts +100 -0
  222. package/src/scanner.ts +716 -0
  223. package/src/schema/material-cook.schema.json +79 -0
  224. package/src/schema-compiled.ts +238 -0
  225. package/src/scriptable-pack-node.ts +1390 -0
  226. package/src/scriptable-pack-worker.ts +306 -0
  227. package/src/scriptable-pack.ts +884 -0
  228. package/src/topology.ts +156 -0
package/src/scanner.ts ADDED
@@ -0,0 +1,716 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import type { ImportedOutputDeclaration, PackErrorCode } from '@forgeax/engine-types';
4
+ import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
5
+ import { loadAssetConfig } from './config.js';
6
+ import { PackError } from './errors.js';
7
+ import { isValidAssetGuidString } from './guid.js';
8
+ import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
9
+ import { resolveAssetSource } from './resolve-asset-source.js';
10
+ import { validateMeta, validatePack } from './schema-compiled.js';
11
+ import { projectScriptablePackMeta, type ScriptablePackMetaJson } from './scriptable-pack.js';
12
+ import {
13
+ createScriptablePackModuleExecutorPool,
14
+ inventoryScriptablePackSource,
15
+ type LoadScriptablePackOptions,
16
+ loadScriptablePack,
17
+ type ScriptablePackSourceClosureEntry,
18
+ } from './scriptable-pack-node.js';
19
+
20
+ // Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
21
+ // but defined locally to avoid a heavy runtime dep in this build-time package.
22
+ export type ScanResult<T, E> =
23
+ | { readonly ok: true; readonly value: T }
24
+ | { readonly ok: false; readonly error: E };
25
+
26
+ function ok<T>(value: T): ScanResult<T, never> {
27
+ return { ok: true, value };
28
+ }
29
+
30
+ function packErr<E>(error: E): ScanResult<never, E> {
31
+ return { ok: false, error };
32
+ }
33
+
34
+ /** Host-owned source paths that should not enter the Pack catalog. */
35
+ export interface ScanOptions {
36
+ readonly ignorePath?: (path: string) => boolean;
37
+ /** Timeout/executor owner for ScriptablePack metadata loading. */
38
+ readonly scriptablePackLoad?: LoadScriptablePackOptions;
39
+ }
40
+
41
+ export type AssetInventoryProducerKind = 'direct-pack' | 'source-meta' | 'scriptable-pack';
42
+
43
+ export interface AssetInventoryOutput {
44
+ readonly guid: string;
45
+ readonly sourceKey?: string;
46
+ readonly sourceIndex?: number;
47
+ readonly kind: string;
48
+ readonly name?: string;
49
+ readonly execution?: 'direct' | 'cooked';
50
+ readonly refs: readonly string[];
51
+ }
52
+
53
+ export interface AssetInventoryProjectedMeta {
54
+ readonly schemaVersion?: string;
55
+ readonly packageId?: string;
56
+ readonly importer?: string;
57
+ readonly source?: string;
58
+ readonly name?: string;
59
+ readonly outputs: readonly AssetInventoryOutput[];
60
+ readonly externalGuids: readonly string[];
61
+ }
62
+
63
+ export interface AssetInventoryDiagnostic {
64
+ readonly code: string;
65
+ readonly message: string;
66
+ }
67
+
68
+ export interface AssetInventoryDeclaration {
69
+ readonly sourcePath: string;
70
+ readonly canonicalSourcePath: string;
71
+ readonly producer: AssetInventoryProducerKind;
72
+ 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[];
78
+ }
79
+
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>;
89
+ }
90
+
91
+ /**
92
+ * Directory names that are skipped during recursive traversal unless
93
+ * explicitly provided as a root in the `roots` parameter (whitelist override).
94
+ * Requirements §3.4 + §5 blacklist.
95
+ *
96
+ * Re-exported as `SCANNER_BLACKLIST` for cross-package reuse: the
97
+ * `forgeax-engine-remote-asset import --check` traversal (M4 / w21 +
98
+ * plan-strategy section 2.8 path b) walks the same set of source-orphan
99
+ * candidates as the scanner, so we share the single SSOT here.
100
+ */
101
+ const BLACKLIST = new Set([
102
+ 'node_modules',
103
+ '.forgeax-harness',
104
+ '.forgeax',
105
+ '.git',
106
+ 'dist',
107
+ '.forgeax-asset-cache',
108
+ 'forgeax-engine-assets',
109
+ 'coverage',
110
+ ]);
111
+
112
+ export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
113
+
114
+ function makePackError(
115
+ code: PackErrorCode,
116
+ detail: ConstructorParameters<typeof PackError>[0]['detail'],
117
+ ): PackError {
118
+ return new PackError({
119
+ code,
120
+ expected: `pack error: ${code}`,
121
+ hint: PACK_ERROR_HINTS[code],
122
+ detail,
123
+ });
124
+ }
125
+
126
+ /**
127
+ * For scene assets with `payload.mounts[]`, return the lowercased GUID
128
+ * each `mount.source` integer resolves to via `asset.refs[]`. Returns an
129
+ * empty iterable for non-scene assets, scene assets without mounts, or
130
+ * mounts with malformed `source` (out-of-range integer / non-integer) —
131
+ * those are caught by ajv schema validation upstream. The yielded GUIDs
132
+ * feed scanner step-6's mount-asset cycle DFS (D-1, R10).
133
+ */
134
+ function* extractMountSourceGuids(asset: {
135
+ kind?: unknown;
136
+ payload?: unknown;
137
+ refs: readonly string[];
138
+ }): Generator<string> {
139
+ if (asset.kind !== 'scene') return;
140
+ const payload = asset.payload as { mounts?: unknown } | undefined;
141
+ if (!payload || !Array.isArray(payload.mounts)) return;
142
+ for (const rawMount of payload.mounts) {
143
+ const mount = rawMount as { source?: unknown };
144
+ const idx = mount.source;
145
+ if (typeof idx !== 'number' || !Number.isInteger(idx)) continue;
146
+ if (idx < 0 || idx >= asset.refs.length) continue;
147
+ const resolved = asset.refs[idx];
148
+ if (typeof resolved !== 'string') continue;
149
+ yield resolved.toLowerCase();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
155
+ * 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)
157
+ * Step 2 - schema validation (ajv strict)
158
+ * Step 3 - GUID string format validation
159
+ * Step 4 - GUID collision detection
160
+ * Step 5 - orphan .meta.json detection
161
+ * Step 6 - cyclic reference detection (hand-written DFS)
162
+ * Step 7 - complete pack and source closure validation
163
+ *
164
+ * Returns one reusable `AssetInventory` or `Err(PackError)` on the first violation.
165
+ *
166
+ * NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
167
+ */
168
+ export async function scanInventory(
169
+ roots: readonly string[],
170
+ 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.
175
+ const rawPaths: string[] = [];
176
+ const explicitRootSet = new Set(roots);
177
+
178
+ async function traverse(dir: string): Promise<void> {
179
+ let entries: import('node:fs').Dirent[];
180
+ try {
181
+ entries = (await readdir(dir, { withFileTypes: true })) as import('node:fs').Dirent[];
182
+ } catch {
183
+ return;
184
+ }
185
+
186
+ for (const entry of entries) {
187
+ const fullPath = join(dir, entry.name);
188
+
189
+ if (opts.ignorePath?.(fullPath) === true) continue;
190
+
191
+ if (entry.isDirectory()) {
192
+ // Skip blacklisted subdirectories unless the subdir is itself an explicit root
193
+ if (BLACKLIST.has(basename(fullPath)) && !explicitRootSet.has(fullPath)) {
194
+ continue;
195
+ }
196
+ await traverse(fullPath);
197
+ } else if (entry.isFile()) {
198
+ const name = entry.name;
199
+ if (
200
+ name.endsWith('.meta.json') ||
201
+ name.endsWith('.pack.json') ||
202
+ name.endsWith('.pack.ts')
203
+ ) {
204
+ rawPaths.push(fullPath);
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ for (const root of roots) {
211
+ // Explicit file roots are useful when a host wants one source package
212
+ // from a larger asset tree without also scanning sibling packages. Keep
213
+ // directory-root behaviour unchanged; this is only an opt-in whitelist.
214
+ try {
215
+ const rootStat = await stat(root);
216
+ if (rootStat.isFile()) {
217
+ if (opts.ignorePath?.(root) === true) continue;
218
+ if (
219
+ root.endsWith('.meta.json') ||
220
+ root.endsWith('.pack.json') ||
221
+ root.endsWith('.pack.ts')
222
+ ) {
223
+ rawPaths.push(root);
224
+ }
225
+ continue;
226
+ }
227
+ } catch {
228
+ // The existing directory traversal treats missing roots as empty.
229
+ }
230
+ await traverse(root);
231
+ }
232
+
233
+ // Separate meta and pack paths
234
+ const metaPaths = rawPaths.filter((p) => p.endsWith('.meta.json'));
235
+ const packPaths = rawPaths.filter((p) => p.endsWith('.pack.json'));
236
+ const scriptablePaths = rawPaths.filter((p) => p.endsWith('.pack.ts'));
237
+
238
+ // Step 2 + 3: parse + schema validate + GUID format validate each pack file
239
+ // One normalized GUID map covers pack assets and meta subAssets. The source
240
+ // kind stays in the path/detail evidence; identity is the normalized GUID.
241
+ const guidToPath = new Map<string, string>();
242
+ 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
+
247
+ 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
+ }
273
+
274
+ const packageContract = validateProducerContract(parsed);
275
+ if (!packageContract.ok) {
276
+ return packErr(
277
+ makePackError('pack-malformed-pack', {
278
+ path: packPath,
279
+ ajvErrors: [{ instancePath: '', message: packageContract.error.code }],
280
+ }),
281
+ );
282
+ }
283
+
284
+ // 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
+ };
299
+ for (const asset of packObj.assets) {
300
+ if (
301
+ asset.kind === 'particle-effect' &&
302
+ asset.execution === 'direct' &&
303
+ (asset.refs.length > 0 || Object.keys(asset.artifacts ?? {}).length > 0)
304
+ ) {
305
+ return packErr(
306
+ makePackError('pack-malformed-pack', {
307
+ path: packPath,
308
+ ajvErrors: [
309
+ {
310
+ instancePath: '/assets',
311
+ message:
312
+ 'authored particle-effect assets are source-only; refs and artifacts must be empty',
313
+ },
314
+ ],
315
+ }),
316
+ );
317
+ }
318
+ }
319
+ const producerAssets = packObj.assets.filter(
320
+ (asset) => asset.sourceKey !== undefined || asset.sourceIndex !== undefined,
321
+ );
322
+ if (producerAssets.length > 0) {
323
+ for (const asset of producerAssets) {
324
+ const assetContract = validateProducerContract(asset);
325
+ if (!assetContract.ok) {
326
+ return packErr(
327
+ makePackError('pack-malformed-pack', {
328
+ path: packPath,
329
+ ajvErrors: [{ instancePath: '/assets', message: assetContract.error.code }],
330
+ }),
331
+ );
332
+ }
333
+ }
334
+ const topologyContract = validateProducerOutputs(
335
+ producerAssets as unknown as readonly ImportedOutputDeclaration[],
336
+ );
337
+ if (!topologyContract.ok) {
338
+ return packErr(
339
+ makePackError('pack-malformed-pack', {
340
+ path: packPath,
341
+ ajvErrors: [{ instancePath: '/assets', message: topologyContract.error.code }],
342
+ }),
343
+ );
344
+ }
345
+ }
346
+ for (const asset of packObj.assets) {
347
+ if (!isValidAssetGuidString(asset.guid)) {
348
+ return packErr(
349
+ makePackError('pack-guid-malformed', {
350
+ raw: asset.guid,
351
+ reason: 'expected 36-char RFC 4122 dash-form UUID',
352
+ }),
353
+ );
354
+ }
355
+ for (const ref of asset.refs) {
356
+ if (!isValidAssetGuidString(ref)) {
357
+ return packErr(
358
+ makePackError('pack-guid-malformed', {
359
+ raw: ref,
360
+ reason: 'expected 36-char RFC 4122 dash-form UUID in refs[]',
361
+ }),
362
+ );
363
+ }
364
+ }
365
+
366
+ // Step 4: collision check
367
+ const normalizedGuid = asset.guid.toLowerCase();
368
+ const existing = guidToPath.get(normalizedGuid);
369
+ if (existing !== undefined) {
370
+ return packErr(
371
+ makePackError('pack-guid-collision', {
372
+ paths: [existing, packPath],
373
+ guid: normalizedGuid,
374
+ }),
375
+ );
376
+ }
377
+ guidToPath.set(normalizedGuid, packPath);
378
+
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
+ // feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
386
+ // mount-payload-extract — for scene assets, redundantly inject the
387
+ // mount.source -> resolved GUID edge into the cycle graph alongside
388
+ // asset.refs[]. By the .pack.json convention mount.source is an
389
+ // integer index into the same asset.refs[], so the resolved GUID is
390
+ // already present in `existingRefs`; this defensive pass guarantees
391
+ // that any author-supplied mounts[] references participate in the
392
+ // cycle DFS even if the schema-emitter forgot to mirror them into
393
+ // refs[]. The `kind: 'mount-asset'` tag on the resulting
394
+ // pack-cyclic-reference detail is set by the cycle producer below
395
+ // (R10).
396
+ for (const guid of extractMountSourceGuids(asset)) {
397
+ existingRefs.push(guid);
398
+ }
399
+ packRefs.set(normalizedGuid, existingRefs);
400
+ }
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({
411
+ 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: [],
424
+ });
425
+ }
426
+
427
+ // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
428
+ const { paths: assetPaths } = loadAssetConfig(process.cwd());
429
+ 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
+ }
455
+
456
+ const metaContract = validateProducerContract(parsed);
457
+ if (!metaContract.ok) {
458
+ return packErr(
459
+ makePackError('pack-malformed-meta', {
460
+ path: metaPath,
461
+ ajvErrors: [{ instancePath: '', message: metaContract.error.code }],
462
+ }),
463
+ );
464
+ }
465
+
466
+ // 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
+ };
484
+ for (const sub of metaObj.subAssets) {
485
+ if (!isValidAssetGuidString(sub.guid)) {
486
+ return packErr(
487
+ makePackError('pack-guid-malformed', {
488
+ raw: sub.guid,
489
+ reason: 'expected 36-char RFC 4122 dash-form UUID in subAssets[].guid',
490
+ }),
491
+ );
492
+ }
493
+ const normalizedGuid = sub.guid.toLowerCase();
494
+ const existing = guidToPath.get(normalizedGuid);
495
+ if (existing !== undefined) {
496
+ return packErr(
497
+ makePackError('pack-guid-collision', {
498
+ paths: [existing, metaPath],
499
+ guid: normalizedGuid,
500
+ }),
501
+ );
502
+ }
503
+ guidToPath.set(normalizedGuid, metaPath);
504
+ metaByGuid.set(normalizedGuid, metaPath);
505
+ }
506
+ const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
507
+ if (producerSubAssets.length > 0) {
508
+ const topologyContract = validateProducerOutputs(
509
+ producerSubAssets as unknown as readonly ImportedOutputDeclaration[],
510
+ );
511
+ if (!topologyContract.ok) {
512
+ return packErr(
513
+ makePackError('pack-malformed-meta', {
514
+ path: metaPath,
515
+ ajvErrors: [{ instancePath: '/subAssets', message: topologyContract.error.code }],
516
+ }),
517
+ );
518
+ }
519
+ }
520
+
521
+ // Step 5: orphan .meta.json check — the source file declared in meta.source must exist.
522
+ // Uses resolveAssetSource for three-mode dispatch (undefined derivation / @name/ path table /
523
+ // relative path), replacing the former hardcoded join(metaDir, metaObj.source).
524
+ const sourceResolved = resolveAssetSource(metaPath, metaObj.source, assetPaths);
525
+ if (!sourceResolved.ok) {
526
+ return packErr(sourceResolved.error);
527
+ }
528
+ const expectedSourcePath = sourceResolved.value;
529
+ try {
530
+ await stat(expectedSourcePath);
531
+ } catch {
532
+ return packErr(
533
+ makePackError('pack-orphan-meta', {
534
+ metaPath,
535
+ expectedFile: expectedSourcePath,
536
+ }),
537
+ );
538
+ }
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
+ }
568
+
569
+ // 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[],
605
+ );
606
+ if (!topology.ok) {
607
+ return packErr(
608
+ makePackError('pack-malformed-meta', {
609
+ path: sourcePath,
610
+ ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
611
+ }),
612
+ );
613
+ }
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
+ });
652
+ }
653
+ } finally {
654
+ await scriptablePackExecutorPool?.dispose();
655
+ }
656
+
657
+ // Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
658
+ // visited: nodes fully processed; recStack: nodes in current DFS path
659
+ const visited = new Set<string>();
660
+ const recStack = new Set<string>();
661
+
662
+ function dfs(guid: string, path: string[]): string[] | null {
663
+ visited.add(guid);
664
+ recStack.add(guid);
665
+
666
+ for (const ref of packRefs.get(guid) ?? []) {
667
+ if (!visited.has(ref)) {
668
+ const cycle = dfs(ref, [...path, ref]);
669
+ if (cycle !== null) return cycle;
670
+ } else if (recStack.has(ref)) {
671
+ // Found a back-edge: reconstruct cycle from the repeated node
672
+ const cycleStart = path.indexOf(ref);
673
+ return cycleStart >= 0 ? [...path.slice(cycleStart), ref] : [...path, ref];
674
+ }
675
+ }
676
+
677
+ recStack.delete(guid);
678
+ return null;
679
+ }
680
+
681
+ for (const guid of guidToPath.keys()) {
682
+ if (!visited.has(guid)) {
683
+ const cycle = dfs(guid, [guid]);
684
+ if (cycle !== null) {
685
+ return packErr(
686
+ makePackError('pack-cyclic-reference', {
687
+ code: 'pack-cyclic-reference',
688
+ kind: 'mount-asset',
689
+ cycle,
690
+ }),
691
+ );
692
+ }
693
+ }
694
+ }
695
+
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
+ });
707
+ }
708
+
709
+ /** Compatibility projection for callers that only need discovered paths. */
710
+ export async function scan(
711
+ roots: readonly string[],
712
+ opts: ScanOptions = {},
713
+ ): Promise<ScanResult<string[], PackError>> {
714
+ const inventory = await scanInventory(roots, opts);
715
+ return inventory.ok ? ok([...inventory.value.paths]) : inventory;
716
+ }