@forgeax/engine-pack 0.0.0-dev.8d955ade1c79

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 (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +429 -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__/owner-chain.integration.test.d.ts +2 -0
  51. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  52. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +2 -0
  53. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +1 -0
  54. package/dist/__tests__/pack-v2-schema.test.d.ts +2 -0
  55. package/dist/__tests__/pack-v2-schema.test.d.ts.map +1 -0
  56. package/dist/__tests__/pack.unit.test.d.ts +2 -0
  57. package/dist/__tests__/pack.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/package-finalizer.contract.test.d.ts +2 -0
  59. package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
  60. package/dist/__tests__/producer-contract-schema.unit.test.d.ts +2 -0
  61. package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/resolve-asset-source.test.d.ts +2 -0
  63. package/dist/__tests__/resolve-asset-source.test.d.ts.map +1 -0
  64. package/dist/__tests__/runtime-projection.unit.test.d.ts +2 -0
  65. package/dist/__tests__/runtime-projection.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/runtime.browser.test.d.ts +2 -0
  67. package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
  68. package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
  69. package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
  70. package/dist/__tests__/scanner-inventory.test.d.ts +2 -0
  71. package/dist/__tests__/scanner-inventory.test.d.ts.map +1 -0
  72. package/dist/__tests__/scanner-mount-cycle.test.d.ts +2 -0
  73. package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +1 -0
  74. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +2 -0
  75. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +1 -0
  76. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +2 -0
  77. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/scriptable-pack.test-d.d.ts +2 -0
  79. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +1 -0
  80. package/dist/__tests__/scriptable-pack.unit.test.d.ts +2 -0
  81. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/topology-actions.unit.test.d.ts +2 -0
  83. package/dist/__tests__/topology-actions.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/topology.unit.test.d.ts +2 -0
  85. package/dist/__tests__/topology.unit.test.d.ts.map +1 -0
  86. package/dist/artifact-path.d.ts +9 -0
  87. package/dist/artifact-path.d.ts.map +1 -0
  88. package/dist/artifact-path.mjs +53 -0
  89. package/dist/artifact-path.mjs.map +1 -0
  90. package/dist/atlas/run-atlas.d.ts +8 -0
  91. package/dist/atlas/run-atlas.d.ts.map +1 -0
  92. package/dist/atlas/shelf-pack.d.ts +55 -0
  93. package/dist/atlas/shelf-pack.d.ts.map +1 -0
  94. package/dist/bridge.d.ts +2 -0
  95. package/dist/bridge.d.ts.map +1 -0
  96. package/dist/bridge.mjs +3 -0
  97. package/dist/bridge.mjs.map +1 -0
  98. package/dist/build.d.ts +19 -0
  99. package/dist/build.d.ts.map +1 -0
  100. package/dist/build.mjs +10203 -0
  101. package/dist/build.mjs.map +1 -0
  102. package/dist/builtin.d.ts +44 -0
  103. package/dist/builtin.d.ts.map +1 -0
  104. package/dist/builtin.mjs +141 -0
  105. package/dist/builtin.mjs.map +1 -0
  106. package/dist/catalog-builder.d.ts +43 -0
  107. package/dist/catalog-builder.d.ts.map +1 -0
  108. package/dist/catalog-delta.d.ts +11 -0
  109. package/dist/catalog-delta.d.ts.map +1 -0
  110. package/dist/catalog-projection.d.ts +91 -0
  111. package/dist/catalog-projection.d.ts.map +1 -0
  112. package/dist/cli-asset.d.ts +22 -0
  113. package/dist/cli-asset.d.ts.map +1 -0
  114. package/dist/cli-asset.mjs +9697 -0
  115. package/dist/cli-asset.mjs.map +1 -0
  116. package/dist/config.d.ts +6 -0
  117. package/dist/config.d.ts.map +1 -0
  118. package/dist/config.mjs +29 -0
  119. package/dist/config.mjs.map +1 -0
  120. package/dist/deriveAssetName.d.ts +15 -0
  121. package/dist/deriveAssetName.d.ts.map +1 -0
  122. package/dist/errors.d.ts +30 -0
  123. package/dist/errors.d.ts.map +1 -0
  124. package/dist/errors.mjs +19 -0
  125. package/dist/errors.mjs.map +1 -0
  126. package/dist/evidence/material-cook.d.ts +92 -0
  127. package/dist/evidence/material-cook.d.ts.map +1 -0
  128. package/dist/evidence/offline-evidence.d.ts +25 -0
  129. package/dist/evidence/offline-evidence.d.ts.map +1 -0
  130. package/dist/evidence/source-inventory.d.ts +29 -0
  131. package/dist/evidence/source-inventory.d.ts.map +1 -0
  132. package/dist/guid.d.ts +45 -0
  133. package/dist/guid.d.ts.map +1 -0
  134. package/dist/guid.mjs +94 -0
  135. package/dist/guid.mjs.map +1 -0
  136. package/dist/index.d.ts +15 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.mjs +8612 -0
  139. package/dist/index.mjs.map +1 -0
  140. package/dist/material/artifact-writer.d.ts +14 -0
  141. package/dist/material/artifact-writer.d.ts.map +1 -0
  142. package/dist/material-cook.d.ts +2 -0
  143. package/dist/material-cook.d.ts.map +1 -0
  144. package/dist/material-cook.mjs +248 -0
  145. package/dist/material-cook.mjs.map +1 -0
  146. package/dist/mesh-bin-contract.d.ts +36 -0
  147. package/dist/mesh-bin-contract.d.ts.map +1 -0
  148. package/dist/mesh-bin-contract.mjs +96 -0
  149. package/dist/mesh-bin-contract.mjs.map +1 -0
  150. package/dist/name.mjs +22 -0
  151. package/dist/name.mjs.map +1 -0
  152. package/dist/native-cooker-registry.d.ts +47 -0
  153. package/dist/native-cooker-registry.d.ts.map +1 -0
  154. package/dist/native-cooker.d.ts +3 -0
  155. package/dist/native-cooker.d.ts.map +1 -0
  156. package/dist/native-cooker.mjs +142 -0
  157. package/dist/native-cooker.mjs.map +1 -0
  158. package/dist/package-finalizer.d.ts +121 -0
  159. package/dist/package-finalizer.d.ts.map +1 -0
  160. package/dist/producer-contract.d.ts +19 -0
  161. package/dist/producer-contract.d.ts.map +1 -0
  162. package/dist/resolve-asset-source.d.ts +4 -0
  163. package/dist/resolve-asset-source.d.ts.map +1 -0
  164. package/dist/resolve-asset-source.mjs +109 -0
  165. package/dist/resolve-asset-source.mjs.map +1 -0
  166. package/dist/runtime-projection.d.ts +14 -0
  167. package/dist/runtime-projection.d.ts.map +1 -0
  168. package/dist/runtime-publication.d.ts +44 -0
  169. package/dist/runtime-publication.d.ts.map +1 -0
  170. package/dist/runtime.d.ts +8 -0
  171. package/dist/runtime.d.ts.map +1 -0
  172. package/dist/runtime.mjs +7261 -0
  173. package/dist/runtime.mjs.map +1 -0
  174. package/dist/scanner.d.ts +113 -0
  175. package/dist/scanner.d.ts.map +1 -0
  176. package/dist/scanner.mjs +8819 -0
  177. package/dist/scanner.mjs.map +1 -0
  178. package/dist/schema-compiled.d.ts +12 -0
  179. package/dist/schema-compiled.d.ts.map +1 -0
  180. package/dist/schema.mjs +7369 -0
  181. package/dist/schema.mjs.map +1 -0
  182. package/dist/scriptable-pack-node.d.ts +62 -0
  183. package/dist/scriptable-pack-node.d.ts.map +1 -0
  184. package/dist/scriptable-pack-node.mjs +1674 -0
  185. package/dist/scriptable-pack-node.mjs.map +1 -0
  186. package/dist/scriptable-pack-worker.d.ts +2 -0
  187. package/dist/scriptable-pack-worker.d.ts.map +1 -0
  188. package/dist/scriptable-pack-worker.mjs +224 -0
  189. package/dist/scriptable-pack-worker.mjs.map +1 -0
  190. package/dist/scriptable-pack.d.ts +558 -0
  191. package/dist/scriptable-pack.d.ts.map +1 -0
  192. package/dist/scriptable-pack.mjs +672 -0
  193. package/dist/scriptable-pack.mjs.map +1 -0
  194. package/dist/topology.d.ts +10 -0
  195. package/dist/topology.d.ts.map +1 -0
  196. package/package.json +154 -0
  197. package/schema/meta.schema.json +173 -0
  198. package/schema/pack.schema.json +232 -0
  199. package/src/__tests__/ai-recovery.unit.test.ts +114 -0
  200. package/src/__tests__/artifact-path.test.ts +41 -0
  201. package/src/__tests__/asset-evidence-error-hints.test.ts +46 -0
  202. package/src/__tests__/asset-evidence-lifecycle.test.ts +104 -0
  203. package/src/__tests__/asset-evidence-schema.test.ts +86 -0
  204. package/src/__tests__/atlas-cli-region-mismatch.test.ts +134 -0
  205. package/src/__tests__/atlas-error-code-owner.test-d.ts +23 -0
  206. package/src/__tests__/cli-asset-evidence.integration.test.ts +172 -0
  207. package/src/__tests__/cli.unit.test.ts +448 -0
  208. package/src/__tests__/evidence-source-inventory.unit.test.ts +37 -0
  209. package/src/__tests__/guid-collision.unit.test.ts +112 -0
  210. package/src/__tests__/guid-validator-surface.unit.test.ts +20 -0
  211. package/src/__tests__/load-asset-config.test.ts +121 -0
  212. package/src/__tests__/material-cook-dependencies.unit.test.ts +112 -0
  213. package/src/__tests__/material-cook-receipt.unit.test.ts +148 -0
  214. package/src/__tests__/material-cook-schema.unit.test.ts +152 -0
  215. package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +22 -0
  216. package/src/__tests__/mesh-bin-contract.unit.test.ts +32 -0
  217. package/src/__tests__/meta-schema-optional.test.ts +116 -0
  218. package/src/__tests__/meta-source-overrides.unit.test.ts +71 -0
  219. package/src/__tests__/native-cooker-registry.test.ts +64 -0
  220. package/src/__tests__/offline-evidence-artifact-verification-owner.test-d.ts +64 -0
  221. package/src/__tests__/offline-evidence.integration.test.ts +106 -0
  222. package/src/__tests__/owner-chain.integration.test.ts +78 -0
  223. package/src/__tests__/pack-v2-fixture-migration.test.ts +87 -0
  224. package/src/__tests__/pack-v2-schema.test.ts +142 -0
  225. package/src/__tests__/pack.unit.test.ts +2218 -0
  226. package/src/__tests__/package-finalizer.contract.test.ts +105 -0
  227. package/src/__tests__/producer-contract-schema.unit.test.ts +92 -0
  228. package/src/__tests__/resolve-asset-source.test.ts +121 -0
  229. package/src/__tests__/runtime-projection.unit.test.ts +87 -0
  230. package/src/__tests__/runtime.browser.test.ts +15 -0
  231. package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
  232. package/src/__tests__/scanner-inventory.test.ts +76 -0
  233. package/src/__tests__/scanner-mount-cycle.test.ts +232 -0
  234. package/src/__tests__/scriptable-pack-cli.integration.test.ts +47 -0
  235. package/src/__tests__/scriptable-pack-ddc-key.unit.test.ts +63 -0
  236. package/src/__tests__/scriptable-pack.test-d.ts +131 -0
  237. package/src/__tests__/scriptable-pack.unit.test.ts +789 -0
  238. package/src/__tests__/topology-actions.unit.test.ts +94 -0
  239. package/src/__tests__/topology.unit.test.ts +85 -0
  240. package/src/artifact-path.ts +69 -0
  241. package/src/atlas/run-atlas.ts +358 -0
  242. package/src/atlas/shelf-pack.ts +172 -0
  243. package/src/atlas/upng-ambient.d.ts +13 -0
  244. package/src/bridge.ts +2 -0
  245. package/src/build.ts +139 -0
  246. package/src/builtin.ts +117 -0
  247. package/src/catalog-builder.ts +313 -0
  248. package/src/catalog-delta.ts +189 -0
  249. package/src/catalog-projection.ts +344 -0
  250. package/src/cli-asset.ts +586 -0
  251. package/src/config.ts +36 -0
  252. package/src/deriveAssetName.ts +42 -0
  253. package/src/errors.ts +47 -0
  254. package/src/evidence/material-cook.ts +411 -0
  255. package/src/evidence/offline-evidence.ts +90 -0
  256. package/src/evidence/source-inventory.ts +193 -0
  257. package/src/guid.ts +99 -0
  258. package/src/index.ts +108 -0
  259. package/src/material/artifact-writer.ts +26 -0
  260. package/src/material-cook.ts +15 -0
  261. package/src/mesh-bin-contract.ts +150 -0
  262. package/src/native-cooker-registry.ts +235 -0
  263. package/src/native-cooker.ts +2 -0
  264. package/src/package-finalizer.ts +394 -0
  265. package/src/producer-contract.ts +277 -0
  266. package/src/resolve-asset-source.ts +88 -0
  267. package/src/runtime-projection.ts +100 -0
  268. package/src/runtime-publication.ts +190 -0
  269. package/src/runtime.ts +23 -0
  270. package/src/scanner.ts +715 -0
  271. package/src/schema/material-cook.schema.json +119 -0
  272. package/src/schema-compiled.ts +238 -0
  273. package/src/scriptable-pack-node.ts +1394 -0
  274. package/src/scriptable-pack-worker.ts +308 -0
  275. package/src/scriptable-pack.ts +1006 -0
  276. package/src/topology.ts +157 -0
package/src/scanner.ts ADDED
@@ -0,0 +1,715 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
+ import { basename, join } from 'node:path';
4
+ import type {
5
+ CatalogDiagnostic,
6
+ PackErrorCode,
7
+ ProviderProvenance,
8
+ ResourceRevision,
9
+ SourceOverrideDescriptor,
10
+ } from '@forgeax/engine-types';
11
+ import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
12
+ import { loadAssetConfig } from './config.js';
13
+ import { PackError } from './errors.js';
14
+ import { isValidAssetGuidString } from './guid.js';
15
+ import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
16
+ import { resolveAssetSource } from './resolve-asset-source.js';
17
+ 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 {
25
+ inventoryScriptablePackSource,
26
+ loadScriptablePack,
27
+ type ScriptablePackModuleExecutor,
28
+ } from './scriptable-pack-node.js';
29
+
30
+ // Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
31
+ // but defined locally to avoid a heavy runtime dep in this build-time package.
32
+ export type ScanResult<T, E> =
33
+ | { readonly ok: true; readonly value: T }
34
+ | { readonly ok: false; readonly error: E };
35
+
36
+ function ok<T>(value: T): ScanResult<T, never> {
37
+ return { ok: true, value };
38
+ }
39
+
40
+ function packErr<E>(error: E): ScanResult<never, E> {
41
+ return { ok: false, error };
42
+ }
43
+
44
+ /** Host-owned source paths that should not enter the Pack catalog. */
45
+ export interface ScanOptions {
46
+ readonly ignorePath?: (path: string) => boolean;
47
+ readonly scriptablePack?: ScriptablePackScanOptions;
48
+ }
49
+
50
+ /** One bounded executor policy shared by ScriptablePack inventory and production. */
51
+ export interface ScriptablePackScanOptions {
52
+ readonly timeoutMs?: number;
53
+ readonly buildTimeoutMs?: number;
54
+ readonly executor?: ScriptablePackModuleExecutor;
55
+ /** Path-only scans must release the isolated loader before returning. */
56
+ readonly metadataOnly?: boolean;
57
+ }
58
+
59
+ /** Stable default policy shared by inventory and production owners. */
60
+ export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze(
61
+ {},
62
+ ) satisfies ScriptablePackScanOptions;
63
+
64
+ export interface InventoryDeclaration {
65
+ readonly guid: string;
66
+ readonly kind: string;
67
+ readonly sourcePath: string;
68
+ readonly sourceRevision: string;
69
+ readonly sourceKey?: string;
70
+ readonly sourceIndex?: number;
71
+ }
72
+
73
+ export interface ScanInventory {
74
+ readonly paths: readonly string[];
75
+ readonly inventory: readonly InventoryDeclaration[];
76
+ /** Complete parsed source declarations captured by the validated scan pass. */
77
+ readonly declarations: ReadonlyMap<string, ScanSourceDeclaration>;
78
+ }
79
+
80
+ export interface ScriptablePackInventoryDeclaration {
81
+ readonly sourcePath: string;
82
+ readonly sourceRevision: string;
83
+ readonly meta: ScriptablePackMetaJson;
84
+ readonly definition: Readonly<ScriptablePackDefinition>;
85
+ readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
86
+ }
87
+
88
+ export interface MetaInventoryDocument {
89
+ readonly schemaVersion: string | number;
90
+ readonly kind: 'external-asset-package';
91
+ readonly packageId?: string;
92
+ readonly name?: string;
93
+ readonly provenance?: ProviderProvenance;
94
+ readonly revision?: ResourceRevision;
95
+ readonly diagnostics?: readonly CatalogDiagnostic[];
96
+ readonly importer: string;
97
+ readonly source?: string;
98
+ readonly importSettings: Readonly<Record<string, unknown>>;
99
+ readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
100
+ readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];
101
+ readonly paramSchema?: readonly Readonly<Record<string, unknown>>[];
102
+ readonly subAssets: readonly MetaInventorySubAsset[];
103
+ }
104
+
105
+ export interface MetaInventorySubAsset {
106
+ readonly guid: string;
107
+ readonly sourceIndex: number;
108
+ readonly sourceKey?: string;
109
+ readonly name?: string;
110
+ readonly kind: string;
111
+ }
112
+
113
+ export interface PackInventoryDocument {
114
+ readonly schemaVersion: '1.0.0' | '2.0.0';
115
+ readonly kind: 'internal-text-package';
116
+ readonly packageId?: string;
117
+ readonly provenance?: ProviderProvenance;
118
+ readonly revision?: ResourceRevision;
119
+ readonly diagnostics?: readonly CatalogDiagnostic[];
120
+ readonly assets: readonly PackInventoryAsset[];
121
+ }
122
+
123
+ export interface PackInventoryAsset {
124
+ readonly guid: string;
125
+ readonly kind: string;
126
+ readonly name?: string;
127
+ readonly execution?: 'direct' | 'cooked';
128
+ readonly sourceKey?: string;
129
+ readonly sourceIndex?: number;
130
+ readonly payload: Readonly<Record<string, unknown>>;
131
+ readonly refs: readonly string[];
132
+ readonly artifacts?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
133
+ }
134
+
135
+ export type ScanSourceDeclaration =
136
+ | {
137
+ readonly format: 'meta.json';
138
+ readonly sourcePath: string;
139
+ readonly sourceRevision: string;
140
+ readonly value: MetaInventoryDocument;
141
+ }
142
+ | {
143
+ readonly format: 'pack.json';
144
+ readonly sourcePath: string;
145
+ readonly sourceRevision: string;
146
+ readonly sourceText: string;
147
+ readonly value: PackInventoryDocument;
148
+ }
149
+ | ({
150
+ readonly format: 'pack.ts';
151
+ readonly sourcePath: string;
152
+ readonly sourceRevision: string;
153
+ readonly value: ScriptablePackMetaJson;
154
+ } & ScriptablePackInventoryDeclaration);
155
+
156
+ interface ScanCapture {
157
+ readonly declarations: Map<string, ScanSourceDeclaration>;
158
+ }
159
+
160
+ /**
161
+ * Directory names that are skipped during recursive traversal unless
162
+ * explicitly provided as a root in the `roots` parameter (whitelist override).
163
+ * Requirements §3.4 + §5 blacklist.
164
+ *
165
+ * Re-exported as `SCANNER_BLACKLIST` for cross-package reuse: the
166
+ * `forgeax-engine-remote-asset import --check` traversal (M4 / w21 +
167
+ * plan-strategy section 2.8 path b) walks the same set of source-orphan
168
+ * candidates as the scanner, so we share the single SSOT here.
169
+ */
170
+ const BLACKLIST = new Set([
171
+ 'node_modules',
172
+ '.forgeax-harness',
173
+ '.forgeax',
174
+ '.git',
175
+ 'dist',
176
+ '.forgeax-asset-cache',
177
+ 'forgeax-engine-assets',
178
+ 'coverage',
179
+ ]);
180
+
181
+ export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
182
+
183
+ type MalformedFileCode = Extract<PackErrorCode, 'pack-malformed-pack' | 'pack-malformed-meta'>;
184
+
185
+ type JsonValidator = {
186
+ (value: unknown): boolean;
187
+ errors?: readonly { readonly instancePath?: string; readonly message?: string }[] | null;
188
+ };
189
+
190
+ async function readValidatedJson(
191
+ path: string,
192
+ code: MalformedFileCode,
193
+ validate: JsonValidator,
194
+ ): Promise<ScanResult<{ readonly raw: string; readonly parsed: unknown }, PackError>> {
195
+ let raw: string;
196
+ let parsed: unknown;
197
+ try {
198
+ raw = await readFile(path, 'utf-8');
199
+ parsed = JSON.parse(raw);
200
+ } catch {
201
+ return packErr(
202
+ makePackError(code, {
203
+ path,
204
+ ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
205
+ }),
206
+ );
207
+ }
208
+ if (!validate(parsed)) {
209
+ return packErr(
210
+ makePackError(code, {
211
+ path,
212
+ ajvErrors: (validate.errors ?? []).map((error) => ({
213
+ instancePath: error.instancePath ?? '',
214
+ message: error.message ?? 'unknown ajv error',
215
+ })),
216
+ }),
217
+ );
218
+ }
219
+ return ok({ raw, parsed });
220
+ }
221
+
222
+ function makePackError(
223
+ code: PackErrorCode,
224
+ detail: ConstructorParameters<typeof PackError>[0]['detail'],
225
+ ): PackError {
226
+ return new PackError({
227
+ code,
228
+ expected: `pack error: ${code}`,
229
+ hint: PACK_ERROR_HINTS[code],
230
+ detail,
231
+ });
232
+ }
233
+
234
+ /**
235
+ * For scene assets with `payload.mounts[]`, return the lowercased GUID
236
+ * each `mount.source` integer resolves to via `asset.refs[]`. Returns an
237
+ * empty iterable for non-scene assets, scene assets without mounts, or
238
+ * mounts with malformed `source` (out-of-range integer / non-integer) —
239
+ * those are caught by ajv schema validation upstream. The yielded GUIDs
240
+ * feed scanner step-6's mount-asset cycle DFS (D-1, R10).
241
+ */
242
+ function* extractMountSourceGuids(asset: {
243
+ kind?: unknown;
244
+ payload?: unknown;
245
+ refs: readonly string[];
246
+ }): Generator<string> {
247
+ if (asset.kind !== 'scene') return;
248
+ const payload = asset.payload as { mounts?: unknown } | undefined;
249
+ if (!payload || !Array.isArray(payload.mounts)) return;
250
+ for (const rawMount of payload.mounts) {
251
+ const mount = rawMount as { source?: unknown };
252
+ const idx = mount.source;
253
+ if (typeof idx !== 'number' || !Number.isInteger(idx)) continue;
254
+ if (idx < 0 || idx >= asset.refs.length) continue;
255
+ const resolved = asset.refs[idx];
256
+ if (typeof resolved !== 'string') continue;
257
+ yield resolved.toLowerCase();
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
263
+ * Runs a 7-step fail-fast validation chain (w17 + M7-T01):
264
+ * Step 1 - collect all .meta.json + .pack.json paths (blacklist skipped)
265
+ * Step 2 - schema validation (ajv strict)
266
+ * Step 3 - GUID string format validation
267
+ * Step 4 - GUID collision detection
268
+ * Step 5 - orphan .meta.json detection
269
+ * Step 6 - cyclic reference detection (hand-written DFS)
270
+ * Step 7 - complete pack and source closure validation
271
+ *
272
+ * Returns `Ok(paths)` or `Err(PackError)` on the first violation.
273
+ *
274
+ * NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
275
+ */
276
+ async function scanValidated(
277
+ roots: readonly string[],
278
+ opts: ScanOptions = {},
279
+ capture?: ScanCapture,
280
+ ): Promise<ScanResult<string[], PackError>> {
281
+ // Step 1: collect all authored package declarations. ScriptablePack runtime
282
+ // validation belongs to its trusted module loader; scanner only inventories
283
+ // the source path so CLI/Vite share one discovery set.
284
+ const rawPaths: string[] = [];
285
+ const explicitRootSet = new Set(roots);
286
+
287
+ async function traverse(dir: string): Promise<void> {
288
+ let entries: import('node:fs').Dirent[];
289
+ try {
290
+ entries = (await readdir(dir, { withFileTypes: true })) as import('node:fs').Dirent[];
291
+ } catch {
292
+ return;
293
+ }
294
+
295
+ for (const entry of entries) {
296
+ const fullPath = join(dir, entry.name);
297
+
298
+ if (opts.ignorePath?.(fullPath) === true) continue;
299
+
300
+ if (entry.isDirectory()) {
301
+ // Skip blacklisted subdirectories unless the subdir is itself an explicit root
302
+ if (BLACKLIST.has(basename(fullPath)) && !explicitRootSet.has(fullPath)) {
303
+ continue;
304
+ }
305
+ await traverse(fullPath);
306
+ } else if (entry.isFile()) {
307
+ const name = entry.name;
308
+ if (
309
+ name.endsWith('.meta.json') ||
310
+ name.endsWith('.pack.json') ||
311
+ name.endsWith('.pack.ts')
312
+ ) {
313
+ rawPaths.push(fullPath);
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ for (const root of roots) {
320
+ // Explicit file roots are useful when a host wants one source package
321
+ // from a larger asset tree without also scanning sibling packages. Keep
322
+ // directory-root behaviour unchanged; this is only an opt-in whitelist.
323
+ try {
324
+ const rootStat = await stat(root);
325
+ if (rootStat.isFile()) {
326
+ if (opts.ignorePath?.(root) === true) continue;
327
+ if (
328
+ root.endsWith('.meta.json') ||
329
+ root.endsWith('.pack.json') ||
330
+ root.endsWith('.pack.ts')
331
+ ) {
332
+ rawPaths.push(root);
333
+ }
334
+ continue;
335
+ }
336
+ } catch {
337
+ // The existing directory traversal treats missing roots as empty.
338
+ }
339
+ await traverse(root);
340
+ }
341
+
342
+ // Separate meta and pack paths
343
+ const metaPaths = rawPaths.filter((p) => p.endsWith('.meta.json'));
344
+ const packPaths = rawPaths.filter((p) => p.endsWith('.pack.json'));
345
+ const scriptablePaths = rawPaths.filter((p) => p.endsWith('.pack.ts'));
346
+
347
+ // Step 2 + 3: parse + schema validate + GUID format validate each pack file
348
+ // One normalized GUID map covers pack assets and meta subAssets. The source
349
+ // kind stays in the path/detail evidence; identity is the normalized GUID.
350
+ const guidToPath = new Map<string, string>();
351
+ const packRefs = new Map<string, string[]>(); // guid -> refs[]
352
+
353
+ for (const packPath of packPaths) {
354
+ const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
355
+ if (!loaded.ok) return loaded;
356
+ const { raw, parsed } = loaded.value;
357
+
358
+ const packageContract = validateProducerContract(parsed);
359
+ if (!packageContract.ok) {
360
+ return packErr(
361
+ makePackError('pack-malformed-pack', {
362
+ path: packPath,
363
+ ajvErrors: [{ instancePath: '', message: packageContract.error.code }],
364
+ }),
365
+ );
366
+ }
367
+
368
+ // Step 3: validate GUIDs in pack
369
+ const packObj = parsed as unknown as PackInventoryDocument;
370
+ for (const asset of packObj.assets) {
371
+ if (
372
+ asset.kind === 'particle-effect' &&
373
+ asset.execution === 'direct' &&
374
+ (asset.refs.length > 0 || Object.keys(asset.artifacts ?? {}).length > 0)
375
+ ) {
376
+ return packErr(
377
+ makePackError('pack-malformed-pack', {
378
+ path: packPath,
379
+ ajvErrors: [
380
+ {
381
+ instancePath: '/assets',
382
+ message:
383
+ 'authored particle-effect assets are source-only; refs and artifacts must be empty',
384
+ },
385
+ ],
386
+ }),
387
+ );
388
+ }
389
+ }
390
+ const producerAssets = packObj.assets.filter(
391
+ (asset) => asset.sourceKey !== undefined || asset.sourceIndex !== undefined,
392
+ );
393
+ if (producerAssets.length > 0) {
394
+ for (const asset of producerAssets) {
395
+ const assetContract = validateProducerContract(asset);
396
+ if (!assetContract.ok) {
397
+ return packErr(
398
+ makePackError('pack-malformed-pack', {
399
+ path: packPath,
400
+ ajvErrors: [{ instancePath: '/assets', message: assetContract.error.code }],
401
+ }),
402
+ );
403
+ }
404
+ }
405
+ const topologyContract = validateProducerOutputs(
406
+ producerAssets.map((asset, sourceIndex) => ({
407
+ guid: asset.guid,
408
+ kind: asset.kind,
409
+ sourceIndex: asset.sourceIndex ?? sourceIndex,
410
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
411
+ })),
412
+ );
413
+ if (!topologyContract.ok) {
414
+ return packErr(
415
+ makePackError('pack-malformed-pack', {
416
+ path: packPath,
417
+ ajvErrors: [{ instancePath: '/assets', message: topologyContract.error.code }],
418
+ }),
419
+ );
420
+ }
421
+ }
422
+ for (const asset of packObj.assets) {
423
+ if (!isValidAssetGuidString(asset.guid)) {
424
+ return packErr(
425
+ makePackError('pack-guid-malformed', {
426
+ raw: asset.guid,
427
+ reason: 'expected 36-char RFC 4122 dash-form UUID',
428
+ }),
429
+ );
430
+ }
431
+ for (const ref of asset.refs) {
432
+ if (!isValidAssetGuidString(ref)) {
433
+ return packErr(
434
+ makePackError('pack-guid-malformed', {
435
+ raw: ref,
436
+ reason: 'expected 36-char RFC 4122 dash-form UUID in refs[]',
437
+ }),
438
+ );
439
+ }
440
+ }
441
+
442
+ // Step 4: collision check
443
+ const normalizedGuid = asset.guid.toLowerCase();
444
+ const existing = guidToPath.get(normalizedGuid);
445
+ if (existing !== undefined) {
446
+ return packErr(
447
+ makePackError('pack-guid-collision', {
448
+ paths: [existing, packPath],
449
+ guid: normalizedGuid,
450
+ }),
451
+ );
452
+ }
453
+ guidToPath.set(normalizedGuid, packPath);
454
+
455
+ // feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
456
+ // mount-payload-extract — for scene assets, redundantly inject the
457
+ // mount.source -> resolved GUID edge into the cycle graph alongside
458
+ // asset.refs[]. By the .pack.json convention mount.source is an
459
+ // integer index into the same asset.refs[], so the resolved GUID is
460
+ // already present in `existingRefs`; this defensive pass guarantees
461
+ // that any author-supplied mounts[] references participate in the
462
+ // cycle DFS even if the schema-emitter forgot to mirror them into
463
+ // refs[]. The `kind: 'mount-asset'` tag on the resulting
464
+ // pack-cyclic-reference detail is set by the cycle producer below
465
+ // (R10).
466
+ packRefs.set(normalizedGuid, [
467
+ ...asset.refs.map((ref) => ref.toLowerCase()),
468
+ ...extractMountSourceGuids(asset),
469
+ ]);
470
+ }
471
+ capture?.declarations.set(packPath, {
472
+ format: 'pack.json',
473
+ sourcePath: packPath,
474
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
475
+ sourceText: raw,
476
+ value: packObj,
477
+ });
478
+ }
479
+
480
+ // Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
481
+ const { paths: assetPaths } = loadAssetConfig(process.cwd());
482
+ for (const metaPath of metaPaths) {
483
+ const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
484
+ if (!loaded.ok) return loaded;
485
+ const { raw, parsed } = loaded.value;
486
+
487
+ const metaContract = validateProducerContract(parsed);
488
+ if (!metaContract.ok) {
489
+ return packErr(
490
+ makePackError('pack-malformed-meta', {
491
+ path: metaPath,
492
+ ajvErrors: [{ instancePath: '', message: metaContract.error.code }],
493
+ }),
494
+ );
495
+ }
496
+
497
+ // Step 3: validate GUIDs in meta subAssets
498
+ const metaObj = parsed as unknown as MetaInventoryDocument;
499
+ capture?.declarations.set(metaPath, {
500
+ format: 'meta.json',
501
+ sourcePath: metaPath,
502
+ sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
503
+ value: metaObj,
504
+ });
505
+ for (const sub of metaObj.subAssets) {
506
+ if (!isValidAssetGuidString(sub.guid)) {
507
+ return packErr(
508
+ makePackError('pack-guid-malformed', {
509
+ raw: sub.guid,
510
+ reason: 'expected 36-char RFC 4122 dash-form UUID in subAssets[].guid',
511
+ }),
512
+ );
513
+ }
514
+ const normalizedGuid = sub.guid.toLowerCase();
515
+ const existing = guidToPath.get(normalizedGuid);
516
+ if (existing !== undefined) {
517
+ return packErr(
518
+ makePackError('pack-guid-collision', {
519
+ paths: [existing, metaPath],
520
+ guid: normalizedGuid,
521
+ }),
522
+ );
523
+ }
524
+ guidToPath.set(normalizedGuid, metaPath);
525
+ }
526
+ const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
527
+ if (producerSubAssets.length > 0) {
528
+ const topologyContract = validateProducerOutputs(producerSubAssets);
529
+ if (!topologyContract.ok) {
530
+ return packErr(
531
+ makePackError('pack-malformed-meta', {
532
+ path: metaPath,
533
+ ajvErrors: [{ instancePath: '/subAssets', message: topologyContract.error.code }],
534
+ }),
535
+ );
536
+ }
537
+ }
538
+
539
+ // 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;
547
+ try {
548
+ await stat(expectedSourcePath);
549
+ } catch {
550
+ return packErr(
551
+ makePackError('pack-orphan-meta', {
552
+ metaPath,
553
+ expectedFile: expectedSourcePath,
554
+ }),
555
+ );
556
+ }
557
+ }
558
+
559
+ // ScriptablePack identity enters the same topology and collision authority as JSON declarations.
560
+ for (const sourcePath of scriptablePaths) {
561
+ let source: string;
562
+ try {
563
+ source = await readFile(sourcePath, 'utf8');
564
+ } catch {
565
+ return packErr(
566
+ makePackError('pack-malformed-meta', {
567
+ path: sourcePath,
568
+ ajvErrors: [{ instancePath: '', message: 'ScriptablePack source read failed' }],
569
+ }),
570
+ );
571
+ }
572
+ const loaded = await loadScriptablePack(sourcePath, {
573
+ ...(opts.scriptablePack ?? {}),
574
+ ...(capture === undefined ? { metadataOnly: true } : {}),
575
+ });
576
+ if (!loaded.ok) {
577
+ const diagnostic =
578
+ 'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
579
+ return packErr(
580
+ makePackError('pack-malformed-meta', {
581
+ path: sourcePath,
582
+ ajvErrors: [
583
+ {
584
+ instancePath: '',
585
+ message:
586
+ diagnostic === undefined
587
+ ? loaded.error.code
588
+ : `${loaded.error.code}: ${diagnostic}`,
589
+ },
590
+ ],
591
+ }),
592
+ );
593
+ }
594
+ let sourceClosure: readonly ScriptablePackSourceClosureEntry[];
595
+ try {
596
+ sourceClosure = await inventoryScriptablePackSource(sourcePath, source);
597
+ } catch {
598
+ return packErr(
599
+ makePackError('pack-malformed-meta', {
600
+ path: sourcePath,
601
+ ajvErrors: [{ instancePath: '', message: 'ScriptablePack source closure read failed' }],
602
+ }),
603
+ );
604
+ }
605
+ const meta = projectScriptablePackMeta(loaded.value, sourcePath);
606
+ const topology = validateProducerOutputs(meta.subAssets);
607
+ if (!topology.ok) {
608
+ return packErr(
609
+ makePackError('pack-malformed-meta', {
610
+ path: sourcePath,
611
+ ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
612
+ }),
613
+ );
614
+ }
615
+ const declaration = {
616
+ sourcePath,
617
+ sourceRevision: `sha256:${createHash('sha256').update(source).digest('hex')}`,
618
+ meta,
619
+ definition: loaded.value,
620
+ sourceClosure,
621
+ } satisfies ScriptablePackInventoryDeclaration;
622
+ capture?.declarations.set(sourcePath, {
623
+ format: 'pack.ts',
624
+ ...declaration,
625
+ value: meta,
626
+ });
627
+ for (const sub of meta.subAssets) {
628
+ const guid = sub.guid.toLowerCase();
629
+ const existing = guidToPath.get(guid);
630
+ if (existing !== undefined) {
631
+ return packErr(
632
+ makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
633
+ );
634
+ }
635
+ guidToPath.set(guid, sourcePath);
636
+ }
637
+ }
638
+
639
+ // Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
640
+ // visited: nodes fully processed; recStack: nodes in current DFS path
641
+ const visited = new Set<string>();
642
+ const recStack = new Set<string>();
643
+
644
+ function dfs(guid: string, path: string[]): string[] | null {
645
+ visited.add(guid);
646
+ recStack.add(guid);
647
+
648
+ for (const ref of packRefs.get(guid) ?? []) {
649
+ if (!visited.has(ref)) {
650
+ const cycle = dfs(ref, [...path, ref]);
651
+ if (cycle !== null) return cycle;
652
+ } else if (recStack.has(ref)) {
653
+ // Found a back-edge: reconstruct cycle from the repeated node
654
+ const cycleStart = path.indexOf(ref);
655
+ return cycleStart >= 0 ? [...path.slice(cycleStart), ref] : [...path, ref];
656
+ }
657
+ }
658
+
659
+ recStack.delete(guid);
660
+ return null;
661
+ }
662
+
663
+ for (const guid of guidToPath.keys()) {
664
+ if (!visited.has(guid)) {
665
+ const cycle = dfs(guid, [guid]);
666
+ if (cycle !== null) {
667
+ return packErr(
668
+ makePackError('pack-cyclic-reference', {
669
+ code: 'pack-cyclic-reference',
670
+ kind: 'mount-asset',
671
+ cycle,
672
+ }),
673
+ );
674
+ }
675
+ }
676
+ }
677
+
678
+ return ok(rawPaths);
679
+ }
680
+
681
+ /** Scan one complete Pack inventory while preserving the public path-only API. */
682
+ export async function scan(
683
+ roots: readonly string[],
684
+ opts: ScanOptions = {},
685
+ ): Promise<ScanResult<string[], PackError>> {
686
+ return scanValidated(roots, opts);
687
+ }
688
+
689
+ /** Return the validated source inventory without interpreting producer output kinds. */
690
+ export async function scanInventory(
691
+ roots: readonly string[],
692
+ opts: ScanOptions = {},
693
+ ): Promise<ScanResult<ScanInventory, PackError>> {
694
+ const declarations = new Map<string, ScanSourceDeclaration>();
695
+ const scanned = await scanValidated(roots, opts, { declarations });
696
+ if (!scanned.ok) return scanned;
697
+ const inventory: InventoryDeclaration[] = [];
698
+ for (const sourcePath of scanned.value) {
699
+ const declaration = declarations.get(sourcePath);
700
+ if (declaration?.format !== 'pack.json') continue;
701
+ for (const [index, asset] of declaration.value.assets.entries()) {
702
+ inventory.push({
703
+ guid: asset.guid,
704
+ kind: asset.kind,
705
+ sourcePath,
706
+ sourceRevision: declaration.sourceRevision,
707
+ ...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
708
+ ...(asset.sourceIndex === undefined
709
+ ? { sourceIndex: index }
710
+ : { sourceIndex: asset.sourceIndex }),
711
+ });
712
+ }
713
+ }
714
+ return ok({ paths: scanned.value, inventory, declarations });
715
+ }