@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
@@ -0,0 +1,235 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type {
3
+ ArtifactDescriptor,
4
+ AssetCodec,
5
+ AssetStageError,
6
+ CookProduct,
7
+ Result,
8
+ } from '@forgeax/engine-types';
9
+ import { err, ok } from '@forgeax/engine-types';
10
+
11
+ export interface NativeCookArtifact {
12
+ readonly mediaType: string;
13
+ readonly assetCodec?: AssetCodec;
14
+ readonly bytes: Uint8Array;
15
+ }
16
+
17
+ export interface NativeCookDraft<P = unknown> {
18
+ readonly guid: string;
19
+ readonly payload: P;
20
+ readonly refs: readonly string[];
21
+ readonly artifacts: Readonly<Record<string, NativeCookArtifact>>;
22
+ readonly inputFingerprint: string;
23
+ }
24
+
25
+ export interface NativeCooker<P = unknown, I = unknown> {
26
+ readonly key: string;
27
+ readonly discover?: (input: unknown) => I | Promise<I>;
28
+ cook(input: I): NativeCookDraft<P> | Promise<NativeCookDraft<P>>;
29
+ }
30
+
31
+ export interface NativeCookTransactionSnapshot<P = unknown> {
32
+ readonly draft: NativeCookDraft<P>;
33
+ readonly generation: number;
34
+ }
35
+
36
+ export interface NativeCookTransactionOptions<P = unknown, I = unknown> {
37
+ readonly key: string;
38
+ readonly input: I;
39
+ readonly previous?: NativeCookTransactionSnapshot<P>;
40
+ readonly validate?: (draft: NativeCookDraft<P>) => string | undefined;
41
+ readonly publish?: (draft: NativeCookDraft<P>, generation: number) => void | Promise<void>;
42
+ }
43
+
44
+ export interface NativeCookTransactionResult<P = unknown> extends NativeCookTransactionSnapshot<P> {
45
+ readonly status: 'committed' | 'recovered';
46
+ readonly lastKnownGood: NativeCookDraft<P>;
47
+ readonly candidateGeneration: number;
48
+ readonly lastKnownGoodGeneration: number;
49
+ readonly recoveryHint?: string;
50
+ }
51
+
52
+ function canonicalize(value: unknown): unknown {
53
+ if (value instanceof Uint8Array) return Buffer.from(value).toString('base64');
54
+ if (Array.isArray(value)) return value.map(canonicalize);
55
+ if (typeof value !== 'object' || value === null) return value;
56
+ return Object.fromEntries(
57
+ Object.entries(value)
58
+ .sort(([left], [right]) => left.localeCompare(right))
59
+ .map(([key, nested]) => [key, canonicalize(nested)]),
60
+ );
61
+ }
62
+
63
+ function digestDraft<P>(draft: NativeCookDraft<P>): string {
64
+ const bytes = JSON.stringify(
65
+ canonicalize({
66
+ guid: draft.guid,
67
+ payload: draft.payload,
68
+ refs: draft.refs,
69
+ artifacts: draft.artifacts,
70
+ }),
71
+ );
72
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
73
+ }
74
+
75
+ type NativeCookErrorDetail = Extract<
76
+ AssetStageError['detail'],
77
+ { readonly guid: string; readonly producer: string }
78
+ >;
79
+
80
+ function failure(key: string, detail: NativeCookErrorDetail): Result<never, AssetStageError> {
81
+ return err({
82
+ stage: 'native-cook',
83
+ code: 'native-cook-failed',
84
+ expected: 'a registered native cooker to produce a complete draft',
85
+ hint: 'register the cooker or repair its draft, then rerun recovery',
86
+ detail,
87
+ recovery: { action: `rerun native cooker ${key}`, retryable: true },
88
+ });
89
+ }
90
+
91
+ function transactionFailure(key: string, reason: string): Result<never, AssetStageError> {
92
+ return failure(key, { guid: 'unknown', producer: reason });
93
+ }
94
+
95
+ function recovered<P>(
96
+ key: string,
97
+ previous: NativeCookTransactionSnapshot<P>,
98
+ ): NativeCookTransactionResult<P> {
99
+ return {
100
+ ...previous,
101
+ status: 'recovered',
102
+ lastKnownGood: previous.draft,
103
+ candidateGeneration: previous.generation + 1,
104
+ lastKnownGoodGeneration: previous.generation,
105
+ recoveryHint: `repair ${key} and submit a new candidate generation`,
106
+ };
107
+ }
108
+
109
+ /** Injectable build-time table for native authored Pack producers. */
110
+ export class NativeCookerRegistry {
111
+ private readonly cookers = new Map<string, NativeCooker>();
112
+
113
+ register(cooker: NativeCooker): void {
114
+ if (typeof cooker.key !== 'string' || cooker.key.length === 0) {
115
+ throw new TypeError('NativeCookerRegistry.register: key must be a non-empty string');
116
+ }
117
+ if (typeof cooker.cook !== 'function') {
118
+ throw new TypeError(`NativeCookerRegistry.register: cooker ${cooker.key} must expose cook`);
119
+ }
120
+ this.cookers.set(cooker.key, cooker);
121
+ }
122
+
123
+ get(key: string): NativeCooker | undefined {
124
+ return this.cookers.get(key);
125
+ }
126
+
127
+ registeredCookers(): readonly string[] {
128
+ return [...this.cookers.keys()];
129
+ }
130
+
131
+ async runDraft<P = unknown, I = unknown>(
132
+ key: string,
133
+ input: I,
134
+ ): Promise<Result<NativeCookDraft<P>, AssetStageError>> {
135
+ const cooker = this.cookers.get(key) as NativeCooker<P, I> | undefined;
136
+ if (cooker === undefined) {
137
+ return failure(key, { guid: 'unknown', producer: key });
138
+ }
139
+ let draft: NativeCookDraft<P>;
140
+ try {
141
+ const typedInput = cooker.discover === undefined ? input : await cooker.discover(input);
142
+ draft = await cooker.cook(typedInput);
143
+ } catch (error) {
144
+ return failure(key, { guid: 'unknown', producer: String(error) });
145
+ }
146
+ if (
147
+ draft.guid.length === 0 ||
148
+ draft.inputFingerprint.length === 0 ||
149
+ !Array.isArray(draft.refs) ||
150
+ typeof draft.payload !== 'object' ||
151
+ draft.payload === null ||
152
+ typeof draft.artifacts !== 'object' ||
153
+ draft.artifacts === null ||
154
+ Object.entries(draft.artifacts).some(
155
+ ([, artifact]) =>
156
+ artifact === null ||
157
+ typeof artifact !== 'object' ||
158
+ typeof artifact.mediaType !== 'string' ||
159
+ artifact.mediaType.length === 0 ||
160
+ !(artifact.bytes instanceof Uint8Array),
161
+ )
162
+ ) {
163
+ return failure(key, { guid: draft.guid || 'unknown', producer: key });
164
+ }
165
+ return ok(draft);
166
+ }
167
+
168
+ async run<P = unknown, I = unknown>(
169
+ key: string,
170
+ input: I,
171
+ ): Promise<Result<CookProduct<P>, AssetStageError>> {
172
+ const draftResult = await this.runDraft<P, I>(key, input);
173
+ if (!draftResult.ok) return draftResult;
174
+ const draft = draftResult.value;
175
+ const digest = digestDraft(draft);
176
+ const artifacts: Record<string, ArtifactDescriptor> = {};
177
+ for (const [path, artifact] of Object.entries(draft.artifacts)) {
178
+ artifacts[path] = {
179
+ path,
180
+ mediaType: artifact.mediaType,
181
+ ...(artifact.assetCodec === undefined ? {} : { assetCodec: artifact.assetCodec }),
182
+ byteLength: artifact.bytes.byteLength,
183
+ };
184
+ }
185
+ const receipt = {
186
+ guid: draft.guid,
187
+ origin: 'authoredPack' as const,
188
+ status: 'succeeded' as const,
189
+ inputFingerprint: draft.inputFingerprint,
190
+ outputDigest: digest,
191
+ };
192
+ const product: CookProduct<P> = {
193
+ guid: draft.guid,
194
+ payload: draft.payload,
195
+ refs: draft.refs,
196
+ artifacts,
197
+ digest,
198
+ receipt,
199
+ };
200
+ return ok(product);
201
+ }
202
+
203
+ async runTransaction<P = unknown, I = unknown>(
204
+ options: NativeCookTransactionOptions<P, I>,
205
+ ): Promise<Result<NativeCookTransactionResult<P>, AssetStageError>> {
206
+ const candidate = await this.runDraft<P, I>(options.key, options.input);
207
+ if (!candidate.ok) {
208
+ return options.previous === undefined
209
+ ? candidate
210
+ : ok(recovered(options.key, options.previous));
211
+ }
212
+ const invalidReason = options.validate?.(candidate.value);
213
+ if (invalidReason !== undefined) {
214
+ return options.previous === undefined
215
+ ? transactionFailure(options.key, invalidReason)
216
+ : ok(recovered(options.key, options.previous));
217
+ }
218
+ const generation = (options.previous?.generation ?? 0) + 1;
219
+ try {
220
+ await options.publish?.(candidate.value, generation);
221
+ } catch {
222
+ return options.previous === undefined
223
+ ? transactionFailure(options.key, 'atomic producer publication failed')
224
+ : ok(recovered(options.key, options.previous));
225
+ }
226
+ return ok({
227
+ draft: candidate.value,
228
+ generation,
229
+ status: 'committed',
230
+ lastKnownGood: candidate.value,
231
+ candidateGeneration: generation,
232
+ lastKnownGoodGeneration: generation,
233
+ });
234
+ }
235
+ }
@@ -0,0 +1,2 @@
1
+ /** Node/build-time native cooker registry. The browser-safe pack root excludes this seam. */
2
+ export * from './native-cooker-registry.js';
@@ -0,0 +1,394 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { AssetCodec, AssetRelation, CookReceipt } from '@forgeax/engine-types';
3
+
4
+ export interface PackageArtifactBody {
5
+ readonly mediaType: string;
6
+ readonly assetCodec?: AssetCodec;
7
+ readonly bytes: Uint8Array;
8
+ }
9
+
10
+ export interface PackageProductAsset {
11
+ readonly guid: string;
12
+ readonly kind: string;
13
+ readonly name?: string;
14
+ readonly payload: Readonly<Record<string, unknown>>;
15
+ readonly refs: readonly string[];
16
+ readonly artifacts: Readonly<Record<string, PackageArtifactBody>>;
17
+ }
18
+
19
+ export interface PackageProduct {
20
+ readonly assets: readonly PackageProductAsset[];
21
+ readonly receipts: readonly CookReceipt[];
22
+ readonly diagnostics: readonly unknown[];
23
+ readonly sourceRevision: string;
24
+ readonly sourceKey?: string;
25
+ }
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
+ export interface PackageDocumentArtifact {
47
+ readonly path: string;
48
+ readonly mediaType: string;
49
+ readonly assetCodec?: AssetCodec;
50
+ readonly byteLength: number;
51
+ readonly integrity: { readonly algorithm: 'sha256'; readonly digest: string };
52
+ }
53
+
54
+ export interface PackageDocumentAsset {
55
+ readonly guid: string;
56
+ readonly kind: string;
57
+ readonly name?: string;
58
+ readonly payload: Record<string, unknown>;
59
+ readonly refs: readonly string[];
60
+ readonly artifacts: Readonly<Record<string, PackageDocumentArtifact>>;
61
+ }
62
+
63
+ export interface PackageDocument {
64
+ readonly schemaVersion: '2.0.0';
65
+ readonly kind: 'internal-text-package';
66
+ readonly assets: readonly PackageDocumentAsset[];
67
+ }
68
+
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
+ export interface PackageFinalizePolicy {
92
+ readonly base: string;
93
+ readonly packagePath: string;
94
+ readonly artifactPath: (guid: string, localKey: string) => string;
95
+ readonly sink?: (path: string, bytes: Uint8Array) => void | Promise<void>;
96
+ }
97
+
98
+ export interface FinalizedPackageProduct {
99
+ readonly pack: PackageDocument;
100
+ readonly packageUrl: string;
101
+ readonly digest: string;
102
+ readonly receipts: readonly CookReceipt[];
103
+ readonly diagnostics: readonly unknown[];
104
+ readonly sourceRevision: string;
105
+ readonly sourceKey?: string;
106
+ }
107
+
108
+ interface PackageTransportSource {
109
+ readonly assets: readonly PackageProduct['assets'][number][];
110
+ readonly receipts?: readonly CookReceipt[];
111
+ readonly diagnostics?: readonly unknown[];
112
+ readonly sourceKey?: string;
113
+ }
114
+
115
+ export interface PackageFinalizerError {
116
+ readonly code:
117
+ | 'duplicate-guid'
118
+ | 'missing-source-revision'
119
+ | 'missing-receipt'
120
+ | 'invalid-artifact';
121
+ readonly expected: string;
122
+ readonly hint: string;
123
+ readonly detail: { readonly guid?: string; readonly key?: string };
124
+ }
125
+
126
+ export type PackageFinalizeResult =
127
+ | { readonly ok: true; readonly value: FinalizedPackageProduct }
128
+ | { readonly ok: false; readonly error: PackageFinalizerError };
129
+
130
+ function failure(
131
+ code: PackageFinalizerError['code'],
132
+ expected: string,
133
+ hint: string,
134
+ detail: PackageFinalizerError['detail'],
135
+ ): PackageFinalizeResult {
136
+ return { ok: false, error: { code, expected, hint, detail } };
137
+ }
138
+
139
+ function sorted(value: unknown): unknown {
140
+ if (value instanceof Uint8Array) return Buffer.from(value).toString('base64');
141
+ if (Array.isArray(value)) return value.map(sorted);
142
+ if (value !== null && typeof value === 'object') {
143
+ const output: Record<string, unknown> = {};
144
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
145
+ output[key] = sorted((value as Record<string, unknown>)[key]);
146
+ }
147
+ return output;
148
+ }
149
+ return value;
150
+ }
151
+
152
+ function packageUrl(base: string, packagePath: string): string {
153
+ const prefix = base === '/' ? '' : `/${base.replace(/^\/+|\/+$/g, '')}`;
154
+ return `${prefix}/${packagePath.replace(/^\/+/, '')}`;
155
+ }
156
+
157
+ function safePath(path: string): boolean {
158
+ const normalized = path.replaceAll('\\', '/');
159
+ return (
160
+ normalized.length > 0 &&
161
+ !normalized.startsWith('/') &&
162
+ !/^[A-Za-z]:\//.test(normalized) &&
163
+ !normalized.split('/').includes('..')
164
+ );
165
+ }
166
+
167
+ function digest(assets: readonly PackageProductAsset[]): string {
168
+ const pack = { schemaVersion: '2.0.0', kind: 'internal-text-package', assets } as const;
169
+ const hash = createHash('sha256').update(
170
+ JSON.stringify(
171
+ sorted({
172
+ ...pack,
173
+ assets: pack.assets.map((asset) => ({
174
+ ...asset,
175
+ artifacts: Object.fromEntries(
176
+ Object.entries(asset.artifacts).map(([key, artifact]) => [
177
+ key,
178
+ {
179
+ mediaType: artifact.mediaType,
180
+ ...(artifact.assetCodec === undefined ? {} : { assetCodec: artifact.assetCodec }),
181
+ byteLength: artifact.bytes.byteLength,
182
+ },
183
+ ]),
184
+ ),
185
+ })),
186
+ }),
187
+ ),
188
+ );
189
+ for (const [key, artifact] of assets
190
+ .flatMap((asset) =>
191
+ Object.entries(asset.artifacts).map(
192
+ ([localKey, body]) => [`${asset.guid}/${localKey}`, body] as const,
193
+ ),
194
+ )
195
+ .sort(([left], [right]) => left.localeCompare(right))) {
196
+ hash.update(key).update(artifact.bytes);
197
+ }
198
+ return `sha256:${hash.digest('hex')}`;
199
+ }
200
+
201
+ function packageDocument(
202
+ assets: readonly PackageProductAsset[],
203
+ artifactPath: (guid: string, localKey: string) => string,
204
+ ): PackageDocument {
205
+ return {
206
+ schemaVersion: '2.0.0',
207
+ kind: 'internal-text-package',
208
+ assets: assets.map((asset) => ({
209
+ guid: asset.guid,
210
+ kind: asset.kind,
211
+ ...(asset.name === undefined ? {} : { name: asset.name }),
212
+ payload: asset.payload,
213
+ refs: asset.refs,
214
+ artifacts: Object.fromEntries(
215
+ Object.entries(asset.artifacts).map(([key, artifact]) => [
216
+ key,
217
+ {
218
+ path: artifactPath(asset.guid, key),
219
+ mediaType: artifact.mediaType,
220
+ ...(artifact.assetCodec === undefined ? {} : { assetCodec: artifact.assetCodec }),
221
+ contentEncoding: 'identity' as const,
222
+ byteLength: artifact.bytes.byteLength,
223
+ integrity: {
224
+ algorithm: 'sha256' as const,
225
+ digest: `sha256:${createHash('sha256').update(artifact.bytes).digest('hex')}`,
226
+ },
227
+ },
228
+ ]),
229
+ ),
230
+ })),
231
+ };
232
+ }
233
+
234
+ function transportArtifacts(
235
+ assets: readonly PackageProductAsset[],
236
+ artifactPath: PackageFinalizePolicy['artifactPath'],
237
+ ) {
238
+ return assets.flatMap((asset) =>
239
+ Object.entries(asset.artifacts).flatMap(([localKey, artifact]) => [
240
+ {
241
+ guid: asset.guid,
242
+ localKey,
243
+ path: artifactPath(asset.guid, localKey),
244
+ mediaType: artifact.mediaType,
245
+ ...(artifact.assetCodec === undefined ? {} : { assetCodec: artifact.assetCodec }),
246
+ bytes: artifact.bytes,
247
+ },
248
+ ]),
249
+ );
250
+ }
251
+
252
+ /** Finalize a complete producer product without owning producer-specific policy. */
253
+ export async function finalizePackageProduct(
254
+ product: PackageProduct,
255
+ policy: PackageFinalizePolicy,
256
+ ): Promise<PackageFinalizeResult> {
257
+ if (product.sourceRevision.trim().length === 0) {
258
+ return failure(
259
+ 'missing-source-revision',
260
+ 'a non-empty source revision',
261
+ 'rebuild the inventory and provide the producer source revision',
262
+ {},
263
+ );
264
+ }
265
+ const seen = new Set<string>();
266
+ for (const asset of product.assets) {
267
+ const guid = asset.guid.toLowerCase();
268
+ if (seen.has(guid)) {
269
+ return failure(
270
+ 'duplicate-guid',
271
+ 'one terminal product row per GUID',
272
+ 'repair the inventory or producer output before finalization',
273
+ { guid },
274
+ );
275
+ }
276
+ seen.add(guid);
277
+ for (const [key, artifact] of Object.entries(asset.artifacts)) {
278
+ if (!safePath(key) || !(artifact.bytes instanceof Uint8Array)) {
279
+ return failure(
280
+ 'invalid-artifact',
281
+ 'asset-local artifact keys and byte bodies',
282
+ 'repair the producer artifact before finalization',
283
+ { guid, key },
284
+ );
285
+ }
286
+ }
287
+ }
288
+ for (const guid of seen) {
289
+ if (!product.receipts.some((receipt) => receipt.guid.toLowerCase() === guid)) {
290
+ return failure(
291
+ 'missing-receipt',
292
+ 'one receipt for every terminal product GUID',
293
+ 'preserve the producer receipt when handing the product to engine-pack',
294
+ { guid },
295
+ );
296
+ }
297
+ }
298
+ const assets = [...product.assets].sort((left, right) => left.guid.localeCompare(right.guid));
299
+ const document = packageDocument(assets, policy.artifactPath);
300
+ const packageBytes = new TextEncoder().encode(JSON.stringify(sorted(document)));
301
+ if (policy.sink !== undefined) {
302
+ for (const asset of assets) {
303
+ for (const [key, artifact] of Object.entries(asset.artifacts)) {
304
+ const path = policy.artifactPath(asset.guid, key);
305
+ if (!safePath(path)) {
306
+ return failure(
307
+ 'invalid-artifact',
308
+ 'package-relative artifact output paths',
309
+ 'repair the finalizer artifact path policy',
310
+ { guid: asset.guid, key },
311
+ );
312
+ }
313
+ await policy.sink(path, artifact.bytes);
314
+ }
315
+ }
316
+ if (!safePath(policy.packagePath)) {
317
+ return failure(
318
+ 'invalid-artifact',
319
+ 'a package-relative package path',
320
+ 'repair the finalizer package path policy',
321
+ { key: policy.packagePath },
322
+ );
323
+ }
324
+ await policy.sink(policy.packagePath, packageBytes);
325
+ }
326
+ return {
327
+ ok: true,
328
+ value: {
329
+ pack: document,
330
+ packageUrl: packageUrl(policy.base, policy.packagePath),
331
+ digest: digest(product.assets),
332
+ receipts: product.receipts,
333
+ diagnostics: product.diagnostics,
334
+ sourceRevision: product.sourceRevision,
335
+ ...(product.sourceKey === undefined ? {} : { sourceKey: product.sourceKey }),
336
+ },
337
+ };
338
+ }
339
+
340
+ /** Finalize a product-shaped source with the deterministic transport revision. */
341
+ export async function finalizePackageTransportSource(
342
+ input: PackageTransportSource,
343
+ policy: PackageFinalizePolicy,
344
+ ) {
345
+ const sourceRevision = packageTransportRevision(input);
346
+ const product: PackageProduct = {
347
+ assets: input.assets,
348
+ receipts:
349
+ input.receipts ??
350
+ input.assets.map((asset) => ({
351
+ guid: asset.guid,
352
+ origin: 'sourceMeta' as const,
353
+ status: 'succeeded' as const,
354
+ inputFingerprint: sourceRevision,
355
+ })),
356
+ diagnostics: input.diagnostics ?? [],
357
+ sourceRevision,
358
+ ...(input.sourceKey === undefined ? {} : { sourceKey: input.sourceKey }),
359
+ };
360
+ const finalized = await finalizePackageProduct(product, policy);
361
+ if (!finalized.ok) throw finalized.error;
362
+ return {
363
+ pack: finalized.value.pack,
364
+ packageUrl: finalized.value.packageUrl,
365
+ artifacts: transportArtifacts(product.assets, policy.artifactPath),
366
+ digest: finalized.value.digest,
367
+ sourceRevision,
368
+ semantic: JSON.stringify(sorted(finalized.value.pack)),
369
+ };
370
+ }
371
+
372
+ function revisionStable(value: unknown): unknown {
373
+ if (value instanceof Uint8Array) {
374
+ return {
375
+ byteLength: value.byteLength,
376
+ digest: createHash('sha256').update(value).digest('hex'),
377
+ };
378
+ }
379
+ if (Array.isArray(value)) return value.map(revisionStable);
380
+ if (value !== null && typeof value === 'object') {
381
+ return Object.fromEntries(
382
+ Object.entries(value as Record<string, unknown>)
383
+ .sort(([left], [right]) => left.localeCompare(right))
384
+ .map(([key, item]) => [key, revisionStable(item)]),
385
+ );
386
+ }
387
+ return value;
388
+ }
389
+
390
+ export function packageTransportRevision(value: unknown): string {
391
+ return createHash('sha256')
392
+ .update(JSON.stringify(revisionStable(value)))
393
+ .digest('hex');
394
+ }