hoi4-agent-tools 0.0.0-bootstrap.1 → 0.1.3

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 (416) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +201 -0
  3. package/README.md +140 -2
  4. package/SECURITY.md +17 -0
  5. package/dist/bin/http.d.ts +3 -0
  6. package/dist/bin/http.d.ts.map +1 -0
  7. package/dist/bin/http.js +27 -0
  8. package/dist/bin/http.js.map +1 -0
  9. package/dist/bin/setup.d.ts +3 -0
  10. package/dist/bin/setup.d.ts.map +1 -0
  11. package/dist/bin/setup.js +188 -0
  12. package/dist/bin/setup.js.map +1 -0
  13. package/dist/bin/stdio.d.ts +3 -0
  14. package/dist/bin/stdio.d.ts.map +1 -0
  15. package/dist/bin/stdio.js +36 -0
  16. package/dist/bin/stdio.js.map +1 -0
  17. package/dist/hoi4_agent_tools/core/artifacts.d.ts +117 -0
  18. package/dist/hoi4_agent_tools/core/artifacts.d.ts.map +1 -0
  19. package/dist/hoi4_agent_tools/core/artifacts.js +1047 -0
  20. package/dist/hoi4_agent_tools/core/artifacts.js.map +1 -0
  21. package/dist/hoi4_agent_tools/core/canonical.d.ts +13 -0
  22. package/dist/hoi4_agent_tools/core/canonical.d.ts.map +1 -0
  23. package/dist/hoi4_agent_tools/core/canonical.js +41 -0
  24. package/dist/hoi4_agent_tools/core/canonical.js.map +1 -0
  25. package/dist/hoi4_agent_tools/core/configuration.d.ts +137 -0
  26. package/dist/hoi4_agent_tools/core/configuration.d.ts.map +1 -0
  27. package/dist/hoi4_agent_tools/core/configuration.js +383 -0
  28. package/dist/hoi4_agent_tools/core/configuration.js.map +1 -0
  29. package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts +18 -0
  30. package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts.map +1 -0
  31. package/dist/hoi4_agent_tools/core/diagnostic-collector.js +45 -0
  32. package/dist/hoi4_agent_tools/core/diagnostic-collector.js.map +1 -0
  33. package/dist/hoi4_agent_tools/core/diagnostics.d.ts +26 -0
  34. package/dist/hoi4_agent_tools/core/diagnostics.d.ts.map +1 -0
  35. package/dist/hoi4_agent_tools/core/diagnostics.js +25 -0
  36. package/dist/hoi4_agent_tools/core/diagnostics.js.map +1 -0
  37. package/dist/hoi4_agent_tools/core/diff.d.ts +22 -0
  38. package/dist/hoi4_agent_tools/core/diff.d.ts.map +1 -0
  39. package/dist/hoi4_agent_tools/core/diff.js +93 -0
  40. package/dist/hoi4_agent_tools/core/diff.js.map +1 -0
  41. package/dist/hoi4_agent_tools/core/engine.d.ts +56 -0
  42. package/dist/hoi4_agent_tools/core/engine.d.ts.map +1 -0
  43. package/dist/hoi4_agent_tools/core/engine.js +319 -0
  44. package/dist/hoi4_agent_tools/core/engine.js.map +1 -0
  45. package/dist/hoi4_agent_tools/core/image-diff.d.ts +14 -0
  46. package/dist/hoi4_agent_tools/core/image-diff.d.ts.map +1 -0
  47. package/dist/hoi4_agent_tools/core/image-diff.js +121 -0
  48. package/dist/hoi4_agent_tools/core/image-diff.js.map +1 -0
  49. package/dist/hoi4_agent_tools/core/index.d.ts +64 -0
  50. package/dist/hoi4_agent_tools/core/index.d.ts.map +1 -0
  51. package/dist/hoi4_agent_tools/core/index.js +963 -0
  52. package/dist/hoi4_agent_tools/core/index.js.map +1 -0
  53. package/dist/hoi4_agent_tools/core/index.public.d.ts +16 -0
  54. package/dist/hoi4_agent_tools/core/index.public.d.ts.map +1 -0
  55. package/dist/hoi4_agent_tools/core/index.public.js +16 -0
  56. package/dist/hoi4_agent_tools/core/index.public.js.map +1 -0
  57. package/dist/hoi4_agent_tools/core/render-budget.d.ts +51 -0
  58. package/dist/hoi4_agent_tools/core/render-budget.d.ts.map +1 -0
  59. package/dist/hoi4_agent_tools/core/render-budget.js +109 -0
  60. package/dist/hoi4_agent_tools/core/render-budget.js.map +1 -0
  61. package/dist/hoi4_agent_tools/core/result.d.ts +45 -0
  62. package/dist/hoi4_agent_tools/core/result.d.ts.map +1 -0
  63. package/dist/hoi4_agent_tools/core/result.js +26 -0
  64. package/dist/hoi4_agent_tools/core/result.js.map +1 -0
  65. package/dist/hoi4_agent_tools/core/scanner.d.ts +28 -0
  66. package/dist/hoi4_agent_tools/core/scanner.d.ts.map +1 -0
  67. package/dist/hoi4_agent_tools/core/scanner.js +136 -0
  68. package/dist/hoi4_agent_tools/core/scanner.js.map +1 -0
  69. package/dist/hoi4_agent_tools/core/server-state.d.ts +31 -0
  70. package/dist/hoi4_agent_tools/core/server-state.d.ts.map +1 -0
  71. package/dist/hoi4_agent_tools/core/server-state.js +413 -0
  72. package/dist/hoi4_agent_tools/core/server-state.js.map +1 -0
  73. package/dist/hoi4_agent_tools/core/source/encoding.d.ts +10 -0
  74. package/dist/hoi4_agent_tools/core/source/encoding.d.ts.map +1 -0
  75. package/dist/hoi4_agent_tools/core/source/encoding.js +54 -0
  76. package/dist/hoi4_agent_tools/core/source/encoding.js.map +1 -0
  77. package/dist/hoi4_agent_tools/core/source/index.d.ts +7 -0
  78. package/dist/hoi4_agent_tools/core/source/index.d.ts.map +1 -0
  79. package/dist/hoi4_agent_tools/core/source/index.js +7 -0
  80. package/dist/hoi4_agent_tools/core/source/index.js.map +1 -0
  81. package/dist/hoi4_agent_tools/core/source/lexer.d.ts +30 -0
  82. package/dist/hoi4_agent_tools/core/source/lexer.d.ts.map +1 -0
  83. package/dist/hoi4_agent_tools/core/source/lexer.js +266 -0
  84. package/dist/hoi4_agent_tools/core/source/lexer.js.map +1 -0
  85. package/dist/hoi4_agent_tools/core/source/limits.d.ts +21 -0
  86. package/dist/hoi4_agent_tools/core/source/limits.d.ts.map +1 -0
  87. package/dist/hoi4_agent_tools/core/source/limits.js +49 -0
  88. package/dist/hoi4_agent_tools/core/source/limits.js.map +1 -0
  89. package/dist/hoi4_agent_tools/core/source/localisation.d.ts +20 -0
  90. package/dist/hoi4_agent_tools/core/source/localisation.d.ts.map +1 -0
  91. package/dist/hoi4_agent_tools/core/source/localisation.js +163 -0
  92. package/dist/hoi4_agent_tools/core/source/localisation.js.map +1 -0
  93. package/dist/hoi4_agent_tools/core/source/parser.d.ts +44 -0
  94. package/dist/hoi4_agent_tools/core/source/parser.d.ts.map +1 -0
  95. package/dist/hoi4_agent_tools/core/source/parser.js +334 -0
  96. package/dist/hoi4_agent_tools/core/source/parser.js.map +1 -0
  97. package/dist/hoi4_agent_tools/core/source/rewrite.d.ts +10 -0
  98. package/dist/hoi4_agent_tools/core/source/rewrite.d.ts.map +1 -0
  99. package/dist/hoi4_agent_tools/core/source/rewrite.js +28 -0
  100. package/dist/hoi4_agent_tools/core/source/rewrite.js.map +1 -0
  101. package/dist/hoi4_agent_tools/core/svg-text.d.ts +34 -0
  102. package/dist/hoi4_agent_tools/core/svg-text.d.ts.map +1 -0
  103. package/dist/hoi4_agent_tools/core/svg-text.js +127 -0
  104. package/dist/hoi4_agent_tools/core/svg-text.js.map +1 -0
  105. package/dist/hoi4_agent_tools/core/transaction-limits.d.ts +9 -0
  106. package/dist/hoi4_agent_tools/core/transaction-limits.d.ts.map +1 -0
  107. package/dist/hoi4_agent_tools/core/transaction-limits.js +9 -0
  108. package/dist/hoi4_agent_tools/core/transaction-limits.js.map +1 -0
  109. package/dist/hoi4_agent_tools/core/transactions.d.ts +162 -0
  110. package/dist/hoi4_agent_tools/core/transactions.d.ts.map +1 -0
  111. package/dist/hoi4_agent_tools/core/transactions.js +1181 -0
  112. package/dist/hoi4_agent_tools/core/transactions.js.map +1 -0
  113. package/dist/hoi4_agent_tools/core/workspace.d.ts +68 -0
  114. package/dist/hoi4_agent_tools/core/workspace.d.ts.map +1 -0
  115. package/dist/hoi4_agent_tools/core/workspace.js +794 -0
  116. package/dist/hoi4_agent_tools/core/workspace.js.map +1 -0
  117. package/dist/hoi4_agent_tools/focus/compiler.d.ts +9 -0
  118. package/dist/hoi4_agent_tools/focus/compiler.d.ts.map +1 -0
  119. package/dist/hoi4_agent_tools/focus/compiler.js +273 -0
  120. package/dist/hoi4_agent_tools/focus/compiler.js.map +1 -0
  121. package/dist/hoi4_agent_tools/focus/continuous-render.d.ts +32 -0
  122. package/dist/hoi4_agent_tools/focus/continuous-render.d.ts.map +1 -0
  123. package/dist/hoi4_agent_tools/focus/continuous-render.js +179 -0
  124. package/dist/hoi4_agent_tools/focus/continuous-render.js.map +1 -0
  125. package/dist/hoi4_agent_tools/focus/drift.d.ts +22 -0
  126. package/dist/hoi4_agent_tools/focus/drift.d.ts.map +1 -0
  127. package/dist/hoi4_agent_tools/focus/drift.js +124 -0
  128. package/dist/hoi4_agent_tools/focus/drift.js.map +1 -0
  129. package/dist/hoi4_agent_tools/focus/geometry.d.ts +17 -0
  130. package/dist/hoi4_agent_tools/focus/geometry.d.ts.map +1 -0
  131. package/dist/hoi4_agent_tools/focus/geometry.js +35 -0
  132. package/dist/hoi4_agent_tools/focus/geometry.js.map +1 -0
  133. package/dist/hoi4_agent_tools/focus/importer.d.ts +9 -0
  134. package/dist/hoi4_agent_tools/focus/importer.d.ts.map +1 -0
  135. package/dist/hoi4_agent_tools/focus/importer.js +732 -0
  136. package/dist/hoi4_agent_tools/focus/importer.js.map +1 -0
  137. package/dist/hoi4_agent_tools/focus/index.d.ts +14 -0
  138. package/dist/hoi4_agent_tools/focus/index.d.ts.map +1 -0
  139. package/dist/hoi4_agent_tools/focus/index.js +14 -0
  140. package/dist/hoi4_agent_tools/focus/index.js.map +1 -0
  141. package/dist/hoi4_agent_tools/focus/layout.d.ts +5 -0
  142. package/dist/hoi4_agent_tools/focus/layout.d.ts.map +1 -0
  143. package/dist/hoi4_agent_tools/focus/layout.js +653 -0
  144. package/dist/hoi4_agent_tools/focus/layout.js.map +1 -0
  145. package/dist/hoi4_agent_tools/focus/limits.d.ts +9 -0
  146. package/dist/hoi4_agent_tools/focus/limits.d.ts.map +1 -0
  147. package/dist/hoi4_agent_tools/focus/limits.js +9 -0
  148. package/dist/hoi4_agent_tools/focus/limits.js.map +1 -0
  149. package/dist/hoi4_agent_tools/focus/lint.d.ts +14 -0
  150. package/dist/hoi4_agent_tools/focus/lint.d.ts.map +1 -0
  151. package/dist/hoi4_agent_tools/focus/lint.js +839 -0
  152. package/dist/hoi4_agent_tools/focus/lint.js.map +1 -0
  153. package/dist/hoi4_agent_tools/focus/model.d.ts +303 -0
  154. package/dist/hoi4_agent_tools/focus/model.d.ts.map +1 -0
  155. package/dist/hoi4_agent_tools/focus/model.js +20 -0
  156. package/dist/hoi4_agent_tools/focus/model.js.map +1 -0
  157. package/dist/hoi4_agent_tools/focus/native-effects.d.ts +9 -0
  158. package/dist/hoi4_agent_tools/focus/native-effects.d.ts.map +1 -0
  159. package/dist/hoi4_agent_tools/focus/native-effects.js +563 -0
  160. package/dist/hoi4_agent_tools/focus/native-effects.js.map +1 -0
  161. package/dist/hoi4_agent_tools/focus/planning.d.ts +15 -0
  162. package/dist/hoi4_agent_tools/focus/planning.d.ts.map +1 -0
  163. package/dist/hoi4_agent_tools/focus/planning.js +241 -0
  164. package/dist/hoi4_agent_tools/focus/planning.js.map +1 -0
  165. package/dist/hoi4_agent_tools/focus/presentation.d.ts +24 -0
  166. package/dist/hoi4_agent_tools/focus/presentation.d.ts.map +1 -0
  167. package/dist/hoi4_agent_tools/focus/presentation.js +204 -0
  168. package/dist/hoi4_agent_tools/focus/presentation.js.map +1 -0
  169. package/dist/hoi4_agent_tools/focus/render.d.ts +35 -0
  170. package/dist/hoi4_agent_tools/focus/render.d.ts.map +1 -0
  171. package/dist/hoi4_agent_tools/focus/render.js +491 -0
  172. package/dist/hoi4_agent_tools/focus/render.js.map +1 -0
  173. package/dist/hoi4_agent_tools/focus/service.d.ts +82 -0
  174. package/dist/hoi4_agent_tools/focus/service.d.ts.map +1 -0
  175. package/dist/hoi4_agent_tools/focus/service.js +492 -0
  176. package/dist/hoi4_agent_tools/focus/service.js.map +1 -0
  177. package/dist/hoi4_agent_tools/focus/source-update.d.ts +6 -0
  178. package/dist/hoi4_agent_tools/focus/source-update.d.ts.map +1 -0
  179. package/dist/hoi4_agent_tools/focus/source-update.js +328 -0
  180. package/dist/hoi4_agent_tools/focus/source-update.js.map +1 -0
  181. package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts +37 -0
  182. package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts.map +1 -0
  183. package/dist/hoi4_agent_tools/gui/animation-manifest.js +66 -0
  184. package/dist/hoi4_agent_tools/gui/animation-manifest.js.map +1 -0
  185. package/dist/hoi4_agent_tools/gui/assets.d.ts +71 -0
  186. package/dist/hoi4_agent_tools/gui/assets.d.ts.map +1 -0
  187. package/dist/hoi4_agent_tools/gui/assets.js +629 -0
  188. package/dist/hoi4_agent_tools/gui/assets.js.map +1 -0
  189. package/dist/hoi4_agent_tools/gui/dds.d.ts +17 -0
  190. package/dist/hoi4_agent_tools/gui/dds.d.ts.map +1 -0
  191. package/dist/hoi4_agent_tools/gui/dds.js +245 -0
  192. package/dist/hoi4_agent_tools/gui/dds.js.map +1 -0
  193. package/dist/hoi4_agent_tools/gui/helpers.d.ts +101 -0
  194. package/dist/hoi4_agent_tools/gui/helpers.d.ts.map +1 -0
  195. package/dist/hoi4_agent_tools/gui/helpers.js +1072 -0
  196. package/dist/hoi4_agent_tools/gui/helpers.js.map +1 -0
  197. package/dist/hoi4_agent_tools/gui/index.d.ts +15 -0
  198. package/dist/hoi4_agent_tools/gui/index.d.ts.map +1 -0
  199. package/dist/hoi4_agent_tools/gui/index.js +15 -0
  200. package/dist/hoi4_agent_tools/gui/index.js.map +1 -0
  201. package/dist/hoi4_agent_tools/gui/layout.d.ts +6 -0
  202. package/dist/hoi4_agent_tools/gui/layout.d.ts.map +1 -0
  203. package/dist/hoi4_agent_tools/gui/layout.js +753 -0
  204. package/dist/hoi4_agent_tools/gui/layout.js.map +1 -0
  205. package/dist/hoi4_agent_tools/gui/limits.d.ts +25 -0
  206. package/dist/hoi4_agent_tools/gui/limits.d.ts.map +1 -0
  207. package/dist/hoi4_agent_tools/gui/limits.js +27 -0
  208. package/dist/hoi4_agent_tools/gui/limits.js.map +1 -0
  209. package/dist/hoi4_agent_tools/gui/renderer.d.ts +18 -0
  210. package/dist/hoi4_agent_tools/gui/renderer.d.ts.map +1 -0
  211. package/dist/hoi4_agent_tools/gui/renderer.js +388 -0
  212. package/dist/hoi4_agent_tools/gui/renderer.js.map +1 -0
  213. package/dist/hoi4_agent_tools/gui/scenario.d.ts +66 -0
  214. package/dist/hoi4_agent_tools/gui/scenario.d.ts.map +1 -0
  215. package/dist/hoi4_agent_tools/gui/scenario.js +191 -0
  216. package/dist/hoi4_agent_tools/gui/scenario.js.map +1 -0
  217. package/dist/hoi4_agent_tools/gui/source-graph.d.ts +7 -0
  218. package/dist/hoi4_agent_tools/gui/source-graph.d.ts.map +1 -0
  219. package/dist/hoi4_agent_tools/gui/source-graph.js +1074 -0
  220. package/dist/hoi4_agent_tools/gui/source-graph.js.map +1 -0
  221. package/dist/hoi4_agent_tools/gui/source-patch.d.ts +13 -0
  222. package/dist/hoi4_agent_tools/gui/source-patch.d.ts.map +1 -0
  223. package/dist/hoi4_agent_tools/gui/source-patch.js +90 -0
  224. package/dist/hoi4_agent_tools/gui/source-patch.js.map +1 -0
  225. package/dist/hoi4_agent_tools/gui/studio.d.ts +86 -0
  226. package/dist/hoi4_agent_tools/gui/studio.d.ts.map +1 -0
  227. package/dist/hoi4_agent_tools/gui/studio.js +830 -0
  228. package/dist/hoi4_agent_tools/gui/studio.js.map +1 -0
  229. package/dist/hoi4_agent_tools/gui/tga.d.ts +16 -0
  230. package/dist/hoi4_agent_tools/gui/tga.d.ts.map +1 -0
  231. package/dist/hoi4_agent_tools/gui/tga.js +157 -0
  232. package/dist/hoi4_agent_tools/gui/tga.js.map +1 -0
  233. package/dist/hoi4_agent_tools/gui/types.d.ts +322 -0
  234. package/dist/hoi4_agent_tools/gui/types.d.ts.map +1 -0
  235. package/dist/hoi4_agent_tools/gui/types.js +19 -0
  236. package/dist/hoi4_agent_tools/gui/types.js.map +1 -0
  237. package/dist/hoi4_agent_tools/gui/validators.d.ts +8 -0
  238. package/dist/hoi4_agent_tools/gui/validators.d.ts.map +1 -0
  239. package/dist/hoi4_agent_tools/gui/validators.js +951 -0
  240. package/dist/hoi4_agent_tools/gui/validators.js.map +1 -0
  241. package/dist/hoi4_agent_tools/map/bmp.d.ts +62 -0
  242. package/dist/hoi4_agent_tools/map/bmp.d.ts.map +1 -0
  243. package/dist/hoi4_agent_tools/map/bmp.js +363 -0
  244. package/dist/hoi4_agent_tools/map/bmp.js.map +1 -0
  245. package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts +5 -0
  246. package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts.map +1 -0
  247. package/dist/hoi4_agent_tools/map/diagnostic-limit.js +21 -0
  248. package/dist/hoi4_agent_tools/map/diagnostic-limit.js.map +1 -0
  249. package/dist/hoi4_agent_tools/map/index.d.ts +8 -0
  250. package/dist/hoi4_agent_tools/map/index.d.ts.map +1 -0
  251. package/dist/hoi4_agent_tools/map/index.js +8 -0
  252. package/dist/hoi4_agent_tools/map/index.js.map +1 -0
  253. package/dist/hoi4_agent_tools/map/limits.d.ts +13 -0
  254. package/dist/hoi4_agent_tools/map/limits.d.ts.map +1 -0
  255. package/dist/hoi4_agent_tools/map/limits.js +13 -0
  256. package/dist/hoi4_agent_tools/map/limits.js.map +1 -0
  257. package/dist/hoi4_agent_tools/map/model.d.ts +269 -0
  258. package/dist/hoi4_agent_tools/map/model.d.ts.map +1 -0
  259. package/dist/hoi4_agent_tools/map/model.js +1114 -0
  260. package/dist/hoi4_agent_tools/map/model.js.map +1 -0
  261. package/dist/hoi4_agent_tools/map/operations.d.ts +401 -0
  262. package/dist/hoi4_agent_tools/map/operations.d.ts.map +1 -0
  263. package/dist/hoi4_agent_tools/map/operations.js +2717 -0
  264. package/dist/hoi4_agent_tools/map/operations.js.map +1 -0
  265. package/dist/hoi4_agent_tools/map/render.d.ts +90 -0
  266. package/dist/hoi4_agent_tools/map/render.d.ts.map +1 -0
  267. package/dist/hoi4_agent_tools/map/render.js +759 -0
  268. package/dist/hoi4_agent_tools/map/render.js.map +1 -0
  269. package/dist/hoi4_agent_tools/map/service.d.ts +81 -0
  270. package/dist/hoi4_agent_tools/map/service.d.ts.map +1 -0
  271. package/dist/hoi4_agent_tools/map/service.js +695 -0
  272. package/dist/hoi4_agent_tools/map/service.js.map +1 -0
  273. package/dist/hoi4_agent_tools/map/validation.d.ts +23 -0
  274. package/dist/hoi4_agent_tools/map/validation.d.ts.map +1 -0
  275. package/dist/hoi4_agent_tools/map/validation.js +1177 -0
  276. package/dist/hoi4_agent_tools/map/validation.js.map +1 -0
  277. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +3 -0
  278. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +1 -0
  279. package/dist/hoi4_agent_tools/mcp/prompts/register.js +50 -0
  280. package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +1 -0
  281. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +9 -0
  282. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts.map +1 -0
  283. package/dist/hoi4_agent_tools/mcp/registry/metadata.js +10 -0
  284. package/dist/hoi4_agent_tools/mcp/registry/metadata.js.map +1 -0
  285. package/dist/hoi4_agent_tools/mcp/resources/register.d.ts +5 -0
  286. package/dist/hoi4_agent_tools/mcp/resources/register.d.ts.map +1 -0
  287. package/dist/hoi4_agent_tools/mcp/resources/register.js +278 -0
  288. package/dist/hoi4_agent_tools/mcp/resources/register.js.map +1 -0
  289. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts +24 -0
  290. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +1 -0
  291. package/dist/hoi4_agent_tools/mcp/security/auth.js +234 -0
  292. package/dist/hoi4_agent_tools/mcp/security/auth.js.map +1 -0
  293. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +35 -0
  294. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +1 -0
  295. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +835 -0
  296. package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +1 -0
  297. package/dist/hoi4_agent_tools/mcp/server/create.d.ts +5 -0
  298. package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +1 -0
  299. package/dist/hoi4_agent_tools/mcp/server/create.js +26 -0
  300. package/dist/hoi4_agent_tools/mcp/server/create.js.map +1 -0
  301. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +133 -0
  302. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +1 -0
  303. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +127 -0
  304. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +1 -0
  305. package/dist/hoi4_agent_tools/mcp/server/progress.d.ts +8 -0
  306. package/dist/hoi4_agent_tools/mcp/server/progress.d.ts.map +1 -0
  307. package/dist/hoi4_agent_tools/mcp/server/progress.js +21 -0
  308. package/dist/hoi4_agent_tools/mcp/server/progress.js.map +1 -0
  309. package/dist/hoi4_agent_tools/mcp/server/result.d.ts +232 -0
  310. package/dist/hoi4_agent_tools/mcp/server/result.d.ts.map +1 -0
  311. package/dist/hoi4_agent_tools/mcp/server/result.js +353 -0
  312. package/dist/hoi4_agent_tools/mcp/server/result.js.map +1 -0
  313. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts +5 -0
  314. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +1 -0
  315. package/dist/hoi4_agent_tools/mcp/tools/focus.js +1059 -0
  316. package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +1 -0
  317. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +111 -0
  318. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +1 -0
  319. package/dist/hoi4_agent_tools/mcp/tools/gui.js +598 -0
  320. package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +1 -0
  321. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts +5 -0
  322. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +1 -0
  323. package/dist/hoi4_agent_tools/mcp/tools/map.js +685 -0
  324. package/dist/hoi4_agent_tools/mcp/tools/map.js.map +1 -0
  325. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts +58 -0
  326. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts.map +1 -0
  327. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +181 -0
  328. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js.map +1 -0
  329. package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts +26 -0
  330. package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts.map +1 -0
  331. package/dist/hoi4_agent_tools/mcp/transports/event-store.js +86 -0
  332. package/dist/hoi4_agent_tools/mcp/transports/event-store.js.map +1 -0
  333. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts +28 -0
  334. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +1 -0
  335. package/dist/hoi4_agent_tools/mcp/transports/http.js +744 -0
  336. package/dist/hoi4_agent_tools/mcp/transports/http.js.map +1 -0
  337. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts +15 -0
  338. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts.map +1 -0
  339. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +51 -0
  340. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js.map +1 -0
  341. package/dist/hoi4_agent_tools/runtime.d.ts +4 -0
  342. package/dist/hoi4_agent_tools/runtime.d.ts.map +1 -0
  343. package/dist/hoi4_agent_tools/runtime.js +27 -0
  344. package/dist/hoi4_agent_tools/runtime.js.map +1 -0
  345. package/dist/hoi4_agent_tools/schemas/common.d.ts +74 -0
  346. package/dist/hoi4_agent_tools/schemas/common.d.ts.map +1 -0
  347. package/dist/hoi4_agent_tools/schemas/common.js +51 -0
  348. package/dist/hoi4_agent_tools/schemas/common.js.map +1 -0
  349. package/dist/hoi4_agent_tools/schemas/focus.d.ts +784 -0
  350. package/dist/hoi4_agent_tools/schemas/focus.d.ts.map +1 -0
  351. package/dist/hoi4_agent_tools/schemas/focus.js +330 -0
  352. package/dist/hoi4_agent_tools/schemas/focus.js.map +1 -0
  353. package/dist/hoi4_agent_tools/schemas/index.d.ts +5 -0
  354. package/dist/hoi4_agent_tools/schemas/index.d.ts.map +1 -0
  355. package/dist/hoi4_agent_tools/schemas/index.js +5 -0
  356. package/dist/hoi4_agent_tools/schemas/index.js.map +1 -0
  357. package/dist/hoi4_agent_tools/schemas/map.d.ts +654 -0
  358. package/dist/hoi4_agent_tools/schemas/map.d.ts.map +1 -0
  359. package/dist/hoi4_agent_tools/schemas/map.js +570 -0
  360. package/dist/hoi4_agent_tools/schemas/map.js.map +1 -0
  361. package/dist/hoi4_agent_tools/schemas/transaction.d.ts +259 -0
  362. package/dist/hoi4_agent_tools/schemas/transaction.d.ts.map +1 -0
  363. package/dist/hoi4_agent_tools/schemas/transaction.js +104 -0
  364. package/dist/hoi4_agent_tools/schemas/transaction.js.map +1 -0
  365. package/dist/hoi4_agent_tools/version.d.ts +6 -0
  366. package/dist/hoi4_agent_tools/version.d.ts.map +1 -0
  367. package/dist/hoi4_agent_tools/version.js +7 -0
  368. package/dist/hoi4_agent_tools/version.js.map +1 -0
  369. package/dist/index.d.ts +3 -0
  370. package/dist/index.d.ts.map +1 -0
  371. package/dist/index.js +3 -0
  372. package/dist/index.js.map +1 -0
  373. package/docs/README.md +22 -0
  374. package/docs/adr/0001-runtime-and-language.md +16 -0
  375. package/docs/adr/0002-lossless-source-model.md +18 -0
  376. package/docs/adr/0003-image-and-font-codecs.md +18 -0
  377. package/docs/adr/0004-focus-layout.md +16 -0
  378. package/docs/adr/0005-offline-rendering.md +18 -0
  379. package/docs/adr/0006-mcp-sdk-and-protocol.md +16 -0
  380. package/docs/adr/0007-storage-and-transactions.md +24 -0
  381. package/docs/adr/0008-test-strategy.md +18 -0
  382. package/docs/adr/0009-packaging-and-registry.md +16 -0
  383. package/docs/adr/0010-unsupported-and-refusal-policy.md +18 -0
  384. package/docs/adr/0011-http-security.md +16 -0
  385. package/docs/adr/README.md +13 -0
  386. package/docs/architecture.md +60 -0
  387. package/docs/artifacts.md +133 -0
  388. package/docs/compatibility.md +33 -0
  389. package/docs/completion-report.md +69 -0
  390. package/docs/configuration.md +285 -0
  391. package/docs/focus-workflow.md +68 -0
  392. package/docs/gui-workflow.md +121 -0
  393. package/docs/limitations.md +30 -0
  394. package/docs/map-workflow.md +89 -0
  395. package/docs/performance.md +117 -0
  396. package/docs/release.md +200 -0
  397. package/docs/research/mcp-2026-07-10.md +46 -0
  398. package/docs/research/source-ledger.md +68 -0
  399. package/docs/security.md +139 -0
  400. package/docs/self-hosting.md +168 -0
  401. package/docs/testing.md +43 -0
  402. package/docs/third-party-notices.md +11 -0
  403. package/docs/transactions.md +162 -0
  404. package/docs/troubleshooting.md +46 -0
  405. package/package.json +98 -5
  406. package/schemas/configuration.schema.json +794 -0
  407. package/schemas/continuous-focus-palette.schema.json +325 -0
  408. package/schemas/focus-plan.schema.json +902 -0
  409. package/schemas/focus-planning-sidecar.schema.json +364 -0
  410. package/schemas/gui-animation-source.schema.json +176 -0
  411. package/schemas/gui-helper.schema.json +301 -0
  412. package/schemas/gui-scenario.schema.json +298 -0
  413. package/schemas/map-operation.schema.json +2284 -0
  414. package/schemas/operation-result.schema.json +344 -0
  415. package/schemas/transaction-manifest.schema.json +685 -0
  416. package/server.json +32 -0
@@ -0,0 +1,1181 @@
1
+ import { access, mkdir, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { constants } from 'node:fs';
4
+ import { hostname } from 'node:os';
5
+ import { ArtifactStore, publicArtifactLink } from './artifacts.js';
6
+ import { compareCodeUnits, canonicalJson, hashCanonical, secureId, sha256Bytes, } from './canonical.js';
7
+ import { binaryDiff, unifiedTextDiff } from './diff.js';
8
+ import { ServiceError } from './result.js';
9
+ import { decodeSource } from './source/encoding.js';
10
+ import { containedGeneratedPath, isWithin, resolvedWorkspaceIdentity } from './workspace.js';
11
+ import { TRANSACTION_MAX_ARTIFACTS, TRANSACTION_MAX_DIAGNOSTICS, TRANSACTION_MAX_FILES, TRANSACTION_MAX_MANIFEST_BYTES, TRANSACTION_MAX_OPERATIONS, TRANSACTION_MAX_READ_DEPENDENCIES, TRANSACTION_MAX_VALIDATION_CHECKS, } from './transaction-limits.js';
12
+ import { PACKAGE_VERSION, TRANSACTION_VERSION } from '../version.js';
13
+ import { transactionManifestSchema } from '../schemas/transaction.js';
14
+ export function readDependenciesFromScannedFiles(files) {
15
+ const unique = new Map();
16
+ for (const file of files) {
17
+ const dependency = {
18
+ rootKind: file.rootKind,
19
+ loadOrder: file.loadOrder,
20
+ relativePath: file.relativePath.replaceAll('\\', '/'),
21
+ sha256: file.sha256,
22
+ };
23
+ unique.set(`${dependency.rootKind}:${dependency.loadOrder}:${dependency.relativePath}`, dependency);
24
+ }
25
+ return [...unique.values()].sort((left, right) => left.loadOrder - right.loadOrder ||
26
+ compareCodeUnits(left.rootKind, right.rootKind) ||
27
+ compareCodeUnits(left.relativePath, right.relativePath));
28
+ }
29
+ const transactionManifestRangeMaxBytes = 1_048_576;
30
+ const transactionManifestCacheMaxBytes = TRANSACTION_MAX_MANIFEST_BYTES;
31
+ const transactionManifestCacheMaxEntries = 8;
32
+ function transactionManifestFileIdentity(value) {
33
+ return {
34
+ dev: value.dev,
35
+ ino: value.ino,
36
+ size: value.size,
37
+ mtimeMs: value.mtimeMs,
38
+ ctimeMs: value.ctimeMs,
39
+ };
40
+ }
41
+ function sameTransactionManifestIdentity(left, right) {
42
+ return (left.dev === right.dev &&
43
+ left.ino === right.ino &&
44
+ left.size === right.size &&
45
+ left.mtimeMs === right.mtimeMs &&
46
+ left.ctimeMs === right.ctimeMs);
47
+ }
48
+ const transactionIdPattern = /^txn_[0-9a-f-]{36}$/u;
49
+ const lockOwnerGraceMs = 30_000;
50
+ const lockHost = hostname().toLowerCase();
51
+ const lockInstanceId = secureId('instance');
52
+ const lockProcessStartedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();
53
+ async function fileExists(filePath) {
54
+ try {
55
+ await access(filePath, constants.F_OK);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function inferMediaType(relativePath) {
63
+ const extension = path.extname(relativePath).toLowerCase();
64
+ if (extension === '.bmp')
65
+ return 'image/bmp';
66
+ if (extension === '.png')
67
+ return 'image/png';
68
+ if (extension === '.svg')
69
+ return 'image/svg+xml';
70
+ if (extension === '.json')
71
+ return 'application/json';
72
+ if (['.txt', '.gui', '.gfx', '.yml', '.csv', '.md'].includes(extension))
73
+ return 'text/plain';
74
+ return 'application/octet-stream';
75
+ }
76
+ function isText(mediaType) {
77
+ return mediaType.startsWith('text/') || mediaType === 'application/json';
78
+ }
79
+ function sourceDiffText(bytes) {
80
+ if (bytes === null)
81
+ return '';
82
+ const decoded = decodeSource(bytes);
83
+ return `${decoded.encoding === 'utf8-bom' ? '\ufeff' : ''}${decoded.text}`;
84
+ }
85
+ async function durableWrite(filePath, content) {
86
+ await mkdir(path.dirname(filePath), { recursive: true });
87
+ const temporary = `${filePath}.${secureId('tmp')}`;
88
+ const handle = await open(temporary, 'wx');
89
+ try {
90
+ await handle.writeFile(content);
91
+ await handle.sync();
92
+ }
93
+ catch (error) {
94
+ await handle.close().catch(() => undefined);
95
+ await unlink(temporary).catch(() => undefined);
96
+ throw error;
97
+ }
98
+ finally {
99
+ await handle.close().catch(() => undefined);
100
+ }
101
+ try {
102
+ await rename(temporary, filePath);
103
+ }
104
+ catch (error) {
105
+ await unlink(temporary).catch(() => undefined);
106
+ throw error;
107
+ }
108
+ }
109
+ function manifestPlanPayload(manifest) {
110
+ return {
111
+ version: manifest.version,
112
+ workspaceId: manifest.workspaceId,
113
+ principal: manifest.principal ?? null,
114
+ rootFingerprint: manifest.rootFingerprint,
115
+ operationKind: manifest.operationKind,
116
+ operations: manifest.operations,
117
+ readDependencies: manifest.readDependencies,
118
+ files: manifest.files.map(({ diffArtifact: _diffArtifact, ...file }) => file),
119
+ diagnostics: manifest.diagnostics,
120
+ validation: manifest.validation,
121
+ artifacts: manifest.artifacts,
122
+ };
123
+ }
124
+ function immutablePlanPayload(payload) {
125
+ const { diagnostics: _diagnostics, validation: _validation, ...immutable } = payload;
126
+ return immutable;
127
+ }
128
+ function manifestIntegrityPayload(manifest) {
129
+ const { integrityHash: _integrityHash, authenticationTag: _authenticationTag, ...payload } = manifest;
130
+ return payload;
131
+ }
132
+ function manifestAuthenticationPayload(manifest) {
133
+ const { authenticationTag: _authenticationTag, ...payload } = manifest;
134
+ return payload;
135
+ }
136
+ export function transactionRootFingerprint(workspace) {
137
+ return resolvedWorkspaceIdentity(workspace);
138
+ }
139
+ export class TransactionManager {
140
+ resolver;
141
+ artifacts;
142
+ ttlSeconds;
143
+ maxJournalBytes;
144
+ maxJournals;
145
+ serverState;
146
+ #manifestByteCache = new Map();
147
+ #manifestByteCacheBytes = 0;
148
+ constructor(resolver, artifacts = new ArtifactStore(), ttlSeconds = 3600, maxJournalBytes = 536_870_912, maxJournals = 128, serverState = resolver.serverState()) {
149
+ this.resolver = resolver;
150
+ this.artifacts = artifacts;
151
+ this.ttlSeconds = ttlSeconds;
152
+ this.maxJournalBytes = maxJournalBytes;
153
+ this.maxJournals = maxJournals;
154
+ this.serverState = serverState;
155
+ }
156
+ async plan(input) {
157
+ input.signal?.throwIfAborted();
158
+ if (input.changes.length > TRANSACTION_MAX_FILES ||
159
+ input.operations.length > TRANSACTION_MAX_OPERATIONS ||
160
+ (input.readDependencies?.length ?? 0) > TRANSACTION_MAX_READ_DEPENDENCIES ||
161
+ (input.artifacts?.length ?? 0) > TRANSACTION_MAX_ARTIFACTS ||
162
+ (input.diagnostics?.length ?? 0) > TRANSACTION_MAX_DIAGNOSTICS) {
163
+ throw new ServiceError('TRANSACTION_STRUCTURE_LIMIT', 'Transaction input exceeds the supported review structure limits');
164
+ }
165
+ if (typeof input.validate !== 'function') {
166
+ throw new ServiceError('TRANSACTION_DRY_RUN_VALIDATION_REQUIRED', 'Every transaction plan requires in-memory validation');
167
+ }
168
+ const workspace = this.resolver.get(input.workspaceId, input.principal);
169
+ const sortedChanges = [...input.changes].sort((a, b) => compareCodeUnits(a.relativePath, b.relativePath));
170
+ const seen = new Set();
171
+ const seenTargets = new Map();
172
+ const resolvedChanges = [];
173
+ for (const change of sortedChanges) {
174
+ input.signal?.throwIfAborted();
175
+ if (seen.has(change.relativePath)) {
176
+ throw new ServiceError('TRANSACTION_DUPLICATE_FILE', `Transaction changes a file more than once: ${change.relativePath}`);
177
+ }
178
+ seen.add(change.relativePath);
179
+ const resolved = await this.resolver.resolvePath(input.workspaceId, change.relativePath, 'write', ['mod'], input.principal);
180
+ const targetKey = process.platform === 'win32' ? resolved.path.toLocaleLowerCase('en-US') : resolved.path;
181
+ const targetAlias = seenTargets.get(targetKey);
182
+ if (targetAlias !== undefined) {
183
+ throw new ServiceError('TRANSACTION_DUPLICATE_FILE', `Transaction path aliases target the same file: ${targetAlias} and ${change.relativePath}`);
184
+ }
185
+ seenTargets.set(targetKey, change.relativePath);
186
+ if (!isWithin(workspace.modRoot, resolved.path)) {
187
+ throw new ServiceError('TRANSACTION_PATH_OUTSIDE_MOD', 'Transaction targets a path outside the mod root');
188
+ }
189
+ resolvedChanges.push({ change, resolved });
190
+ }
191
+ const transactionId = secureId('txn');
192
+ const pendingBlobs = new Map();
193
+ let pendingBlobBytes = 0;
194
+ const rememberBlob = (bytes) => {
195
+ const hash = sha256Bytes(bytes);
196
+ if (!pendingBlobs.has(hash)) {
197
+ pendingBlobBytes += bytes.length;
198
+ if (!Number.isSafeInteger(pendingBlobBytes) || pendingBlobBytes > this.maxJournalBytes) {
199
+ throw new ServiceError('TRANSACTION_JOURNAL_LIMIT', 'Proposed rollback data exceeds the configured transaction-journal budget');
200
+ }
201
+ pendingBlobs.set(hash, bytes);
202
+ }
203
+ return path.join('blobs', hash).replaceAll('\\', '/');
204
+ };
205
+ const proposed = new Map();
206
+ const files = [];
207
+ const suppliedArtifacts = (input.artifacts ?? []).map(publicArtifactLink);
208
+ const pendingDiffs = [];
209
+ for (const { change, resolved } of resolvedChanges) {
210
+ input.signal?.throwIfAborted();
211
+ const before = (await fileExists(resolved.path)) ? await readFile(resolved.path) : null;
212
+ const after = change.content === null ? null : Buffer.from(change.content);
213
+ if (before !== null && after !== null && before.equals(after))
214
+ continue;
215
+ if (before === null && after === null)
216
+ continue;
217
+ proposed.set(change.relativePath, after);
218
+ const beforeSha256 = before === null ? null : sha256Bytes(before);
219
+ const afterSha256 = after === null ? null : sha256Bytes(after);
220
+ const beforeBlob = before === null ? null : rememberBlob(before);
221
+ const afterBlob = after === null ? null : rememberBlob(after);
222
+ const mediaType = change.mediaType ?? inferMediaType(change.relativePath);
223
+ pendingDiffs.push({ relativePath: change.relativePath, before, after });
224
+ files.push({
225
+ relativePath: change.relativePath,
226
+ operationIds: [...change.operationIds].sort((a, b) => compareCodeUnits(a, b)),
227
+ beforeSha256,
228
+ afterSha256,
229
+ beforeSize: before?.length ?? null,
230
+ afterSize: after?.length ?? null,
231
+ beforeBlob,
232
+ afterBlob,
233
+ mediaType,
234
+ });
235
+ }
236
+ const validation = await input.validate(proposed, input.signal);
237
+ if (validation.checks.length === 0) {
238
+ throw new ServiceError('TRANSACTION_DRY_RUN_VALIDATION_REQUIRED', 'Dry-run validation must report at least one explicit check');
239
+ }
240
+ const diagnostics = [...(input.diagnostics ?? []), ...validation.diagnostics];
241
+ if (validation.checks.length > TRANSACTION_MAX_VALIDATION_CHECKS ||
242
+ diagnostics.length > TRANSACTION_MAX_DIAGNOSTICS ||
243
+ suppliedArtifacts.length + files.length > TRANSACTION_MAX_ARTIFACTS) {
244
+ throw new ServiceError('TRANSACTION_STRUCTURE_LIMIT', 'Transaction validation or evidence exceeds the supported review structure limits');
245
+ }
246
+ const passed = !diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker') &&
247
+ validation.checks.every(({ passed }) => passed);
248
+ input.signal?.throwIfAborted();
249
+ const diffWrites = files.map((file, index) => {
250
+ const pending = pendingDiffs[index];
251
+ const sourceHashes = {
252
+ ...(file.beforeSha256 === null ? {} : { before: file.beforeSha256 }),
253
+ ...(file.afterSha256 === null ? {} : { after: file.afterSha256 }),
254
+ };
255
+ const sourceStateMetadata = {
256
+ beforeState: file.beforeSha256 === null ? 'missing' : 'present',
257
+ afterState: file.afterSha256 === null ? 'deleted' : 'present',
258
+ };
259
+ if (isText(file.mediaType)) {
260
+ return {
261
+ name: `${path.basename(pending.relativePath)}.diff`,
262
+ mimeType: 'text/x-diff',
263
+ content: unifiedTextDiff(sourceDiffText(pending.before), sourceDiffText(pending.after), `a/${pending.relativePath}`, `b/${pending.relativePath}`, { ...(input.signal === undefined ? {} : { signal: input.signal }) }),
264
+ provenance: {
265
+ kind: 'source-diff',
266
+ toolVersion: PACKAGE_VERSION,
267
+ schemaVersion: 'transaction.v1',
268
+ sourceHashes,
269
+ metadata: sourceStateMetadata,
270
+ },
271
+ };
272
+ }
273
+ return {
274
+ name: `${path.basename(pending.relativePath)}.binary-diff.json`,
275
+ mimeType: 'application/json',
276
+ content: `${canonicalJson(binaryDiff(pending.before ?? Buffer.alloc(0), pending.after ?? Buffer.alloc(0)))}\n`,
277
+ provenance: {
278
+ kind: 'binary-diff',
279
+ toolVersion: PACKAGE_VERSION,
280
+ schemaVersion: 'transaction.v1',
281
+ sourceHashes,
282
+ metadata: sourceStateMetadata,
283
+ },
284
+ };
285
+ });
286
+ return this.artifacts.withAtomicWrites(workspace, diffWrites, async (storedDiffs) => {
287
+ input.signal?.throwIfAborted();
288
+ const diffArtifacts = storedDiffs.map(publicArtifactLink);
289
+ const filesWithDiffs = files.map((file, index) => ({
290
+ ...file,
291
+ diffArtifact: diffArtifacts[index],
292
+ }));
293
+ const artifacts = [...suppliedArtifacts, ...diffArtifacts];
294
+ const now = Date.now();
295
+ const manifestWithoutHashes = {
296
+ version: TRANSACTION_VERSION,
297
+ revision: 1,
298
+ transactionId,
299
+ workspaceId: input.workspaceId,
300
+ ...(input.principal === undefined ? {} : { principal: input.principal }),
301
+ rootFingerprint: this.rootFingerprint(workspace),
302
+ createdAt: new Date(now).toISOString(),
303
+ expiresAt: new Date(now + this.ttlSeconds * 1000).toISOString(),
304
+ state: 'planned',
305
+ operationKind: input.operationKind,
306
+ operations: input.operations,
307
+ readDependencies: [...(input.readDependencies ?? [])].sort((left, right) => left.loadOrder - right.loadOrder ||
308
+ compareCodeUnits(left.rootKind, right.rootKind) ||
309
+ compareCodeUnits(left.relativePath, right.relativePath)),
310
+ files: filesWithDiffs,
311
+ diagnostics,
312
+ validation: { passed, checks: validation.checks },
313
+ artifacts,
314
+ appliedFiles: [],
315
+ rollbackStatus: 'available',
316
+ };
317
+ const planPayload = manifestPlanPayload(manifestWithoutHashes);
318
+ const manifest = {
319
+ ...manifestWithoutHashes,
320
+ planPayload,
321
+ planHash: hashCanonical(planPayload),
322
+ integrityHash: '',
323
+ authenticationTag: '',
324
+ };
325
+ await this.commitPlannedJournal(workspace, manifest, pendingBlobs);
326
+ return manifest;
327
+ }, input.signal);
328
+ }
329
+ async apply(workspaceId, transactionId, expectedPlanHash, options) {
330
+ if (options === undefined || typeof options.postValidate !== 'function') {
331
+ throw new ServiceError('TRANSACTION_POST_VALIDATION_REQUIRED', 'Every transaction apply requires post-write validation');
332
+ }
333
+ options.signal?.throwIfAborted();
334
+ const workspace = this.resolver.get(workspaceId, options.principal);
335
+ if (!workspace.writeEnabled)
336
+ throw new ServiceError('WRITE_POLICY_DISABLED', 'Workspace writes are not enabled');
337
+ const manifest = await this.load(workspace, transactionId, {
338
+ headMode: 'reconcile',
339
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
340
+ });
341
+ this.assertBinding(manifest, workspace, expectedPlanHash, options.principal);
342
+ if (!manifest.planPayload.validation.passed)
343
+ throw new ServiceError('TRANSACTION_VALIDATION_BLOCKED', 'Dry-run validation did not pass');
344
+ if (manifest.files.length === 0)
345
+ throw new ServiceError('TRANSACTION_NO_CHANGES', 'Transaction contains no file changes');
346
+ if (manifest.state !== 'planned')
347
+ throw new ServiceError('TRANSACTION_STATE_INVALID', `Transaction is ${manifest.state}`);
348
+ if (Date.parse(manifest.expiresAt) <= Date.now())
349
+ throw new ServiceError('TRANSACTION_EXPIRED', 'Transaction has expired');
350
+ return this.withWorkspaceLock(workspace, transactionId, async () => {
351
+ await this.verifyReadDependencies(workspace, manifest, options.principal, options.signal);
352
+ await this.verifyCurrentFiles(workspace, manifest, 'before', options.principal, options.signal);
353
+ const targets = [];
354
+ try {
355
+ manifest.state = 'applying';
356
+ await this.writeManifest(workspace, manifest, options.hooks);
357
+ for (const [index, file] of manifest.files.entries()) {
358
+ options.signal?.throwIfAborted();
359
+ const { path: target } = await this.resolver.resolvePath(workspaceId, file.relativePath, 'write', ['mod'], options.principal);
360
+ await mkdir(path.dirname(target), { recursive: true });
361
+ const staged = file.afterBlob === null ? null : `${target}.hoi4-agent-${transactionId}.stage`;
362
+ const backup = `${target}.hoi4-agent-${transactionId}.backup`;
363
+ if (staged !== null && file.afterBlob !== null) {
364
+ const bytes = await this.readVerifiedBlob(workspace, manifest, file, 'after');
365
+ await durableWrite(staged, bytes);
366
+ }
367
+ targets.push({ target, staged, backup });
368
+ await options.hooks?.afterStage?.(file.relativePath, index);
369
+ }
370
+ for (const [index, file] of manifest.files.entries()) {
371
+ options.signal?.throwIfAborted();
372
+ const target = targets[index];
373
+ const existsBeforeReplace = await fileExists(target.target);
374
+ const actualBeforeReplace = existsBeforeReplace
375
+ ? sha256Bytes(await readFile(target.target))
376
+ : null;
377
+ if (actualBeforeReplace !== file.beforeSha256) {
378
+ throw new ServiceError('TRANSACTION_STALE', 'Source changed while transaction output was being staged');
379
+ }
380
+ if (existsBeforeReplace)
381
+ await rename(target.target, target.backup);
382
+ await options.hooks?.afterBackup?.(file.relativePath, index);
383
+ if (target.staged !== null) {
384
+ if (await fileExists(target.target)) {
385
+ throw new ServiceError('TRANSACTION_STALE', 'Source target was recreated during atomic replacement');
386
+ }
387
+ await rename(target.staged, target.target);
388
+ }
389
+ manifest.appliedFiles.push(file.relativePath);
390
+ await this.writeManifest(workspace, manifest, options.hooks);
391
+ await options.hooks?.afterReplace?.(file.relativePath, index);
392
+ }
393
+ await options.hooks?.beforePostValidation?.();
394
+ const result = await options.postValidate(manifest, options.signal);
395
+ if (result.checks.length === 0) {
396
+ throw new ServiceError('TRANSACTION_POST_VALIDATION_REQUIRED', 'Post-write validation must report at least one explicit check');
397
+ }
398
+ if (manifest.diagnostics.length + result.diagnostics.length > TRANSACTION_MAX_DIAGNOSTICS ||
399
+ manifest.validation.checks.length + result.checks.length >
400
+ TRANSACTION_MAX_VALIDATION_CHECKS) {
401
+ throw new ServiceError('TRANSACTION_STRUCTURE_LIMIT', 'Post-write validation exceeds the supported transaction review structure limits');
402
+ }
403
+ manifest.diagnostics.push(...result.diagnostics);
404
+ manifest.validation.checks.push(...result.checks);
405
+ manifest.validation.passed =
406
+ manifest.validation.passed &&
407
+ result.checks.every(({ passed }) => passed) &&
408
+ !result.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
409
+ if (!manifest.validation.passed) {
410
+ throw new ServiceError('TRANSACTION_POST_VALIDATION_FAILED', 'Post-write validation failed');
411
+ }
412
+ await this.verifyCurrentFiles(workspace, manifest, 'after', options.principal, options.signal);
413
+ for (const target of targets) {
414
+ if (await fileExists(target.backup))
415
+ await unlink(target.backup);
416
+ }
417
+ manifest.state = 'applied';
418
+ await this.writeManifest(workspace, manifest, options.hooks);
419
+ return manifest;
420
+ }
421
+ catch (error) {
422
+ manifest.failure = this.safeFailure(error, 'TRANSACTION_APPLY_FAILED', 'Transaction apply failed');
423
+ let rollbackError;
424
+ try {
425
+ await this.restoreBeforeState(workspace, manifest, options.principal, options.hooks);
426
+ }
427
+ catch (caught) {
428
+ rollbackError = caught;
429
+ manifest.rollbackStatus = 'failed';
430
+ manifest.failure = this.safeFailure(caught, 'TRANSACTION_ROLLBACK_FAILED', 'Automatic rollback could not safely complete');
431
+ }
432
+ manifest.state = manifest.rollbackStatus === 'applied' ? 'rolled_back' : 'failed';
433
+ await this.writeManifest(workspace, manifest, options.hooks);
434
+ if (rollbackError instanceof ServiceError)
435
+ throw rollbackError;
436
+ if (rollbackError !== undefined) {
437
+ throw new ServiceError('TRANSACTION_ROLLBACK_FAILED', 'Automatic rollback could not safely complete');
438
+ }
439
+ if (error instanceof ServiceError)
440
+ throw error;
441
+ throw new ServiceError('TRANSACTION_APPLY_FAILED', 'Transaction apply failed');
442
+ }
443
+ });
444
+ }
445
+ async rollback(workspaceId, transactionId, expectedPlanHash, principal, signal) {
446
+ signal?.throwIfAborted();
447
+ const workspace = this.resolver.get(workspaceId, principal);
448
+ if (!workspace.writeEnabled)
449
+ throw new ServiceError('WRITE_POLICY_DISABLED', 'Workspace writes are not enabled');
450
+ const manifest = await this.load(workspace, transactionId, {
451
+ headMode: 'reconcile',
452
+ ...(signal === undefined ? {} : { signal }),
453
+ });
454
+ this.assertBinding(manifest, workspace, expectedPlanHash, principal);
455
+ if (manifest.state !== 'applied')
456
+ throw new ServiceError('TRANSACTION_NOT_APPLIED', 'Only applied transactions can be rolled back');
457
+ return this.withWorkspaceLock(workspace, transactionId, async () => {
458
+ signal?.throwIfAborted();
459
+ await this.verifyCurrentFiles(workspace, manifest, 'after', principal, signal);
460
+ signal?.throwIfAborted();
461
+ // Restoring source bytes and advancing the authenticated journal are one
462
+ // non-cancellable critical phase once preflight has completed.
463
+ await this.restoreBeforeState(workspace, manifest, principal);
464
+ manifest.state = 'rolled_back';
465
+ await this.writeManifest(workspace, manifest);
466
+ return manifest;
467
+ });
468
+ }
469
+ async status(workspaceId, transactionId, principal, signal) {
470
+ signal?.throwIfAborted();
471
+ const manifest = await this.load(this.resolver.get(workspaceId, principal), transactionId, {
472
+ ...(signal === undefined ? {} : { signal }),
473
+ });
474
+ if ((manifest.principal ?? null) !== (principal ?? null)) {
475
+ throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction belongs to another principal');
476
+ }
477
+ return manifest;
478
+ }
479
+ async readManifestRange(workspaceId, transactionId, range, principal, signal) {
480
+ signal?.throwIfAborted();
481
+ if (!Number.isSafeInteger(range.offset) ||
482
+ !Number.isSafeInteger(range.length) ||
483
+ range.offset < 0 ||
484
+ range.length < 1 ||
485
+ range.length > transactionManifestRangeMaxBytes) {
486
+ throw new ServiceError('TRANSACTION_RANGE_INVALID', `Transaction manifest ranges must request 1 through ${transactionManifestRangeMaxBytes} bytes`);
487
+ }
488
+ const workspace = this.resolver.get(workspaceId, principal);
489
+ const cacheKey = this.manifestCacheKey(workspace, transactionId);
490
+ const manifestPath = await this.manifestPath(workspace, transactionId);
491
+ signal?.throwIfAborted();
492
+ let metadata;
493
+ try {
494
+ metadata = transactionManifestFileIdentity(await stat(manifestPath));
495
+ }
496
+ catch (error) {
497
+ if (error.name === 'AbortError')
498
+ throw error;
499
+ this.removeManifestByteCache(cacheKey);
500
+ throw new ServiceError('TRANSACTION_NOT_FOUND', 'Transaction was not found');
501
+ }
502
+ signal?.throwIfAborted();
503
+ let cached = this.#manifestByteCache.get(cacheKey);
504
+ if (cached !== undefined && sameTransactionManifestIdentity(cached.identity, metadata)) {
505
+ if (cached.principal !== (principal ?? null)) {
506
+ throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction belongs to another principal');
507
+ }
508
+ await this.requireServerState().verifyJournalHead({
509
+ workspaceIdentity: workspace.workspaceIdentity,
510
+ transactionId,
511
+ revision: cached.revision,
512
+ authenticationTag: cached.authenticationTag,
513
+ manifestHash: cached.manifestHash,
514
+ }, signal);
515
+ this.#manifestByteCache.delete(cacheKey);
516
+ this.#manifestByteCache.set(cacheKey, cached);
517
+ }
518
+ else {
519
+ this.removeManifestByteCache(cacheKey);
520
+ const manifest = await this.load(workspace, transactionId, {
521
+ cacheBytes: true,
522
+ ...(signal === undefined ? {} : { signal }),
523
+ });
524
+ if ((manifest.principal ?? null) !== (principal ?? null)) {
525
+ throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction belongs to another principal');
526
+ }
527
+ cached = this.#manifestByteCache.get(cacheKey);
528
+ if (cached === undefined) {
529
+ throw new ServiceError('TRANSACTION_MANIFEST_INVALID', 'Transaction manifest could not be prepared for bounded resource access');
530
+ }
531
+ }
532
+ signal?.throwIfAborted();
533
+ const start = Math.min(range.offset, cached.bytes.length);
534
+ return {
535
+ bytes: Buffer.from(cached.bytes.subarray(start, Math.min(cached.bytes.length, start + range.length))),
536
+ totalSize: cached.bytes.length,
537
+ };
538
+ }
539
+ async recover(workspaceId, principal, signal) {
540
+ signal?.throwIfAborted();
541
+ const workspace = this.resolver.get(workspaceId, principal);
542
+ const directory = await containedGeneratedPath(workspace.cacheRoot, 'transactions');
543
+ signal?.throwIfAborted();
544
+ if (!(await fileExists(directory))) {
545
+ signal?.throwIfAborted();
546
+ await this.removeOrphanJournalHeads(workspace, new Set(), signal);
547
+ return [];
548
+ }
549
+ const directoryEntries = await readdir(directory, { withFileTypes: true });
550
+ signal?.throwIfAborted();
551
+ const transactionIds = new Set(directoryEntries
552
+ .filter((entry) => entry.isDirectory() && transactionIdPattern.test(entry.name))
553
+ .map((entry) => entry.name));
554
+ await this.removeOrphanJournalHeads(workspace, transactionIds, signal);
555
+ const recovered = [];
556
+ for (const entry of directoryEntries.filter((value) => value.isDirectory())) {
557
+ signal?.throwIfAborted();
558
+ if (!transactionIdPattern.test(entry.name))
559
+ continue;
560
+ const manifest = await this.load(workspace, entry.name, {
561
+ headMode: 'reconcile',
562
+ ...(signal === undefined ? {} : { signal }),
563
+ });
564
+ if (principal !== undefined && (manifest.principal ?? null) !== principal) {
565
+ throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction recovery belongs to another principal');
566
+ }
567
+ if (manifest.state !== 'applying' && manifest.state !== 'rolling_back')
568
+ continue;
569
+ await this.withWorkspaceLock(workspace, manifest.transactionId, async () => {
570
+ signal?.throwIfAborted();
571
+ // Once source restoration starts, recovery and authenticated journal
572
+ // advancement form a non-cancellable critical phase.
573
+ try {
574
+ await this.restoreBeforeState(workspace, manifest, principal);
575
+ manifest.state = 'rolled_back';
576
+ manifest.failure = {
577
+ code: 'TRANSACTION_RECOVERED',
578
+ message: 'Incomplete transaction was rolled back during recovery',
579
+ };
580
+ await this.writeManifest(workspace, manifest);
581
+ }
582
+ catch (error) {
583
+ manifest.rollbackStatus = 'failed';
584
+ manifest.state = 'failed';
585
+ manifest.failure = this.safeFailure(error, 'TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
586
+ await this.writeManifest(workspace, manifest);
587
+ if (error instanceof ServiceError)
588
+ throw error;
589
+ throw new ServiceError('TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
590
+ }
591
+ });
592
+ recovered.push(manifest);
593
+ }
594
+ return recovered;
595
+ }
596
+ async restoreBeforeState(workspace, manifest, principal, hooks) {
597
+ const restorations = [];
598
+ // Prove that every required rollback source is intact before changing any
599
+ // current file. This keeps a corrupt cache from turning a safe refusal into
600
+ // source loss halfway through rollback.
601
+ for (const file of manifest.files) {
602
+ const { path: target } = await this.resolver.resolvePath(workspace.id, file.relativePath, 'write', ['mod'], principal);
603
+ const backup = `${target}.hoi4-agent-${manifest.transactionId}.backup`;
604
+ const staged = `${target}.hoi4-agent-${manifest.transactionId}.stage`;
605
+ const current = (await fileExists(target)) ? await readFile(target) : null;
606
+ const currentHash = current === null ? null : sha256Bytes(current);
607
+ const backupExists = await fileExists(backup);
608
+ let backupValid = false;
609
+ if (backupExists) {
610
+ if (file.beforeSha256 === null) {
611
+ throw new ServiceError('TRANSACTION_ROLLBACK_STALE', 'Rollback encountered unexpected backup state');
612
+ }
613
+ const backupBytes = await readFile(backup);
614
+ if (sha256Bytes(backupBytes) !== file.beforeSha256) {
615
+ throw new ServiceError('TRANSACTION_BLOB_INTEGRITY_FAILED', 'Rollback backup failed integrity validation');
616
+ }
617
+ backupValid = true;
618
+ }
619
+ if (currentHash === file.beforeSha256) {
620
+ restorations.push({ file, target, backup, staged, mode: 'keep' });
621
+ continue;
622
+ }
623
+ const matchesAfter = currentHash === file.afterSha256;
624
+ const missingAfterBackup = currentHash === null && file.afterSha256 !== null && backupValid;
625
+ // Versions before the atomic replacement fix could crash after unlinking an
626
+ // applied target but before restoring its persisted before-blob. A durable
627
+ // rolling_back manifest proves rollback had already started; the verified
628
+ // before-blob is therefore the only safe recovery source when no backup is
629
+ // present. Never grant this exception to applying or terminal journals.
630
+ const missingDuringBlobRollback = manifest.state === 'rolling_back' &&
631
+ currentHash === null &&
632
+ file.beforeSha256 !== null &&
633
+ !backupValid;
634
+ if (!matchesAfter && !missingAfterBackup && !missingDuringBlobRollback) {
635
+ throw new ServiceError('TRANSACTION_ROLLBACK_STALE', 'Rollback refused to overwrite source bytes not produced by this transaction');
636
+ }
637
+ if (file.beforeSha256 === null) {
638
+ restorations.push({ file, target, backup, staged, mode: 'delete' });
639
+ continue;
640
+ }
641
+ if (backupValid) {
642
+ restorations.push({ file, target, backup, staged, mode: 'backup' });
643
+ continue;
644
+ }
645
+ restorations.push({
646
+ file,
647
+ target,
648
+ backup,
649
+ staged,
650
+ mode: 'blob',
651
+ bytes: await this.readVerifiedBlob(workspace, manifest, file, 'before'),
652
+ });
653
+ }
654
+ manifest.state = 'rolling_back';
655
+ await this.writeManifest(workspace, manifest, hooks);
656
+ for (const restoration of restorations.reverse()) {
657
+ if (restoration.mode === 'backup') {
658
+ // Renaming over the target is the atomic replacement primitive. The
659
+ // verified backup remains available if replacement fails or the process
660
+ // stops before the rename completes.
661
+ await rename(restoration.backup, restoration.target);
662
+ }
663
+ else if (restoration.mode === 'blob') {
664
+ // durableWrite stages and fsyncs a same-directory temporary before an
665
+ // atomic rename over the target. Do not unlink first: doing so creates a
666
+ // crash window in which neither old nor restored bytes occupy the path.
667
+ await durableWrite(restoration.target, restoration.bytes);
668
+ if (await fileExists(restoration.backup))
669
+ await unlink(restoration.backup);
670
+ }
671
+ else if (restoration.mode === 'delete') {
672
+ if (await fileExists(restoration.target))
673
+ await unlink(restoration.target);
674
+ if (await fileExists(restoration.backup))
675
+ await unlink(restoration.backup);
676
+ }
677
+ else if (await fileExists(restoration.backup)) {
678
+ await unlink(restoration.backup);
679
+ }
680
+ if (await fileExists(restoration.staged))
681
+ await unlink(restoration.staged);
682
+ }
683
+ await this.verifyCurrentFiles(workspace, manifest, 'before', principal);
684
+ manifest.rollbackStatus = 'applied';
685
+ }
686
+ safeFailure(error, fallbackCode, message) {
687
+ return {
688
+ code: error instanceof ServiceError ? error.code : fallbackCode,
689
+ message,
690
+ };
691
+ }
692
+ assertBinding(manifest, workspace, expectedPlanHash, principal) {
693
+ if (manifest.workspaceId !== workspace.id)
694
+ throw new ServiceError('TRANSACTION_WORKSPACE_MISMATCH', 'Transaction belongs to another workspace');
695
+ if (manifest.planHash !== expectedPlanHash)
696
+ throw new ServiceError('TRANSACTION_PLAN_HASH_MISMATCH', 'Expected plan hash does not match transaction');
697
+ if (manifest.rootFingerprint !== this.rootFingerprint(workspace))
698
+ throw new ServiceError('TRANSACTION_ROOT_CHANGED', 'Workspace root configuration changed after planning');
699
+ if ((manifest.principal ?? null) !== (principal ?? null))
700
+ throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction belongs to another principal');
701
+ }
702
+ async verifyCurrentFiles(workspace, manifest, phase, principal, signal) {
703
+ for (const file of manifest.files) {
704
+ signal?.throwIfAborted();
705
+ const { path: target } = await this.resolver.resolvePath(workspace.id, file.relativePath, 'write', ['mod'], principal);
706
+ const exists = await fileExists(target);
707
+ const actual = exists ? sha256Bytes(await readFile(target)) : null;
708
+ const expected = phase === 'before' ? file.beforeSha256 : file.afterSha256;
709
+ if (actual !== expected) {
710
+ throw new ServiceError(phase === 'before' ? 'TRANSACTION_STALE' : 'TRANSACTION_WRITE_MISMATCH', `${file.relativePath} does not match the ${phase} hash`, { relativePath: file.relativePath, expected, actual });
711
+ }
712
+ }
713
+ }
714
+ async verifyReadDependencies(workspace, manifest, principal, signal) {
715
+ for (const dependency of manifest.readDependencies) {
716
+ signal?.throwIfAborted();
717
+ let actual;
718
+ try {
719
+ const resolved = await this.resolver.resolvePathInRoot(workspace.id, dependency.relativePath, dependency.rootKind, dependency.loadOrder, principal);
720
+ const bytes = signal === undefined
721
+ ? await readFile(resolved.path)
722
+ : await readFile(resolved.path, { signal });
723
+ signal?.throwIfAborted();
724
+ actual = sha256Bytes(bytes);
725
+ }
726
+ catch (error) {
727
+ if (error instanceof ServiceError && error.code.startsWith('PATH_'))
728
+ actual = null;
729
+ else
730
+ throw error;
731
+ }
732
+ if (actual !== dependency.sha256) {
733
+ throw new ServiceError('TRANSACTION_SOURCE_STALE', `${dependency.relativePath} changed in a source root after planning`, {
734
+ relativePath: dependency.relativePath,
735
+ rootKind: dependency.rootKind,
736
+ loadOrder: dependency.loadOrder,
737
+ expected: dependency.sha256,
738
+ actual,
739
+ });
740
+ }
741
+ }
742
+ }
743
+ async commitPlannedJournal(workspace, manifest, blobs) {
744
+ await this.withWorkspaceLock(workspace, manifest.transactionId, async () => {
745
+ let transactionsDirectory = await containedGeneratedPath(workspace.cacheRoot, 'transactions');
746
+ await mkdir(transactionsDirectory, { recursive: true });
747
+ transactionsDirectory = await containedGeneratedPath(workspace.cacheRoot, 'transactions');
748
+ await this.pruneExpiredJournals(workspace, transactionsDirectory);
749
+ const usage = await this.journalUsage(transactionsDirectory);
750
+ this.authenticateManifest(manifest);
751
+ const serializedManifest = `${canonicalJson(manifest)}\n`;
752
+ if (Buffer.byteLength(serializedManifest, 'utf8') > TRANSACTION_MAX_MANIFEST_BYTES) {
753
+ throw new ServiceError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
754
+ }
755
+ const blobBytes = [...blobs.values()].reduce((total, bytes) => total + bytes.length, 0);
756
+ const prospectiveBytes = blobBytes + Buffer.byteLength(serializedManifest, 'utf8');
757
+ const replacementBytes = manifest.files.reduce((total, file) => total + (file.beforeSize ?? 0) + (file.afterSize ?? 0), 0);
758
+ const transactionWorkBytes = prospectiveBytes + replacementBytes;
759
+ if (!Number.isSafeInteger(prospectiveBytes) ||
760
+ !Number.isSafeInteger(transactionWorkBytes) ||
761
+ usage.count >= this.maxJournals ||
762
+ transactionWorkBytes > this.maxJournalBytes ||
763
+ usage.bytes > this.maxJournalBytes - prospectiveBytes) {
764
+ throw new ServiceError('TRANSACTION_JOURNAL_LIMIT', 'Transaction journal retention limit has been reached');
765
+ }
766
+ const transactionDirectory = await this.transactionDirectory(workspace, manifest.transactionId);
767
+ try {
768
+ const blobsDirectory = await containedGeneratedPath(transactionDirectory, 'blobs');
769
+ await mkdir(blobsDirectory, { recursive: true });
770
+ for (const [hash, bytes] of blobs) {
771
+ await durableWrite(await containedGeneratedPath(blobsDirectory, hash), bytes);
772
+ }
773
+ await durableWrite(await containedGeneratedPath(transactionDirectory, 'manifest.json'), serializedManifest);
774
+ await this.recordInitialManifestHead(workspace, manifest);
775
+ }
776
+ catch (error) {
777
+ await rm(transactionDirectory, { recursive: true, force: true }).catch(() => undefined);
778
+ throw error;
779
+ }
780
+ });
781
+ }
782
+ async pruneExpiredJournals(workspace, transactionsDirectory) {
783
+ const now = Date.now();
784
+ const entries = await readdir(transactionsDirectory, { withFileTypes: true });
785
+ const cacheTransactionIds = new Set(entries.filter((entry) => transactionIdPattern.test(entry.name)).map((entry) => entry.name));
786
+ await this.removeOrphanJournalHeads(workspace, cacheTransactionIds);
787
+ for (const entry of entries) {
788
+ if (!transactionIdPattern.test(entry.name))
789
+ continue;
790
+ if (entry.isSymbolicLink()) {
791
+ throw new ServiceError('TRANSACTION_JOURNAL_UNSAFE', 'Transaction journal contains a symbolic link or junction');
792
+ }
793
+ if (!entry.isDirectory())
794
+ continue;
795
+ const directory = await this.transactionDirectory(workspace, entry.name);
796
+ const manifestPath = await containedGeneratedPath(directory, 'manifest.json');
797
+ if (!(await fileExists(manifestPath))) {
798
+ const metadata = await stat(directory);
799
+ if (now - metadata.mtimeMs > this.ttlSeconds * 1000) {
800
+ await this.requireServerState().removeJournalHead(workspace.workspaceIdentity, entry.name);
801
+ await rm(directory, { recursive: true, force: true });
802
+ }
803
+ continue;
804
+ }
805
+ let manifest;
806
+ try {
807
+ manifest = await this.load(workspace, entry.name, { headMode: 'reconcile' });
808
+ }
809
+ catch (error) {
810
+ if (error instanceof ServiceError && error.code === 'TRANSACTION_HEAD_MISSING') {
811
+ try {
812
+ const orphan = await this.load(workspace, entry.name, { headMode: 'none' });
813
+ if (orphan.state === 'planned' || orphan.state === 'rolled_back') {
814
+ await rm(directory, { recursive: true, force: true });
815
+ continue;
816
+ }
817
+ }
818
+ catch {
819
+ // Malformed or unauthenticated orphan journals remain visible to quota accounting.
820
+ }
821
+ }
822
+ continue;
823
+ }
824
+ if (Date.parse(manifest.expiresAt) <= now &&
825
+ (manifest.state === 'planned' || manifest.state === 'rolled_back')) {
826
+ // Removing protected state first makes a crash or later cache replay fail closed.
827
+ await this.requireServerState().removeJournalHead(workspace.workspaceIdentity, entry.name);
828
+ await rm(directory, { recursive: true, force: true });
829
+ }
830
+ }
831
+ }
832
+ async removeOrphanJournalHeads(workspace, cacheTransactionIds, signal) {
833
+ if (this.serverState === undefined)
834
+ return;
835
+ for (const transactionId of await this.serverState.listJournalHeadTransactionIds(workspace.workspaceIdentity, signal)) {
836
+ signal?.throwIfAborted();
837
+ if (!cacheTransactionIds.has(transactionId)) {
838
+ await this.requireServerState().removeJournalHead(workspace.workspaceIdentity, transactionId);
839
+ }
840
+ }
841
+ }
842
+ async journalUsage(transactionsDirectory) {
843
+ let count = 0;
844
+ let bytes = 0;
845
+ for (const entry of await readdir(transactionsDirectory, { withFileTypes: true })) {
846
+ if (!transactionIdPattern.test(entry.name))
847
+ continue;
848
+ if (entry.isSymbolicLink()) {
849
+ throw new ServiceError('TRANSACTION_JOURNAL_UNSAFE', 'Transaction journal contains a symbolic link or junction');
850
+ }
851
+ if (!entry.isDirectory())
852
+ continue;
853
+ count += 1;
854
+ bytes += await this.directoryBytes(await containedGeneratedPath(transactionsDirectory, entry.name));
855
+ if (!Number.isSafeInteger(bytes)) {
856
+ throw new ServiceError('TRANSACTION_JOURNAL_LIMIT', 'Transaction journal size cannot be represented safely');
857
+ }
858
+ }
859
+ return { count, bytes };
860
+ }
861
+ async directoryBytes(directory) {
862
+ let bytes = 0;
863
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
864
+ if (entry.isSymbolicLink()) {
865
+ throw new ServiceError('TRANSACTION_JOURNAL_UNSAFE', 'Transaction journal contains a symbolic link or junction');
866
+ }
867
+ const candidate = await containedGeneratedPath(directory, entry.name);
868
+ if (entry.isDirectory())
869
+ bytes += await this.directoryBytes(candidate);
870
+ else if (entry.isFile())
871
+ bytes += (await stat(candidate)).size;
872
+ if (!Number.isSafeInteger(bytes)) {
873
+ throw new ServiceError('TRANSACTION_JOURNAL_LIMIT', 'Transaction journal size cannot be represented safely');
874
+ }
875
+ }
876
+ return bytes;
877
+ }
878
+ async readVerifiedBlob(workspace, manifest, file, phase) {
879
+ const expectedHash = phase === 'before' ? file.beforeSha256 : file.afterSha256;
880
+ const expectedSize = phase === 'before' ? file.beforeSize : file.afterSize;
881
+ const blob = phase === 'before' ? file.beforeBlob : file.afterBlob;
882
+ if (expectedHash === null || expectedSize === null || blob === null) {
883
+ throw new ServiceError('TRANSACTION_BLOB_REFERENCE_INVALID', `${file.relativePath} has an incomplete ${phase} blob reference`, { relativePath: file.relativePath, phase });
884
+ }
885
+ const expectedBlob = path.join('blobs', expectedHash).replaceAll('\\', '/');
886
+ if (blob !== expectedBlob) {
887
+ throw new ServiceError('TRANSACTION_BLOB_REFERENCE_INVALID', `${file.relativePath} has an invalid ${phase} blob path`, { relativePath: file.relativePath, phase });
888
+ }
889
+ let bytes;
890
+ try {
891
+ const transactionDirectory = await this.transactionDirectory(workspace, manifest.transactionId);
892
+ bytes = await readFile(await containedGeneratedPath(transactionDirectory, blob));
893
+ }
894
+ catch (_error) {
895
+ throw new ServiceError('TRANSACTION_BLOB_INTEGRITY_FAILED', `${file.relativePath} ${phase} rollback data is missing`, {
896
+ relativePath: file.relativePath,
897
+ phase,
898
+ });
899
+ }
900
+ if (bytes.length !== expectedSize || sha256Bytes(bytes) !== expectedHash) {
901
+ throw new ServiceError('TRANSACTION_BLOB_INTEGRITY_FAILED', `${file.relativePath} ${phase} rollback data failed integrity validation`, { relativePath: file.relativePath, phase });
902
+ }
903
+ return bytes;
904
+ }
905
+ rootFingerprint(workspace) {
906
+ return transactionRootFingerprint(workspace);
907
+ }
908
+ requireServerState() {
909
+ if (this.serverState === undefined) {
910
+ throw new ServiceError('TRANSACTION_AUTHENTICATION_UNAVAILABLE', 'Authenticated transaction journals require an operator-controlled server state root');
911
+ }
912
+ return this.serverState;
913
+ }
914
+ authenticateManifest(manifest) {
915
+ manifest.integrityHash = hashCanonical(manifestIntegrityPayload(manifest));
916
+ manifest.authenticationTag = this.requireServerState().authenticateJournal(manifestAuthenticationPayload(manifest));
917
+ }
918
+ journalHeadInput(workspace, manifest, manifestHash = hashCanonical(manifest)) {
919
+ return {
920
+ workspaceIdentity: workspace.workspaceIdentity,
921
+ transactionId: manifest.transactionId,
922
+ revision: manifest.revision,
923
+ authenticationTag: manifest.authenticationTag,
924
+ manifestHash,
925
+ };
926
+ }
927
+ async recordInitialManifestHead(workspace, manifest) {
928
+ await this.requireServerState().recordInitialJournalHead(this.journalHeadInput(workspace, manifest));
929
+ }
930
+ async recordManifestSuccessor(workspace, manifest) {
931
+ await this.requireServerState().recordJournalSuccessor(this.journalHeadInput(workspace, manifest));
932
+ }
933
+ async verifyManifestHead(workspace, manifest, mode, signal, manifestHash) {
934
+ const input = this.journalHeadInput(workspace, manifest, manifestHash);
935
+ if (mode === 'reconcile') {
936
+ await this.requireServerState().verifyOrReconcileJournalHead(input, signal);
937
+ return;
938
+ }
939
+ await this.requireServerState().verifyJournalHead(input, signal);
940
+ }
941
+ async transactionDirectory(workspace, transactionId) {
942
+ if (!transactionIdPattern.test(transactionId))
943
+ throw new ServiceError('TRANSACTION_ID_INVALID', 'Invalid transaction ID');
944
+ return containedGeneratedPath(workspace.cacheRoot, 'transactions', transactionId);
945
+ }
946
+ async manifestPath(workspace, transactionId) {
947
+ return containedGeneratedPath(await this.transactionDirectory(workspace, transactionId), 'manifest.json');
948
+ }
949
+ manifestCacheKey(workspace, transactionId) {
950
+ return `${workspace.workspaceIdentity}\0${transactionId}`;
951
+ }
952
+ removeManifestByteCache(cacheKey) {
953
+ const existing = this.#manifestByteCache.get(cacheKey);
954
+ if (existing === undefined)
955
+ return;
956
+ this.#manifestByteCache.delete(cacheKey);
957
+ this.#manifestByteCacheBytes -= existing.bytes.length;
958
+ }
959
+ storeManifestByteCache(cacheKey, entry) {
960
+ this.removeManifestByteCache(cacheKey);
961
+ this.#manifestByteCache.set(cacheKey, entry);
962
+ this.#manifestByteCacheBytes += entry.bytes.length;
963
+ while (this.#manifestByteCache.size > transactionManifestCacheMaxEntries ||
964
+ this.#manifestByteCacheBytes > transactionManifestCacheMaxBytes) {
965
+ const oldest = this.#manifestByteCache.keys().next().value;
966
+ if (oldest === undefined)
967
+ break;
968
+ this.removeManifestByteCache(oldest);
969
+ }
970
+ }
971
+ async writeManifest(workspace, manifest, hooks) {
972
+ const candidate = structuredClone(manifest);
973
+ candidate.revision += 1;
974
+ this.authenticateManifest(candidate);
975
+ const serializedCandidate = `${canonicalJson(candidate)}\n`;
976
+ if (Buffer.byteLength(serializedCandidate, 'utf8') > TRANSACTION_MAX_MANIFEST_BYTES) {
977
+ throw new ServiceError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
978
+ }
979
+ const transactionDirectory = await this.transactionDirectory(workspace, candidate.transactionId);
980
+ await hooks?.beforeManifestCacheWrite?.();
981
+ this.removeManifestByteCache(this.manifestCacheKey(workspace, candidate.transactionId));
982
+ await durableWrite(await containedGeneratedPath(transactionDirectory, 'manifest.json'), serializedCandidate);
983
+ await hooks?.afterManifestCacheWrite?.();
984
+ await hooks?.beforeProtectedHeadCommit?.();
985
+ await this.recordManifestSuccessor(workspace, candidate);
986
+ Object.assign(manifest, candidate);
987
+ await hooks?.afterProtectedHeadCommit?.();
988
+ }
989
+ async load(workspace, transactionId, options = {}) {
990
+ const { headMode = 'verify', signal, cacheBytes = false } = options;
991
+ signal?.throwIfAborted();
992
+ let manifestText;
993
+ let manifestPath;
994
+ let manifestIdentity;
995
+ try {
996
+ const transactionDirectory = await this.transactionDirectory(workspace, transactionId);
997
+ signal?.throwIfAborted();
998
+ manifestPath = await containedGeneratedPath(transactionDirectory, 'manifest.json');
999
+ const metadata = await stat(manifestPath);
1000
+ manifestIdentity = transactionManifestFileIdentity(metadata);
1001
+ signal?.throwIfAborted();
1002
+ if (!metadata.isFile() || metadata.size > TRANSACTION_MAX_MANIFEST_BYTES) {
1003
+ throw new ServiceError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
1004
+ }
1005
+ manifestText =
1006
+ signal === undefined
1007
+ ? await readFile(manifestPath, 'utf8')
1008
+ : await readFile(manifestPath, { encoding: 'utf8', signal });
1009
+ const afterReadIdentity = transactionManifestFileIdentity(await stat(manifestPath));
1010
+ if (!sameTransactionManifestIdentity(manifestIdentity, afterReadIdentity)) {
1011
+ throw new ServiceError('TRANSACTION_MANIFEST_CHANGED', 'Transaction manifest changed while it was being verified');
1012
+ }
1013
+ }
1014
+ catch (error) {
1015
+ if (error instanceof ServiceError || error.name === 'AbortError')
1016
+ throw error;
1017
+ throw new ServiceError('TRANSACTION_NOT_FOUND', 'Transaction was not found');
1018
+ }
1019
+ signal?.throwIfAborted();
1020
+ let parsed;
1021
+ try {
1022
+ parsed = JSON.parse(manifestText);
1023
+ }
1024
+ catch {
1025
+ throw new ServiceError('TRANSACTION_MANIFEST_INVALID', 'Transaction manifest is invalid');
1026
+ }
1027
+ signal?.throwIfAborted();
1028
+ const validated = transactionManifestSchema.safeParse(parsed);
1029
+ signal?.throwIfAborted();
1030
+ if (!validated.success) {
1031
+ throw new ServiceError('TRANSACTION_MANIFEST_INVALID', 'Transaction manifest is invalid', {
1032
+ issues: validated.error.issues,
1033
+ });
1034
+ }
1035
+ const manifest = validated.data;
1036
+ signal?.throwIfAborted();
1037
+ if (!this.requireServerState().verifyJournal(manifestAuthenticationPayload(manifest), manifest.authenticationTag)) {
1038
+ throw new ServiceError('TRANSACTION_MANIFEST_AUTHENTICATION_FAILED', `Transaction manifest authentication failed: ${transactionId}`);
1039
+ }
1040
+ signal?.throwIfAborted();
1041
+ if (manifest.transactionId !== transactionId || manifest.workspaceId !== workspace.id) {
1042
+ throw new ServiceError('TRANSACTION_WORKSPACE_MISMATCH', 'Transaction manifest identity does not match its workspace or journal directory');
1043
+ }
1044
+ if (manifest.rootFingerprint !== this.rootFingerprint(workspace)) {
1045
+ throw new ServiceError('TRANSACTION_ROOT_CHANGED', 'Workspace root configuration changed after planning');
1046
+ }
1047
+ if (hashCanonical(manifestIntegrityPayload(manifest)) !== manifest.integrityHash) {
1048
+ throw new ServiceError('TRANSACTION_MANIFEST_INTEGRITY_FAILED', `Transaction manifest integrity failed: ${transactionId}`);
1049
+ }
1050
+ signal?.throwIfAborted();
1051
+ if (hashCanonical(manifest.planPayload) !== manifest.planHash) {
1052
+ throw new ServiceError('TRANSACTION_PLAN_INTEGRITY_FAILED', `Transaction plan hash no longer matches its persisted payload: ${transactionId}`);
1053
+ }
1054
+ signal?.throwIfAborted();
1055
+ const currentPayload = manifestPlanPayload(manifest);
1056
+ if (hashCanonical(immutablePlanPayload(currentPayload)) !==
1057
+ hashCanonical(immutablePlanPayload(manifest.planPayload))) {
1058
+ throw new ServiceError('TRANSACTION_PLAN_INTEGRITY_FAILED', `Transaction manifest no longer matches its hash-bound plan: ${transactionId}`);
1059
+ }
1060
+ if (manifest.state === 'planned' && hashCanonical(currentPayload) !== manifest.planHash) {
1061
+ throw new ServiceError('TRANSACTION_PLAN_INTEGRITY_FAILED', `Planned transaction content no longer matches its plan hash: ${transactionId}`);
1062
+ }
1063
+ signal?.throwIfAborted();
1064
+ const manifestHash = hashCanonical(manifest);
1065
+ signal?.throwIfAborted();
1066
+ if (headMode !== 'none') {
1067
+ await this.verifyManifestHead(workspace, manifest, headMode, signal, manifestHash);
1068
+ }
1069
+ signal?.throwIfAborted();
1070
+ if (cacheBytes) {
1071
+ const canonicalBytes = Buffer.from(`${canonicalJson(manifest)}\n`, 'utf8');
1072
+ if (canonicalBytes.length > TRANSACTION_MAX_MANIFEST_BYTES) {
1073
+ throw new ServiceError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
1074
+ }
1075
+ this.storeManifestByteCache(this.manifestCacheKey(workspace, transactionId), {
1076
+ identity: manifestIdentity,
1077
+ bytes: canonicalBytes,
1078
+ principal: manifest.principal ?? null,
1079
+ revision: manifest.revision,
1080
+ authenticationTag: manifest.authenticationTag,
1081
+ manifestHash,
1082
+ });
1083
+ }
1084
+ return manifest;
1085
+ }
1086
+ async withWorkspaceLock(workspace, transactionId, action) {
1087
+ let locksDirectory = await containedGeneratedPath(workspace.cacheRoot, 'locks');
1088
+ await mkdir(locksDirectory, { recursive: true });
1089
+ locksDirectory = await containedGeneratedPath(workspace.cacheRoot, 'locks');
1090
+ const lock = await containedGeneratedPath(locksDirectory, 'write.lock');
1091
+ try {
1092
+ await mkdir(lock);
1093
+ }
1094
+ catch (error) {
1095
+ if (error.code === 'EEXIST') {
1096
+ const cleared = await this.clearCrashedProcessLock(lock);
1097
+ if (!cleared) {
1098
+ throw new ServiceError('TRANSACTION_LOCKED', 'Another write transaction owns the workspace lock');
1099
+ }
1100
+ try {
1101
+ await mkdir(lock);
1102
+ }
1103
+ catch (retryError) {
1104
+ if (retryError.code === 'EEXIST') {
1105
+ throw new ServiceError('TRANSACTION_LOCKED', 'Another write transaction acquired the workspace lock during recovery');
1106
+ }
1107
+ throw retryError;
1108
+ }
1109
+ }
1110
+ else
1111
+ throw error;
1112
+ }
1113
+ try {
1114
+ await durableWrite(await containedGeneratedPath(lock, 'owner.json'), `${canonicalJson({
1115
+ transactionId,
1116
+ pid: process.pid,
1117
+ host: lockHost,
1118
+ instanceId: lockInstanceId,
1119
+ processStartedAt: lockProcessStartedAt,
1120
+ })}\n`);
1121
+ return await action();
1122
+ }
1123
+ finally {
1124
+ await rm(lock, { recursive: true, force: true });
1125
+ }
1126
+ }
1127
+ async clearCrashedProcessLock(lock) {
1128
+ let owner;
1129
+ try {
1130
+ owner = JSON.parse(await readFile(await containedGeneratedPath(lock, 'owner.json'), 'utf8'));
1131
+ }
1132
+ catch (error) {
1133
+ if (error.code !== 'ENOENT') {
1134
+ // Corrupt or unrecognized owner evidence is never cleared automatically.
1135
+ return false;
1136
+ }
1137
+ // mkdir is the exclusive primitive, so there is necessarily a small window
1138
+ // before owner.json is durably renamed into place. Only an ownerless lock
1139
+ // older than a generous grace period is evidence of a crash in that window.
1140
+ try {
1141
+ const metadata = await stat(lock);
1142
+ if (Date.now() - metadata.mtimeMs < lockOwnerGraceMs)
1143
+ return false;
1144
+ }
1145
+ catch {
1146
+ return false;
1147
+ }
1148
+ await rm(lock, { recursive: true, force: true });
1149
+ return true;
1150
+ }
1151
+ if (typeof owner !== 'object' ||
1152
+ owner === null ||
1153
+ !('pid' in owner) ||
1154
+ typeof owner.pid !== 'number' ||
1155
+ !Number.isSafeInteger(owner.pid) ||
1156
+ owner.pid <= 0 ||
1157
+ !('host' in owner) ||
1158
+ typeof owner.host !== 'string' ||
1159
+ !('instanceId' in owner) ||
1160
+ typeof owner.instanceId !== 'string' ||
1161
+ !('processStartedAt' in owner) ||
1162
+ typeof owner.processStartedAt !== 'string') {
1163
+ return false;
1164
+ }
1165
+ // A PID has meaning only on the host that wrote it. Shared-filesystem locks
1166
+ // from another or unknown host require explicit operator coordination.
1167
+ if (owner.host.toLowerCase() !== lockHost)
1168
+ return false;
1169
+ try {
1170
+ process.kill(owner.pid, 0);
1171
+ return false;
1172
+ }
1173
+ catch (error) {
1174
+ if (error.code !== 'ESRCH')
1175
+ return false;
1176
+ }
1177
+ await rm(lock, { recursive: true, force: true });
1178
+ return true;
1179
+ }
1180
+ }
1181
+ //# sourceMappingURL=transactions.js.map