@quolu/lattice 0.1.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +87 -4
  2. package/bin/lattice-bridge.mjs +87 -0
  3. package/bin/lattice-dashboard.mjs +70 -0
  4. package/bin/lattice-mcp.mjs +9 -9
  5. package/bin/lattice.mjs +61 -8
  6. package/docs/bridge-setup.md +87 -0
  7. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -0
  8. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -0
  9. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -0
  10. package/package.json +19 -7
  11. package/sensor/dist/bin/command-supervision.d.ts +1 -1
  12. package/sensor/dist/bin/command-supervision.js +4 -4
  13. package/sensor/dist/bin/command-supervision.js.map +1 -1
  14. package/sensor/dist/bin/fatal-handler.js +2 -2
  15. package/sensor/dist/bin/fatal-handler.js.map +1 -1
  16. package/sensor/dist/bin/{codegraph.js → lattice-sensor.js} +207 -328
  17. package/sensor/dist/bin/node-version-check.d.ts +7 -5
  18. package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
  19. package/sensor/dist/bin/node-version-check.js +19 -14
  20. package/sensor/dist/bin/node-version-check.js.map +1 -1
  21. package/sensor/dist/context/index.d.ts +1 -1
  22. package/sensor/dist/context/index.js +6 -6
  23. package/sensor/dist/context/index.js.map +1 -1
  24. package/sensor/dist/db/index.d.ts +5 -5
  25. package/sensor/dist/db/index.d.ts.map +1 -1
  26. package/sensor/dist/db/index.js +7 -7
  27. package/sensor/dist/db/index.js.map +1 -1
  28. package/sensor/dist/db/migrations.js +1 -1
  29. package/sensor/dist/db/migrations.js.map +1 -1
  30. package/sensor/dist/db/queries.d.ts +1 -1
  31. package/sensor/dist/db/queries.js +5 -5
  32. package/sensor/dist/db/queries.js.map +1 -1
  33. package/sensor/dist/db/schema.sql +2 -2
  34. package/sensor/dist/db/sqlite-adapter.d.ts +2 -2
  35. package/sensor/dist/db/sqlite-adapter.js +3 -3
  36. package/sensor/dist/db/sqlite-adapter.js.map +1 -1
  37. package/sensor/dist/db/wal-valve.d.ts +1 -1
  38. package/sensor/dist/db/wal-valve.d.ts.map +1 -1
  39. package/sensor/dist/db/wal-valve.js +2 -2
  40. package/sensor/dist/db/wal-valve.js.map +1 -1
  41. package/sensor/dist/directory.d.ts +27 -63
  42. package/sensor/dist/directory.d.ts.map +1 -1
  43. package/sensor/dist/directory.js +78 -134
  44. package/sensor/dist/directory.js.map +1 -1
  45. package/sensor/dist/errors.d.ts +15 -15
  46. package/sensor/dist/errors.d.ts.map +1 -1
  47. package/sensor/dist/errors.js +21 -21
  48. package/sensor/dist/errors.js.map +1 -1
  49. package/sensor/dist/extraction/dynamic-import.d.ts +1 -1
  50. package/sensor/dist/extraction/dynamic-import.js +1 -1
  51. package/sensor/dist/extraction/extraction-version.d.ts +1 -1
  52. package/sensor/dist/extraction/extraction-version.js +1 -1
  53. package/sensor/dist/extraction/generated-detection.d.ts +1 -1
  54. package/sensor/dist/extraction/generated-detection.js +1 -1
  55. package/sensor/dist/extraction/grammars.d.ts +3 -3
  56. package/sensor/dist/extraction/grammars.js +5 -5
  57. package/sensor/dist/extraction/grammars.js.map +1 -1
  58. package/sensor/dist/extraction/index.d.ts +6 -6
  59. package/sensor/dist/extraction/index.js +50 -50
  60. package/sensor/dist/extraction/index.js.map +1 -1
  61. package/sensor/dist/extraction/kernel/decode.d.ts +1 -1
  62. package/sensor/dist/extraction/kernel/decode.js +1 -1
  63. package/sensor/dist/extraction/kernel/index.d.ts +3 -3
  64. package/sensor/dist/extraction/kernel/index.js +6 -6
  65. package/sensor/dist/extraction/kernel/index.js.map +1 -1
  66. package/sensor/dist/extraction/kernel/layout.d.ts +1 -1
  67. package/sensor/dist/extraction/kernel/layout.js +1 -1
  68. package/sensor/dist/extraction/kernel/loader.d.ts +6 -6
  69. package/sensor/dist/extraction/kernel/loader.js +13 -13
  70. package/sensor/dist/extraction/kernel/loader.js.map +1 -1
  71. package/sensor/dist/extraction/languages/cfquery.d.ts +1 -1
  72. package/sensor/dist/extraction/languages/cfquery.js +1 -1
  73. package/sensor/dist/extraction/languages/lua.d.ts +11 -0
  74. package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
  75. package/sensor/dist/extraction/languages/lua.js +1 -0
  76. package/sensor/dist/extraction/languages/lua.js.map +1 -1
  77. package/sensor/dist/extraction/parse-pool.d.ts +5 -5
  78. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  79. package/sensor/dist/extraction/parse-pool.js +4 -4
  80. package/sensor/dist/extraction/parse-worker.js +1 -1
  81. package/sensor/dist/extraction/parse-worker.js.map +1 -1
  82. package/sensor/dist/extraction/store-writer.d.ts +1 -1
  83. package/sensor/dist/extraction/store-writer.js +1 -1
  84. package/sensor/dist/extraction/svelte-extractor.d.ts +1 -1
  85. package/sensor/dist/extraction/svelte-extractor.js +1 -1
  86. package/sensor/dist/extraction/tree-sitter-helpers.js +1 -1
  87. package/sensor/dist/extraction/tree-sitter.d.ts +2 -2
  88. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  89. package/sensor/dist/extraction/tree-sitter.js +71 -4
  90. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  91. package/sensor/dist/extraction/wasm-runtime-flags.d.ts +2 -2
  92. package/sensor/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  93. package/sensor/dist/extraction/wasm-runtime-flags.js +5 -5
  94. package/sensor/dist/extraction/wasm-runtime-flags.js.map +1 -1
  95. package/sensor/dist/index.d.ts +36 -36
  96. package/sensor/dist/index.d.ts.map +1 -1
  97. package/sensor/dist/index.js +71 -71
  98. package/sensor/dist/index.js.map +1 -1
  99. package/sensor/dist/mcp/daemon-manager.js +1 -1
  100. package/sensor/dist/mcp/daemon-paths.d.ts +3 -3
  101. package/sensor/dist/mcp/daemon-paths.js +7 -7
  102. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  103. package/sensor/dist/mcp/daemon-registry.d.ts +2 -2
  104. package/sensor/dist/mcp/daemon-registry.js +5 -5
  105. package/sensor/dist/mcp/daemon.d.ts +7 -7
  106. package/sensor/dist/mcp/daemon.d.ts.map +1 -1
  107. package/sensor/dist/mcp/daemon.js +21 -21
  108. package/sensor/dist/mcp/daemon.js.map +1 -1
  109. package/sensor/dist/mcp/dynamic-boundaries.js +1 -1
  110. package/sensor/dist/mcp/engine.d.ts +9 -9
  111. package/sensor/dist/mcp/engine.d.ts.map +1 -1
  112. package/sensor/dist/mcp/engine.js +44 -44
  113. package/sensor/dist/mcp/engine.js.map +1 -1
  114. package/sensor/dist/mcp/index.d.ts +14 -14
  115. package/sensor/dist/mcp/index.d.ts.map +1 -1
  116. package/sensor/dist/mcp/index.js +41 -41
  117. package/sensor/dist/mcp/index.js.map +1 -1
  118. package/sensor/dist/mcp/liveness-watchdog.js +7 -7
  119. package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
  120. package/sensor/dist/mcp/ppid-watchdog.d.ts +3 -3
  121. package/sensor/dist/mcp/ppid-watchdog.js +2 -2
  122. package/sensor/dist/mcp/proxy.d.ts.map +1 -1
  123. package/sensor/dist/mcp/proxy.js +37 -37
  124. package/sensor/dist/mcp/proxy.js.map +1 -1
  125. package/sensor/dist/mcp/query-pool.d.ts +3 -3
  126. package/sensor/dist/mcp/query-pool.js +7 -7
  127. package/sensor/dist/mcp/query-pool.js.map +1 -1
  128. package/sensor/dist/mcp/query-worker.d.ts +2 -2
  129. package/sensor/dist/mcp/query-worker.js +7 -7
  130. package/sensor/dist/mcp/query-worker.js.map +1 -1
  131. package/sensor/dist/mcp/server-instructions.d.ts +10 -12
  132. package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
  133. package/sensor/dist/mcp/server-instructions.js +39 -37
  134. package/sensor/dist/mcp/server-instructions.js.map +1 -1
  135. package/sensor/dist/mcp/session.d.ts +2 -2
  136. package/sensor/dist/mcp/session.js +18 -18
  137. package/sensor/dist/mcp/session.js.map +1 -1
  138. package/sensor/dist/mcp/startup-handshake.d.ts +3 -3
  139. package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
  140. package/sensor/dist/mcp/startup-handshake.js +3 -3
  141. package/sensor/dist/mcp/startup-handshake.js.map +1 -1
  142. package/sensor/dist/mcp/tools.d.ts +53 -53
  143. package/sensor/dist/mcp/tools.d.ts.map +1 -1
  144. package/sensor/dist/mcp/tools.js +180 -216
  145. package/sensor/dist/mcp/tools.js.map +1 -1
  146. package/sensor/dist/mcp/transport.d.ts +1 -1
  147. package/sensor/dist/mcp/transport.js +6 -6
  148. package/sensor/dist/mcp/transport.js.map +1 -1
  149. package/sensor/dist/mcp/version.d.ts +2 -2
  150. package/sensor/dist/mcp/version.d.ts.map +1 -1
  151. package/sensor/dist/mcp/version.js +3 -3
  152. package/sensor/dist/mcp/version.js.map +1 -1
  153. package/sensor/dist/project-config.d.ts +6 -6
  154. package/sensor/dist/project-config.d.ts.map +1 -1
  155. package/sensor/dist/project-config.js +10 -10
  156. package/sensor/dist/project-config.js.map +1 -1
  157. package/sensor/dist/resolution/callback-synthesizer.js +4 -4
  158. package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
  159. package/sensor/dist/resolution/frameworks/drupal.d.ts +3 -3
  160. package/sensor/dist/resolution/frameworks/drupal.js +3 -3
  161. package/sensor/dist/resolution/frameworks/fabric.js +1 -1
  162. package/sensor/dist/resolution/frameworks/fabric.js.map +1 -1
  163. package/sensor/dist/resolution/frameworks/java.js +1 -1
  164. package/sensor/dist/resolution/frameworks/java.js.map +1 -1
  165. package/sensor/dist/resolution/frameworks/svelte.js +1 -1
  166. package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
  167. package/sensor/dist/resolution/goframe-synthesizer.d.ts +1 -1
  168. package/sensor/dist/resolution/goframe-synthesizer.js +1 -1
  169. package/sensor/dist/resolution/index.d.ts +1 -1
  170. package/sensor/dist/resolution/index.js +9 -9
  171. package/sensor/dist/resolution/index.js.map +1 -1
  172. package/sensor/dist/resolution/name-matcher.js +2 -2
  173. package/sensor/dist/resolution/name-matcher.js.map +1 -1
  174. package/sensor/dist/resolution/resolver-pool.d.ts +2 -2
  175. package/sensor/dist/resolution/resolver-pool.js +4 -4
  176. package/sensor/dist/resolution/resolver-pool.js.map +1 -1
  177. package/sensor/dist/resolution/resolver-worker.js +2 -2
  178. package/sensor/dist/resolution/resolver-worker.js.map +1 -1
  179. package/sensor/dist/resolution/workspace-packages.js +1 -1
  180. package/sensor/dist/resolution/workspace-packages.js.map +1 -1
  181. package/sensor/dist/search/identifier-segments.d.ts +1 -1
  182. package/sensor/dist/search/identifier-segments.js +1 -1
  183. package/sensor/dist/search/query-utils.d.ts +1 -0
  184. package/sensor/dist/search/query-utils.d.ts.map +1 -1
  185. package/sensor/dist/search/query-utils.js +10 -3
  186. package/sensor/dist/search/query-utils.js.map +1 -1
  187. package/sensor/dist/sync/git-hooks.d.ts +9 -9
  188. package/sensor/dist/sync/git-hooks.d.ts.map +1 -1
  189. package/sensor/dist/sync/git-hooks.js +15 -15
  190. package/sensor/dist/sync/git-hooks.js.map +1 -1
  191. package/sensor/dist/sync/watch-policy.d.ts +2 -2
  192. package/sensor/dist/sync/watch-policy.js +5 -5
  193. package/sensor/dist/sync/watch-policy.js.map +1 -1
  194. package/sensor/dist/sync/watcher.d.ts +2 -2
  195. package/sensor/dist/sync/watcher.d.ts.map +1 -1
  196. package/sensor/dist/sync/watcher.js +16 -16
  197. package/sensor/dist/sync/watcher.js.map +1 -1
  198. package/sensor/dist/sync/worktree.d.ts +5 -5
  199. package/sensor/dist/sync/worktree.d.ts.map +1 -1
  200. package/sensor/dist/sync/worktree.js +9 -9
  201. package/sensor/dist/sync/worktree.js.map +1 -1
  202. package/sensor/dist/telemetry/index.d.ts +8 -8
  203. package/sensor/dist/telemetry/index.d.ts.map +1 -1
  204. package/sensor/dist/telemetry/index.js +16 -16
  205. package/sensor/dist/telemetry/index.js.map +1 -1
  206. package/sensor/dist/types.d.ts +2 -2
  207. package/sensor/dist/types.js +1 -1
  208. package/sensor/dist/ui/color.d.ts +2 -2
  209. package/sensor/dist/ui/color.js +2 -2
  210. package/sensor/dist/ui/glyphs.d.ts +3 -3
  211. package/sensor/dist/ui/glyphs.js +7 -7
  212. package/sensor/dist/ui/glyphs.js.map +1 -1
  213. package/sensor/dist/utils.d.ts +4 -4
  214. package/sensor/dist/utils.js +8 -8
  215. package/sensor/dist/utils.js.map +1 -1
  216. package/sensor/package.json +8 -13
  217. package/src/artifact-contracts.mjs +2 -2
  218. package/src/boundary-compiler.mjs +28 -28
  219. package/src/boundary-observation-compiler-v2.mjs +10 -10
  220. package/src/bounded-seam.mjs +144 -0
  221. package/src/bridge-cli.mjs +227 -0
  222. package/src/bridge-config.mjs +346 -0
  223. package/src/bridge-daemon.mjs +328 -0
  224. package/src/bridge-launch-agent.mjs +311 -0
  225. package/src/bridge-server.mjs +309 -0
  226. package/src/cli-help.mjs +163 -0
  227. package/src/control-compiler.mjs +30 -30
  228. package/src/dag-chain.mjs +261 -0
  229. package/src/factory-diagnostics.mjs +8 -7
  230. package/src/hash-chain.mjs +76 -0
  231. package/src/node-version-guard.mjs +5 -4
  232. package/src/project-cli.mjs +392 -0
  233. package/src/project-identity.mjs +71 -0
  234. package/src/rc1-evidence-bundle.mjs +16 -16
  235. package/src/rc1-v4-campaign.mjs +15 -14
  236. package/src/rc1-v5-artifact-set.mjs +2 -2
  237. package/src/rc1-v5-campaign.mjs +15 -14
  238. package/src/rc1-v6-artifact-set.mjs +18 -18
  239. package/src/rc1-v6-campaign.mjs +16 -16
  240. package/src/rc1-v6-causal-binding.mjs +16 -14
  241. package/src/rc1-v6-measurement.mjs +20 -38
  242. package/src/rc2-artifact-set.mjs +45 -40
  243. package/src/rc2-campaign.mjs +48 -66
  244. package/src/rc2-delivery-policy-front-end.mjs +30 -30
  245. package/src/rc2-rc1-transfer-front-end.mjs +5 -5
  246. package/src/rc3-actual-dogfood.mjs +6 -6
  247. package/src/rc3-dogfood-scaffold.mjs +12 -7
  248. package/src/rc3-scripted-campaign.mjs +75 -21
  249. package/src/rc4-stage1-dogfood.mjs +24 -7
  250. package/src/runtime-cli.mjs +2424 -45
  251. package/src/runtime-contracts.mjs +7 -7
  252. package/src/runtime-control-store.mjs +547 -0
  253. package/src/runtime-controller-protocol.mjs +517 -0
  254. package/src/runtime-direct-os-observer.mjs +284 -0
  255. package/src/runtime-engine.mjs +2 -2
  256. package/src/runtime-errors.mjs +1 -1
  257. package/src/runtime-event-store.mjs +15 -43
  258. package/src/runtime-front-end.mjs +26 -26
  259. package/src/runtime-gate-store.mjs +477 -0
  260. package/src/runtime-hold-recompile.mjs +214 -23
  261. package/src/runtime-lifecycle-lock.mjs +290 -0
  262. package/src/runtime-managed-supervisor.mjs +1276 -0
  263. package/src/runtime-multi-epoch-store.mjs +761 -0
  264. package/src/runtime-projection.mjs +62 -0
  265. package/src/seam-transform.mjs +38 -38
  266. package/src/{codegraph-adapter.mjs → sensor-adapter.mjs} +7 -7
  267. package/src/sensor-cli.mjs +107 -0
  268. package/src/sensor-runtime.mjs +48 -0
  269. package/src/timestamp-contract.mjs +8 -0
  270. package/src/todo-chain.mjs +178 -0
  271. package/src/todo-cli.mjs +1032 -0
  272. package/src/todo-contracts.mjs +501 -0
  273. package/src/todo-dashboard-registry.mjs +318 -0
  274. package/src/todo-gantt-html.mjs +416 -0
  275. package/src/todo-gantt-layout.mjs +603 -0
  276. package/src/todo-gantt-live.mjs +269 -0
  277. package/src/todo-gantt-presentation.mjs +217 -0
  278. package/src/todo-gantt-svg.mjs +249 -0
  279. package/src/todo-markdown-renderer.mjs +260 -0
  280. package/src/todo-migration.mjs +260 -0
  281. package/src/todo-narrative-anchor.mjs +130 -0
  282. package/src/todo-revision.mjs +425 -0
  283. package/src/todo-status.mjs +340 -0
  284. package/src/todo-store.mjs +3451 -0
  285. package/src/treatment-compiler.mjs +28 -28
  286. package/src/treatment-runner.mjs +42 -44
  287. package/sensor/dist/bin/codegraph.d.ts +0 -26
  288. package/sensor/dist/bin/codegraph.d.ts.map +0 -1
  289. package/sensor/dist/bin/codegraph.js.map +0 -1
  290. package/sensor/dist/bin/uninstall.d.ts +0 -14
  291. package/sensor/dist/bin/uninstall.d.ts.map +0 -1
  292. package/sensor/dist/bin/uninstall.js +0 -36
  293. package/sensor/dist/bin/uninstall.js.map +0 -1
  294. package/sensor/dist/installer/beta-signup.d.ts +0 -54
  295. package/sensor/dist/installer/beta-signup.d.ts.map +0 -1
  296. package/sensor/dist/installer/beta-signup.js +0 -178
  297. package/sensor/dist/installer/beta-signup.js.map +0 -1
  298. package/sensor/dist/installer/config-writer.d.ts +0 -28
  299. package/sensor/dist/installer/config-writer.d.ts.map +0 -1
  300. package/sensor/dist/installer/config-writer.js +0 -91
  301. package/sensor/dist/installer/config-writer.js.map +0 -1
  302. package/sensor/dist/installer/index.d.ts +0 -142
  303. package/sensor/dist/installer/index.d.ts.map +0 -1
  304. package/sensor/dist/installer/index.js +0 -622
  305. package/sensor/dist/installer/index.js.map +0 -1
  306. package/sensor/dist/installer/instructions-template.d.ts +0 -41
  307. package/sensor/dist/installer/instructions-template.d.ts.map +0 -1
  308. package/sensor/dist/installer/instructions-template.js +0 -53
  309. package/sensor/dist/installer/instructions-template.js.map +0 -1
  310. package/sensor/dist/installer/targets/antigravity.d.ts +0 -57
  311. package/sensor/dist/installer/targets/antigravity.d.ts.map +0 -1
  312. package/sensor/dist/installer/targets/antigravity.js +0 -308
  313. package/sensor/dist/installer/targets/antigravity.js.map +0 -1
  314. package/sensor/dist/installer/targets/claude.d.ts +0 -62
  315. package/sensor/dist/installer/targets/claude.d.ts.map +0 -1
  316. package/sensor/dist/installer/targets/claude.js +0 -454
  317. package/sensor/dist/installer/targets/claude.js.map +0 -1
  318. package/sensor/dist/installer/targets/codex.d.ts +0 -18
  319. package/sensor/dist/installer/targets/codex.d.ts.map +0 -1
  320. package/sensor/dist/installer/targets/codex.js +0 -185
  321. package/sensor/dist/installer/targets/codex.js.map +0 -1
  322. package/sensor/dist/installer/targets/cursor.d.ts +0 -35
  323. package/sensor/dist/installer/targets/cursor.d.ts.map +0 -1
  324. package/sensor/dist/installer/targets/cursor.js +0 -254
  325. package/sensor/dist/installer/targets/cursor.js.map +0 -1
  326. package/sensor/dist/installer/targets/gemini.d.ts +0 -26
  327. package/sensor/dist/installer/targets/gemini.d.ts.map +0 -1
  328. package/sensor/dist/installer/targets/gemini.js +0 -165
  329. package/sensor/dist/installer/targets/gemini.js.map +0 -1
  330. package/sensor/dist/installer/targets/hermes.d.ts +0 -18
  331. package/sensor/dist/installer/targets/hermes.d.ts.map +0 -1
  332. package/sensor/dist/installer/targets/hermes.js +0 -359
  333. package/sensor/dist/installer/targets/hermes.js.map +0 -1
  334. package/sensor/dist/installer/targets/kiro.d.ts +0 -27
  335. package/sensor/dist/installer/targets/kiro.d.ts.map +0 -1
  336. package/sensor/dist/installer/targets/kiro.js +0 -178
  337. package/sensor/dist/installer/targets/kiro.js.map +0 -1
  338. package/sensor/dist/installer/targets/opencode.d.ts +0 -38
  339. package/sensor/dist/installer/targets/opencode.d.ts.map +0 -1
  340. package/sensor/dist/installer/targets/opencode.js +0 -288
  341. package/sensor/dist/installer/targets/opencode.js.map +0 -1
  342. package/sensor/dist/installer/targets/registry.d.ts +0 -35
  343. package/sensor/dist/installer/targets/registry.d.ts.map +0 -1
  344. package/sensor/dist/installer/targets/registry.js +0 -91
  345. package/sensor/dist/installer/targets/registry.js.map +0 -1
  346. package/sensor/dist/installer/targets/shared.d.ts +0 -101
  347. package/sensor/dist/installer/targets/shared.d.ts.map +0 -1
  348. package/sensor/dist/installer/targets/shared.js +0 -264
  349. package/sensor/dist/installer/targets/shared.js.map +0 -1
  350. package/sensor/dist/installer/targets/toml.d.ts +0 -52
  351. package/sensor/dist/installer/targets/toml.d.ts.map +0 -1
  352. package/sensor/dist/installer/targets/toml.js +0 -147
  353. package/sensor/dist/installer/targets/toml.js.map +0 -1
  354. package/sensor/dist/installer/targets/types.d.ts +0 -108
  355. package/sensor/dist/installer/targets/types.d.ts.map +0 -1
  356. package/sensor/dist/installer/targets/types.js +0 -16
  357. package/sensor/dist/installer/targets/types.js.map +0 -1
  358. package/sensor/dist/upgrade/index.d.ts +0 -171
  359. package/sensor/dist/upgrade/index.d.ts.map +0 -1
  360. package/sensor/dist/upgrade/index.js +0 -659
  361. package/sensor/dist/upgrade/index.js.map +0 -1
  362. package/sensor/dist/upgrade/remove-binary.d.ts +0 -87
  363. package/sensor/dist/upgrade/remove-binary.d.ts.map +0 -1
  364. package/sensor/dist/upgrade/remove-binary.js +0 -289
  365. package/sensor/dist/upgrade/remove-binary.js.map +0 -1
  366. package/sensor/dist/upgrade/update-check.d.ts +0 -92
  367. package/sensor/dist/upgrade/update-check.d.ts.map +0 -1
  368. package/sensor/dist/upgrade/update-check.js +0 -258
  369. package/sensor/dist/upgrade/update-check.js.map +0 -1
  370. package/src/bootstrap.mjs +0 -88
@@ -0,0 +1,3451 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import {
4
+ lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir,
5
+ } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import {
8
+ TODO_LIMITS,
9
+ canonicalizeTodoArtifact,
10
+ digestTodoArtifact,
11
+ exactRecord,
12
+ isStrictTodoTimestamp,
13
+ isTodoDigest,
14
+ isTodoIdentifier,
15
+ todoSelfDigest,
16
+ validateEvidenceDescriptor,
17
+ validateTodoImportSource,
18
+ validateTodoEvent,
19
+ validateTodoManifest,
20
+ validateTodoPlan,
21
+ validateTodoSnapshot,
22
+ } from './todo-contracts.mjs';
23
+ import { sha256Bytes, verifyLinearHashChain } from './hash-chain.mjs';
24
+ import {
25
+ parseTodoSourceRef,
26
+ todoCutoverArchiveSourceRef,
27
+ todoLegacyReconciliationDigest,
28
+ validatePhaseTodoRevision,
29
+ validateTodoRevision,
30
+ validateTodoRevisionSet,
31
+ } from './todo-revision.mjs';
32
+
33
+ const STORE_ROOT_REF = '.lattice/todo';
34
+ const MANIFEST_REF = `${STORE_ROOT_REF}/manifest.json`;
35
+ const SOURCE_CUTOVER_BARRIER_REF = `${STORE_ROOT_REF}/source-cutover-recovery.json`;
36
+ const SOURCE_CUTOVER_RECOVERY_CAPABILITY = Symbol('lattice.todo.source-cutover-recovery');
37
+ const MAX_FUTURE_SKEW_MS = 5 * 60 * 1_000;
38
+ const WRITER_CALLERS = new Set(['g4-migration', 'g5-authoring']);
39
+ const TODO_REVISION_SCHEMAS = Object.freeze([
40
+ 'lattice.todo_revision.v1', 'lattice.todo_revision.v2',
41
+ ]);
42
+ const PHASE_TODO_REVISION_SCHEMAS = Object.freeze([
43
+ 'lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
44
+ 'lattice.phase_todo_revision.v3',
45
+ ]);
46
+
47
+ export class TodoStoreError extends Error {
48
+ constructor(code, reason, message = reason, detail = {}) {
49
+ super(message);
50
+ this.name = 'TodoStoreError';
51
+ this.code = code;
52
+ this.detail = { reason, ...detail };
53
+ }
54
+ }
55
+
56
+ export function createTodoStoreWriter(options = {}) {
57
+ if (!exactRecord(options, ['caller']) || !WRITER_CALLERS.has(options.caller)) {
58
+ throw new TypeError('todo store writer caller must be g4-migration or g5-authoring');
59
+ }
60
+ return Object.freeze({ schema: 'lattice.todo_store_writer.v1', caller: options.caller });
61
+ }
62
+
63
+ function fail(code, reason, detail) {
64
+ throw new TodoStoreError(code, reason, reason, detail);
65
+ }
66
+
67
+ function rejectUnsupportedRevisionSchema(value, { family, supportedSchemas }) {
68
+ const schema = value?.schema;
69
+ if (typeof schema === 'string'
70
+ && new RegExp(`^lattice\\.${family}\\.v[1-9]\\d*$`, 'u').test(schema)
71
+ && !supportedSchemas.includes(schema)) {
72
+ fail('UNSUPPORTED_SCHEMA', 'unsupported_revision_schema', {
73
+ schema, supported_schemas: [...supportedSchemas],
74
+ });
75
+ }
76
+ }
77
+
78
+ function requireWriter(writer, caller) {
79
+ if (!exactRecord(writer, ['schema', 'caller']) || writer.schema !== 'lattice.todo_store_writer.v1'
80
+ || writer.caller !== caller) throw new TypeError(`writer capability for ${caller} required`);
81
+ }
82
+
83
+ function canonicalLine(value) {
84
+ return Buffer.from(`${canonicalizeTodoArtifact(value)}\n`, 'utf8');
85
+ }
86
+
87
+ async function pathState(repoRoot, ref, classification, { missing = false } = {}) {
88
+ let canonicalRepoRoot;
89
+ try { canonicalRepoRoot = await realpath(repoRoot); } catch { canonicalRepoRoot = path.resolve(repoRoot); }
90
+ const absolute = path.resolve(canonicalRepoRoot, ref);
91
+ const root = path.resolve(canonicalRepoRoot, STORE_ROOT_REF);
92
+ if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
93
+ fail(classification, 'path_outside_store', { ref });
94
+ }
95
+ let stats;
96
+ try { stats = await lstat(absolute); } catch (error) {
97
+ if (missing && error?.code === 'ENOENT') return null;
98
+ fail(classification, 'artifact_missing', { ref });
99
+ }
100
+ if (stats.isSymbolicLink() || !stats.isFile()) fail(classification, 'unsafe_artifact_path', { ref });
101
+ const resolved = await realpath(absolute);
102
+ if (resolved !== absolute) fail(classification, 'path_alias_or_escape', { ref });
103
+ return { absolute, stats };
104
+ }
105
+
106
+ function decodeUtf8(bytes, code, reason) {
107
+ try { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
108
+ catch { fail(code, reason); }
109
+ }
110
+
111
+ function parseCanonicalJsonLine(bytes, { code, reason, maxBytes, validate }) {
112
+ if (bytes.length === 0 || bytes.length > maxBytes) fail(code, bytes.length > maxBytes ? 'size_limit_exceeded' : reason);
113
+ const text = decodeUtf8(bytes, code, 'invalid_utf8');
114
+ if (!text.endsWith('\n') || text.includes('\r') || text.startsWith('\uFEFF')
115
+ || text.slice(0, -1).includes('\n')) fail(code, reason);
116
+ let value;
117
+ try { value = JSON.parse(text.slice(0, -1)); } catch { fail(code, reason); }
118
+ let expected;
119
+ try { expected = `${canonicalizeTodoArtifact(value)}\n`; } catch { fail(code, reason); }
120
+ if (text !== expected) fail(code, 'non_canonical_or_duplicate_key');
121
+ if (!validate(value)) fail(code, 'schema_invalid');
122
+ return value;
123
+ }
124
+
125
+ async function readArtifact(repoRoot, ref, { code, maxBytes, validate, missing = false }) {
126
+ const state = await pathState(repoRoot, ref, code, { missing });
127
+ if (state === null) return null;
128
+ const bytes = await readFile(state.absolute);
129
+ return parseCanonicalJsonLine(bytes, { code, reason: 'artifact_truncated_or_trailing_bytes', maxBytes, validate });
130
+ }
131
+
132
+ async function readSnapshotArtifact(repoRoot, ref) {
133
+ const state = await pathState(repoRoot, ref, 'STORE_INCONSISTENT', { missing: true });
134
+ if (state === null) return null;
135
+ const bytes = await readFile(state.absolute);
136
+ return parseCanonicalJsonLine(bytes, {
137
+ code: 'SNAPSHOT_INVALID', reason: 'snapshot_truncated_or_trailing_bytes',
138
+ maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoSnapshot,
139
+ });
140
+ }
141
+
142
+ function parseJournalSegment(bytes) {
143
+ if (bytes.length === 0 || bytes.length > TODO_LIMITS.journalSegmentBytes) {
144
+ fail('STORE_CORRUPT', bytes.length > TODO_LIMITS.journalSegmentBytes ? 'journal_segment_limit_exceeded' : 'journal_empty');
145
+ }
146
+ const text = decodeUtf8(bytes, 'STORE_CORRUPT', 'journal_invalid_utf8');
147
+ if (!text.endsWith('\n') || text.includes('\r') || text.startsWith('\uFEFF')) fail('STORE_CORRUPT', 'journal_byte_contract');
148
+ const lines = text.slice(0, -1).split('\n');
149
+ if (lines.some((line) => line.length === 0)) fail('STORE_CORRUPT', 'journal_truncated_or_empty_line');
150
+ return lines.map((line) => {
151
+ let event;
152
+ try { event = JSON.parse(line); } catch { fail('STORE_CORRUPT', 'journal_json_invalid'); }
153
+ let canonical;
154
+ try { canonical = canonicalizeTodoArtifact(event); } catch { fail('STORE_CORRUPT', 'journal_schema_invalid'); }
155
+ if (line !== canonical) fail('STORE_CORRUPT', 'journal_non_canonical_or_duplicate_key');
156
+ if (!validateTodoEvent(event)) fail('STORE_CORRUPT', 'journal_schema_invalid');
157
+ return event;
158
+ });
159
+ }
160
+
161
+ async function readJournal(repoRoot, journalRef) {
162
+ const state = await pathState(repoRoot, journalRef, 'STORE_CORRUPT');
163
+ const directory = path.dirname(state.absolute);
164
+ const sealedDirectory = path.join(directory, 'sealed');
165
+ const segments = [];
166
+ try {
167
+ const sealedStats = await lstat(sealedDirectory);
168
+ if (sealedStats.isSymbolicLink() || !sealedStats.isDirectory()) fail('STORE_CORRUPT', 'unsafe_sealed_directory');
169
+ const names = (await readdir(sealedDirectory)).sort();
170
+ for (const name of names) {
171
+ if (!/^\d{12}-\d{12}-[0-9a-f]{64}-[0-9a-f]{64}\.jsonl$/u.test(name)) {
172
+ fail('STORE_CORRUPT', 'sealed_segment_name_invalid', { name });
173
+ }
174
+ const ref = path.posix.join(path.posix.dirname(journalRef), 'sealed', name);
175
+ const sealed = await pathState(repoRoot, ref, 'STORE_CORRUPT');
176
+ const bytes = await readFile(sealed.absolute);
177
+ const [, startText, endText, previousDigest, segmentDigest] = name.match(
178
+ /^(\d{12})-(\d{12})-([0-9a-f]{64})-([0-9a-f]{64})\.jsonl$/u,
179
+ );
180
+ if (sha256Bytes(bytes) !== segmentDigest) fail('STORE_CORRUPT', 'sealed_segment_digest_mismatch');
181
+ const events = parseJournalSegment(bytes);
182
+ if (events[0].sequence !== Number(startText) || events.at(-1).sequence !== Number(endText)) {
183
+ fail('STORE_CORRUPT', 'sealed_segment_range_mismatch');
184
+ }
185
+ if (previousDigest !== (segments.at(-1)?.events.at(-1).event_digest ?? '0'.repeat(64))) {
186
+ fail('STORE_CORRUPT', 'sealed_segment_link_mismatch');
187
+ }
188
+ segments.push({ ref, bytes, events });
189
+ }
190
+ } catch (error) {
191
+ if (error instanceof TodoStoreError) throw error;
192
+ if (error?.code !== 'ENOENT') fail('STORE_CORRUPT', 'sealed_segment_read_failed');
193
+ }
194
+ const activeBytes = await readFile(state.absolute);
195
+ segments.push({ ref: journalRef, bytes: activeBytes, events: parseJournalSegment(activeBytes) });
196
+ const events = segments.flatMap(({ events: entries }) => entries);
197
+ const failures = verifyLinearHashChain({
198
+ entries: events,
199
+ canonicalize: canonicalizeTodoArtifact,
200
+ digestField: 'event_digest',
201
+ genesisPrevious: events[0]?.previous_digest ?? null,
202
+ });
203
+ if (failures.size > 0) fail('STORE_CORRUPT', [...failures].sort()[0], { failed_conditions: [...failures].sort() });
204
+ if (events[0]?.kind !== 'plan_genesis' || events.slice(1).some(({ kind }) => kind === 'plan_genesis')) {
205
+ fail('STORE_CORRUPT', 'genesis_missing_or_repeated');
206
+ }
207
+ const successorSchema = events[0].schema === 'lattice.todo_event.v2';
208
+ const phaseSchema = ['lattice.todo_event.v3', 'lattice.todo_event.v4'].includes(events[0].schema);
209
+ const phaseRevisionSchema = events[0].schema === 'lattice.todo_event.v4';
210
+ const phaseTail = (event) => event.schema === 'lattice.todo_event.v3';
211
+ const legacyTail = (event) => event.schema === 'lattice.todo_event.v1';
212
+ if ((phaseSchema && events.some(({ schema }, index) => index === 0
213
+ ? !['lattice.todo_event.v3', 'lattice.todo_event.v4'].includes(schema) : !phaseTail(events[index])))
214
+ || (!phaseSchema && events.slice(1).some((event) => !legacyTail(event)))
215
+ || (!phaseSchema && !successorSchema && events.some((event, index) => index === 0
216
+ ? event.schema !== 'lattice.todo_event.v1' : !legacyTail(event)))) {
217
+ fail('STORE_CORRUPT', 'journal_schema_sequence_invalid');
218
+ }
219
+ return { segments, events, activeBytes };
220
+ }
221
+
222
+ function taskState(taskId) {
223
+ return { task_id: taskId, status: 'pending', started_at: null, done_at: null, blocked_reason: null,
224
+ evidence: null, evidence_unverified: false, imported: false };
225
+ }
226
+
227
+ function emptyPhaseState(phaseId) {
228
+ return { phase_id: phaseId, status: 'locked', review_event_digest: null,
229
+ decision_event_digest: null, decision_evidence: null };
230
+ }
231
+
232
+ function derivedPhaseStatus(plan, taskStates, phaseStates, phaseId) {
233
+ const state = phaseStates.get(phaseId);
234
+ if (['reviewing', 'accepted', 'rejected'].includes(state.status)) return state.status;
235
+ const phase = plan.phases.find((entry) => entry.phase_id === phaseId);
236
+ if (!phase.predecessor_phase_ids.every((id) => phaseStates.get(id)?.status === 'accepted')) return 'locked';
237
+ const tasks = plan.tasks.filter((entry) => entry.phase_id === phaseId);
238
+ return tasks.every((entry) => taskStates.get(entry.task_id)?.status === 'done') ? 'gate_ready' : 'active';
239
+ }
240
+
241
+ function projectPhaseStates(plan, events, taskStates) {
242
+ if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema)) return [];
243
+ const states = new Map(plan.phases.map(({ phase_id }) => [phase_id, emptyPhaseState(phase_id)]));
244
+ for (const event of events) {
245
+ if (event.schema === 'lattice.todo_event.v4') {
246
+ for (const migration of event.phase_state_migration) {
247
+ if (migration.state_policy === 'carry') {
248
+ Object.assign(states.get(migration.phase_id), structuredClone(migration.state));
249
+ }
250
+ }
251
+ } else if (event.kind === 'phase_review') {
252
+ const state = states.get(event.phase_id);
253
+ state.status = 'reviewing'; state.review_event_digest = event.event_digest;
254
+ state.decision_event_digest = null; state.decision_evidence = null;
255
+ } else if (event.kind === 'phase_accept') {
256
+ const state = states.get(event.phase_id);
257
+ state.status = 'accepted'; state.decision_event_digest = event.event_digest;
258
+ state.decision_evidence = event.payload.decision_evidence;
259
+ } else if (event.kind === 'phase_reject') {
260
+ const state = states.get(event.phase_id);
261
+ state.status = 'rejected'; state.decision_event_digest = event.event_digest;
262
+ state.decision_evidence = event.payload.decision_evidence;
263
+ } else if (event.kind === 'phase_reopen') {
264
+ Object.assign(states.get(event.phase_id), emptyPhaseState(event.phase_id));
265
+ }
266
+ }
267
+ for (const phase of plan.phases) {
268
+ const state = states.get(phase.phase_id);
269
+ state.status = derivedPhaseStatus(plan, taskStates, states, phase.phase_id);
270
+ }
271
+ return [...states.values()].sort((left, right) => left.phase_id.localeCompare(right.phase_id));
272
+ }
273
+
274
+ function localPredecessors(plan, taskId) {
275
+ const result = [];
276
+ for (const edge of plan.hard_dependencies) {
277
+ if (edge.to.project_id === plan.project_id && edge.to.plan_key === plan.plan_key && edge.to.task_id === taskId
278
+ && edge.from.project_id === plan.project_id && edge.from.plan_key === plan.plan_key) result.push(edge.from.task_id);
279
+ }
280
+ for (const join of plan.joins) {
281
+ if (join.before.project_id === plan.project_id && join.before.plan_key === plan.plan_key && join.before.task_id === taskId) {
282
+ for (const after of join.after) if (after.project_id === plan.project_id && after.plan_key === plan.plan_key) result.push(after.task_id);
283
+ }
284
+ }
285
+ return [...new Set(result)];
286
+ }
287
+
288
+ function localSuccessors(plan, taskId) {
289
+ return plan.tasks.map(({ task_id }) => task_id).filter((candidate) => localPredecessors(plan, candidate).includes(taskId));
290
+ }
291
+
292
+ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSource } = {}) {
293
+ const states = new Map(plan.tasks.map(({ task_id }) => [task_id, taskState(task_id)]));
294
+ const phaseStates = new Map((plan.phases ?? []).map(({ phase_id }) => [phase_id, emptyPhaseState(phase_id)]));
295
+ const doneDigest = new Map();
296
+ const completion = new Map();
297
+ const importedGenesis = events[0]?.payload.historical_import === true;
298
+ let previousTime = null;
299
+ for (const event of events) {
300
+ if (event.project_id !== plan.project_id || event.plan_key !== plan.plan_key || event.plan_version !== plan.plan_version) {
301
+ fail('STORE_CORRUPT', 'journal_identity_mismatch');
302
+ }
303
+ if (!isStrictTodoTimestamp(event.recorded_at)) fail('STORE_CORRUPT', 'timestamp_invalid');
304
+ const time = Date.parse(event.recorded_at);
305
+ if (previousTime !== null && time < previousTime) fail('STORE_INCONSISTENT', 'clock_reversal');
306
+ if (time > now.valueOf() + MAX_FUTURE_SKEW_MS) fail('STORE_INCONSISTENT', 'future_clock_skew');
307
+ previousTime = time;
308
+ if (event.kind === 'plan_genesis') {
309
+ if (event.payload.plan_digest !== plan.plan_digest || event.payload.topology_digest !== plan.topology_digest
310
+ || event.payload.predecessor_plan_digest !== plan.predecessor_plan_digest) {
311
+ fail('STORE_INCONSISTENT', 'genesis_plan_binding_mismatch');
312
+ }
313
+ if (['lattice.todo_event.v2', 'lattice.todo_event.v4'].includes(event.schema)) {
314
+ const projected = event.state_migration.map(({ from_task_id, to_task_id }) => ({
315
+ from_task_id, to_task_id,
316
+ }));
317
+ if (canonicalizeTodoArtifact(projected) !== canonicalizeTodoArtifact(event.payload.task_migration)) {
318
+ fail('STORE_INCONSISTENT', 'genesis_migration_projection_mismatch');
319
+ }
320
+ const activeTargets = event.state_migration
321
+ .filter(({ to_task_id }) => to_task_id !== 'removed').map(({ to_task_id }) => to_task_id);
322
+ if (new Set(activeTargets).size !== activeTargets.length
323
+ || activeTargets.some((taskId) => !states.has(taskId))) {
324
+ fail('STORE_INCONSISTENT', 'genesis_migration_target_invalid');
325
+ }
326
+ for (const migration of event.state_migration) {
327
+ if (!['carry', 'carry_reconciled_metadata'].includes(migration.state_policy)) continue;
328
+ const state = states.get(migration.to_task_id);
329
+ Object.assign(state, structuredClone(migration.state), { evidence_unverified: false });
330
+ if (state.evidence !== null) {
331
+ if (state.imported) {
332
+ if (verifyImportSource) verifyImportSource(state.evidence);
333
+ } else if (verifyEvidence) verifyEvidence(state.evidence);
334
+ }
335
+ if (state.status === 'done') {
336
+ doneDigest.set(migration.to_task_id, event.event_digest);
337
+ completion.set(migration.to_task_id, state.imported
338
+ ? { mode: 'historical_import', completed_at: state.done_at ?? 'unknown_requires_evidence' }
339
+ : { mode: 'authored', completed_at: state.done_at });
340
+ }
341
+ }
342
+ if (event.schema === 'lattice.todo_event.v4') {
343
+ for (const migration of event.phase_state_migration) {
344
+ if (migration.state_policy === 'carry') {
345
+ Object.assign(phaseStates.get(migration.phase_id), structuredClone(migration.state));
346
+ }
347
+ }
348
+ }
349
+ }
350
+ continue;
351
+ }
352
+ if (event.kind.startsWith('phase_')) {
353
+ if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema) || !phaseStates.has(event.phase_id)) {
354
+ fail('STORE_INCONSISTENT', 'event_phase_missing');
355
+ }
356
+ const state = phaseStates.get(event.phase_id);
357
+ const currentStatus = derivedPhaseStatus(plan, states, phaseStates, event.phase_id);
358
+ if (event.kind === 'phase_review') {
359
+ if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
360
+ state.status = 'reviewing'; state.review_event_digest = event.event_digest;
361
+ state.decision_event_digest = null; state.decision_evidence = null;
362
+ } else if (event.kind === 'phase_accept') {
363
+ const phase = plan.phases.find(({ phase_id }) => phase_id === event.phase_id);
364
+ const slots = event.payload.evidence_slots.map(({ slot_id }) => slot_id);
365
+ if (currentStatus !== 'reviewing' || state.review_event_digest !== event.payload.review_event_digest
366
+ || canonicalizeTodoArtifact(slots) !== canonicalizeTodoArtifact(phase.required_evidence_slots)) {
367
+ fail('STORE_INCONSISTENT', 'phase_accept_binding_invalid');
368
+ }
369
+ if (verifyEvidence) {
370
+ verifyEvidence(event.payload.decision_evidence);
371
+ for (const slot of event.payload.evidence_slots) verifyEvidence(slot.evidence);
372
+ }
373
+ state.status = 'accepted'; state.decision_event_digest = event.event_digest;
374
+ state.decision_evidence = event.payload.decision_evidence;
375
+ } else if (event.kind === 'phase_reject') {
376
+ if (currentStatus !== 'reviewing' || state.review_event_digest !== event.payload.review_event_digest) {
377
+ fail('STORE_INCONSISTENT', 'phase_reject_binding_invalid');
378
+ }
379
+ if (verifyEvidence) verifyEvidence(event.payload.decision_evidence);
380
+ state.status = 'rejected'; state.decision_event_digest = event.event_digest;
381
+ state.decision_evidence = event.payload.decision_evidence;
382
+ } else {
383
+ if (!['accepted', 'rejected'].includes(currentStatus)
384
+ || state.decision_event_digest !== event.payload.target_decision_digest) {
385
+ fail('STORE_INCONSISTENT', 'phase_reopen_binding_invalid');
386
+ }
387
+ const successorStarted = currentStatus === 'accepted' && (plan.schema === 'lattice.todo_plan.v4'
388
+ ? plan.phases.some((phase) => phase.predecessor_phase_ids.includes(event.phase_id)
389
+ && (phaseStates.get(phase.phase_id).status !== 'locked'
390
+ || plan.tasks.some((task) => task.phase_id === phase.phase_id
391
+ && states.get(task.task_id).status !== 'pending')))
392
+ : plan.phases.some((phase) => phase.predecessor_phase_ids.includes(event.phase_id)
393
+ && phaseStates.get(phase.phase_id).status !== 'locked')
394
+ || plan.phase_accept_dependencies.some((edge) => edge.from.project_id === plan.project_id
395
+ && edge.from.plan_key === plan.plan_key && edge.from.phase_id === event.phase_id
396
+ && edge.to.project_id === plan.project_id && edge.to.plan_key === plan.plan_key
397
+ && states.get(edge.to.task_id)?.status !== 'pending'));
398
+ if (successorStarted && event.payload.override_reason === null) {
399
+ fail('STORE_INCONSISTENT', 'phase_reopen_has_started_successor');
400
+ }
401
+ Object.assign(state, emptyPhaseState(event.phase_id));
402
+ }
403
+ continue;
404
+ }
405
+ const state = states.get(event.task_id);
406
+ if (state === undefined) fail('STORE_INCONSISTENT', 'event_task_missing');
407
+ const dependenciesDone = localPredecessors(plan, event.task_id).every((id) => states.get(id)?.status === 'done');
408
+ if (event.kind === 'start') {
409
+ if (event.payload.start_mode === 'historical_import') {
410
+ if (!importedGenesis || state.status !== 'pending') {
411
+ fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
412
+ }
413
+ if (verifyImportSource) verifyImportSource(event.payload.evidence);
414
+ state.status = 'in-progress';
415
+ state.started_at = event.payload.started_at === 'unknown_requires_evidence'
416
+ ? null : event.payload.started_at;
417
+ state.evidence = event.payload.evidence;
418
+ state.imported = true;
419
+ } else {
420
+ const phaseStatus = plan.schema === 'lattice.todo_plan.v4'
421
+ ? derivedPhaseStatus(plan, states, phaseStates,
422
+ plan.tasks.find(({ task_id }) => task_id === event.task_id).phase_id) : 'active';
423
+ if (state.status !== 'pending' || phaseStatus !== 'active'
424
+ || (!dependenciesDone && event.payload.override_reason === null)) {
425
+ fail('STORE_INCONSISTENT', 'invalid_start_transition');
426
+ }
427
+ state.status = 'in-progress'; state.started_at = event.recorded_at;
428
+ }
429
+ } else if (event.kind === 'block') {
430
+ if (state.status !== 'in-progress') fail('STORE_INCONSISTENT', 'invalid_block_transition');
431
+ state.status = 'blocked'; state.blocked_reason = event.payload.reason;
432
+ } else if (event.kind === 'unblock') {
433
+ if (state.status !== 'blocked') fail('STORE_INCONSISTENT', 'invalid_unblock_transition');
434
+ state.status = 'in-progress'; state.blocked_reason = null;
435
+ } else if (event.kind === 'done') {
436
+ if (event.payload.done_mode === 'authored') {
437
+ if (state.status !== 'in-progress' || !dependenciesDone) fail('STORE_INCONSISTENT', 'invalid_done_transition');
438
+ if (verifyEvidence) verifyEvidence(event.payload.evidence);
439
+ state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
440
+ state.imported = false;
441
+ completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
442
+ } else if (event.payload.done_mode === 'historical_import') {
443
+ if (!importedGenesis || state.status !== 'pending') fail('STORE_INCONSISTENT', 'invalid_historical_import_transition');
444
+ if (verifyImportSource) verifyImportSource(event.payload.evidence);
445
+ state.status = 'done'; state.done_at = event.payload.completed_at === 'unknown_requires_evidence'
446
+ ? null : event.payload.completed_at;
447
+ state.evidence = event.payload.evidence; state.imported = true;
448
+ completion.set(event.task_id, { mode: 'historical_import', completed_at: event.payload.completed_at });
449
+ } else {
450
+ const current = completion.get(event.task_id);
451
+ if (state.status !== 'done' || current?.mode !== 'historical_import'
452
+ || current.completed_at !== 'unknown_requires_evidence'
453
+ || doneDigest.get(event.task_id) !== event.payload.target_done_digest) {
454
+ fail('STORE_INCONSISTENT', 'invalid_evidence_promotion');
455
+ }
456
+ if (verifyEvidence) verifyEvidence(event.payload.evidence);
457
+ state.evidence = event.payload.evidence;
458
+ completion.set(event.task_id, { mode: 'evidence_promotion', completed_at: current.completed_at });
459
+ }
460
+ doneDigest.set(event.task_id, event.event_digest);
461
+ } else if (event.kind === 'reopen') {
462
+ if (state.status !== 'done' || doneDigest.get(event.task_id) !== event.payload.target_done_digest) {
463
+ fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
464
+ }
465
+ if (['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema)) {
466
+ const phaseId = plan.tasks.find(({ task_id }) => task_id === event.task_id).phase_id;
467
+ if (derivedPhaseStatus(plan, states, phaseStates, phaseId) === 'accepted') {
468
+ fail('STORE_INCONSISTENT', 'task_reopen_requires_phase_reopen');
469
+ }
470
+ }
471
+ const startedSuccessor = localSuccessors(plan, event.task_id).some((id) => states.get(id).status !== 'pending');
472
+ if (startedSuccessor && event.payload.override_reason === null) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
473
+ state.status = 'in-progress'; state.done_at = null; state.evidence = null;
474
+ completion.delete(event.task_id);
475
+ }
476
+ }
477
+ return [...states.values()].sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0);
478
+ }
479
+
480
+ function snapshotFor(plan, events, tasks) {
481
+ const head = events.at(-1);
482
+ const phasePlan = ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema);
483
+ const snapshot = {
484
+ schema: phasePlan ? 'lattice.todo_snapshot.v2' : 'lattice.todo_snapshot.v1',
485
+ project_id: plan.project_id, plan_key: plan.plan_key,
486
+ plan_version: plan.plan_version, projection_version: phasePlan ? 2 : 1, through_sequence: head.sequence,
487
+ journal_head_digest: head.event_digest, tasks, snapshot_digest: '',
488
+ ...(phasePlan ? { phases: projectPhaseStates(plan, events,
489
+ new Map(tasks.map((task) => [task.task_id, task]))) } : {}),
490
+ };
491
+ snapshot.snapshot_digest = todoSelfDigest(snapshot, 'snapshot_digest');
492
+ return snapshot;
493
+ }
494
+
495
+ function validateMergedGraph(members) {
496
+ const tasks = new Map();
497
+ for (const member of members) for (const task of member.plan.tasks) {
498
+ tasks.set(`${member.plan.project_id}\0${member.plan.plan_key}\0${task.task_id}`, member.plan.topology_digest);
499
+ }
500
+ const adjacency = new Map([...tasks.keys()].map((key) => [key, []]));
501
+ const phases = new Map();
502
+ for (const member of members) {
503
+ if (member.plan.schema !== 'lattice.todo_plan.v5') continue;
504
+ for (const phase of member.plan.phases) {
505
+ const key = `phase\0${member.plan.project_id}\0${member.plan.plan_key}\0${phase.phase_id}`;
506
+ phases.set(key, member.plan.topology_digest);
507
+ adjacency.set(key, []);
508
+ }
509
+ }
510
+ const bind = (ref, ownerPlan) => {
511
+ const key = `${ref.project_id}\0${ref.plan_key}\0${ref.task_id}`;
512
+ const topology = tasks.get(key);
513
+ if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_dependency');
514
+ if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
515
+ && ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
516
+ if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
517
+ fail('STORE_INCONSISTENT', 'binding_stale');
518
+ }
519
+ return key;
520
+ };
521
+ const bindPhase = (ref, ownerPlan) => {
522
+ const key = `phase\0${ref.project_id}\0${ref.plan_key}\0${ref.phase_id}`;
523
+ const topology = phases.get(key);
524
+ if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_phase_dependency');
525
+ if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
526
+ && ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
527
+ if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
528
+ fail('STORE_INCONSISTENT', 'binding_stale');
529
+ }
530
+ return key;
531
+ };
532
+ for (const { plan } of members) {
533
+ for (const edge of plan.hard_dependencies) {
534
+ const from = bind(edge.from, plan); const to = bind(edge.to, plan);
535
+ if (from === to) fail('STORE_INCONSISTENT', 'self_edge');
536
+ adjacency.get(from).push(to);
537
+ }
538
+ for (const join of plan.joins) {
539
+ const before = bind(join.before, plan);
540
+ for (const afterRef of join.after) {
541
+ const after = bind(afterRef, plan);
542
+ if (after === before) fail('STORE_INCONSISTENT', 'join_self_edge');
543
+ adjacency.get(after).push(before);
544
+ }
545
+ }
546
+ if (plan.schema === 'lattice.todo_plan.v5') {
547
+ for (const task of plan.tasks) {
548
+ const taskKey = `${plan.project_id}\0${plan.plan_key}\0${task.task_id}`;
549
+ const phaseKey = `phase\0${plan.project_id}\0${plan.plan_key}\0${task.phase_id}`;
550
+ adjacency.get(taskKey).push(phaseKey);
551
+ }
552
+ for (const phase of plan.phases) {
553
+ const to = `phase\0${plan.project_id}\0${plan.plan_key}\0${phase.phase_id}`;
554
+ for (const predecessor of phase.predecessor_phase_ids) {
555
+ adjacency.get(`phase\0${plan.project_id}\0${plan.plan_key}\0${predecessor}`).push(to);
556
+ }
557
+ }
558
+ for (const edge of plan.phase_accept_dependencies) {
559
+ adjacency.get(bindPhase(edge.from, plan)).push(bind(edge.to, plan));
560
+ }
561
+ }
562
+ }
563
+ const colors = new Map();
564
+ const visit = (node) => {
565
+ if (colors.get(node) === 1) fail('STORE_INCONSISTENT', 'merged_cycle');
566
+ if (colors.get(node) === 2) return;
567
+ colors.set(node, 1); for (const next of adjacency.get(node)) visit(next); colors.set(node, 2);
568
+ };
569
+ for (const node of adjacency.keys()) visit(node);
570
+ }
571
+
572
+ function mergedTaskKey(refValue) {
573
+ return `${refValue.project_id}\0${refValue.plan_key}\0${refValue.task_id}`;
574
+ }
575
+
576
+ function mergedTaskStates(store) {
577
+ return new Map(store.members.flatMap((member) => member.tasks.map((taskValue) => ([
578
+ mergedTaskKey({
579
+ project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: taskValue.task_id,
580
+ }),
581
+ taskValue.status,
582
+ ]))));
583
+ }
584
+
585
+ function mergedTaskPhase(store, taskKey) {
586
+ for (const member of store.members) {
587
+ const task = member.plan.tasks.find((entry) => mergedTaskKey({
588
+ project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: entry.task_id,
589
+ }) === taskKey);
590
+ if (task === undefined) continue;
591
+ if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(member.plan.schema)) return null;
592
+ return { schema: member.plan.schema, plan_key: member.plan.plan_key, phase_id: task.phase_id,
593
+ status: member.snapshot.phases.find(({ phase_id }) => phase_id === task.phase_id)?.status };
594
+ }
595
+ return undefined;
596
+ }
597
+
598
+ function mergedDependencyPhaseReady(store, predecessorKey, targetKey) {
599
+ const predecessor = mergedTaskPhase(store, predecessorKey);
600
+ const target = mergedTaskPhase(store, targetKey);
601
+ if (predecessor === undefined) return false;
602
+ if (predecessor === null) return true;
603
+ if (predecessor.schema === 'lattice.todo_plan.v5') return true;
604
+ if (target !== null && target !== undefined && predecessor.plan_key === target.plan_key
605
+ && predecessor.phase_id === target.phase_id) return true;
606
+ return predecessor.status === 'accepted';
607
+ }
608
+
609
+ function mergedPhaseAcceptReady(store, targetKey) {
610
+ for (const member of store.members) {
611
+ if (member.plan.schema !== 'lattice.todo_plan.v5') continue;
612
+ for (const edge of member.plan.phase_accept_dependencies) {
613
+ if (mergedTaskKey(edge.to) !== targetKey) continue;
614
+ const source = store.members.find((candidate) => candidate.plan.project_id === edge.from.project_id
615
+ && candidate.plan.plan_key === edge.from.plan_key);
616
+ if (source?.snapshot.phases.find(({ phase_id }) => phase_id === edge.from.phase_id)?.status !== 'accepted') {
617
+ return false;
618
+ }
619
+ }
620
+ }
621
+ return true;
622
+ }
623
+
624
+ function mergedPredecessorKeys(store, targetKey) {
625
+ const result = [];
626
+ for (const member of store.members) {
627
+ for (const edge of member.plan.hard_dependencies) {
628
+ if (mergedTaskKey(edge.to) === targetKey) result.push(mergedTaskKey(edge.from));
629
+ }
630
+ for (const join of member.plan.joins) {
631
+ if (mergedTaskKey(join.before) === targetKey) {
632
+ for (const after of join.after) result.push(mergedTaskKey(after));
633
+ }
634
+ }
635
+ }
636
+ return [...new Set(result)];
637
+ }
638
+
639
+ function mergedSuccessorKeys(store, targetKey) {
640
+ const result = [];
641
+ for (const member of store.members) {
642
+ for (const edge of member.plan.hard_dependencies) {
643
+ if (mergedTaskKey(edge.from) === targetKey) result.push(mergedTaskKey(edge.to));
644
+ }
645
+ for (const join of member.plan.joins) {
646
+ if (join.after.some((after) => mergedTaskKey(after) === targetKey)) {
647
+ result.push(mergedTaskKey(join.before));
648
+ }
649
+ }
650
+ }
651
+ return [...new Set(result)];
652
+ }
653
+
654
+ function validateMergedTransition(store, member, event) {
655
+ const targetKey = mergedTaskKey({
656
+ project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: event.task_id,
657
+ });
658
+ const states = mergedTaskStates(store);
659
+ const taskDependenciesDone = mergedPredecessorKeys(store, targetKey)
660
+ .every((key) => states.get(key) === 'done' && mergedDependencyPhaseReady(store, key, targetKey));
661
+ const phaseAcceptReady = mergedPhaseAcceptReady(store, targetKey);
662
+ const dependenciesDone = taskDependenciesDone && phaseAcceptReady;
663
+ if (event.kind === 'start' && event.payload.start_mode !== 'historical_import'
664
+ && (!phaseAcceptReady || (!taskDependenciesDone && event.payload.override_reason === null))) {
665
+ fail('STORE_INCONSISTENT', 'invalid_start_transition');
666
+ }
667
+ if (event.kind === 'done' && event.payload.done_mode === 'authored' && !dependenciesDone) {
668
+ fail('STORE_INCONSISTENT', 'invalid_done_transition');
669
+ }
670
+ if (event.kind === 'reopen' && event.payload.override_reason === null) {
671
+ const startedSuccessor = mergedSuccessorKeys(store, targetKey)
672
+ .some((key) => states.get(key) !== 'pending');
673
+ if (startedSuccessor) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
674
+ }
675
+ if (event.kind === 'phase_reopen' && event.payload.override_reason === null) {
676
+ const startedSuccessor = member.plan.schema === 'lattice.todo_plan.v4'
677
+ ? member.plan.tasks.filter(({ phase_id }) => phase_id === event.phase_id)
678
+ .map(({ task_id }) => mergedTaskKey({ project_id: member.plan.project_id,
679
+ plan_key: member.plan.plan_key, task_id }))
680
+ .flatMap((key) => mergedSuccessorKeys(store, key))
681
+ .some((key) => states.get(key) !== 'pending')
682
+ : store.members.some((candidate) => candidate.plan.schema === 'lattice.todo_plan.v5'
683
+ && candidate.plan.phase_accept_dependencies.some((edge) => (
684
+ edge.from.project_id === member.plan.project_id
685
+ && edge.from.plan_key === member.plan.plan_key && edge.from.phase_id === event.phase_id
686
+ && states.get(mergedTaskKey(edge.to)) !== 'pending'
687
+ )));
688
+ if (startedSuccessor) fail('STORE_INCONSISTENT', 'phase_reopen_has_started_successor');
689
+ }
690
+ }
691
+
692
+ function evidenceVerifier(manifest, repoRoot, hard) {
693
+ const repositories = new Map(manifest.repositories.map((repo) => [repo.repo_id, repo.path]));
694
+ return (descriptor) => {
695
+ if (!validateEvidenceDescriptor(descriptor)) fail('STORE_INCONSISTENT', 'evidence_descriptor_invalid');
696
+ const repoRef = repositories.get(descriptor.repo_id);
697
+ if (repoRef === undefined) fail('STORE_INCONSISTENT', 'evidence_repo_missing');
698
+ const absoluteRepo = path.resolve(repoRoot, repoRef);
699
+ try {
700
+ const type = execFileSync('git', ['cat-file', '-t', descriptor.git_blob_oid], { cwd: absoluteRepo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
701
+ if (type !== 'blob') throw new Error('not blob');
702
+ const bytes = execFileSync('git', ['cat-file', 'blob', descriptor.git_blob_oid], { cwd: absoluteRepo, encoding: 'buffer', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: TODO_LIMITS.narrativeSectionBytes + 1 });
703
+ if (sha256Bytes(bytes) !== descriptor.content_digest) throw new Error('digest mismatch');
704
+ return true;
705
+ } catch {
706
+ if (hard) fail('STORE_INCONSISTENT', 'evidence_unverified');
707
+ return false;
708
+ }
709
+ };
710
+ }
711
+
712
+ function pinnedSourceLine(repoRoot, source, cache = null) {
713
+ if (cache === null || !cache.commits.has(source.source_commit)) {
714
+ const commitType = execFileSync('git', ['cat-file', '-t', source.source_commit], {
715
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
716
+ }).trim();
717
+ if (commitType !== 'commit') throw new Error('not commit');
718
+ cache?.commits.add(source.source_commit);
719
+ }
720
+ const objectSpec = `${source.source_commit}:${source.origin_plan_ref}`;
721
+ let blob = cache?.blobs.get(objectSpec);
722
+ if (blob === undefined) {
723
+ const blobType = execFileSync('git', ['cat-file', '-t', objectSpec], {
724
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
725
+ }).trim();
726
+ if (blobType !== 'blob') throw new Error('not blob');
727
+ blob = execFileSync('git', ['cat-file', 'blob', objectSpec], {
728
+ cwd: repoRoot, encoding: 'buffer', stdio: ['ignore', 'pipe', 'ignore'],
729
+ maxBuffer: TODO_LIMITS.narrativeSectionBytes + 1,
730
+ });
731
+ cache?.blobs.set(objectSpec, blob);
732
+ }
733
+ let start = 0;
734
+ let line = 1;
735
+ for (let index = 0; index < blob.length; index += 1) {
736
+ if (blob[index] !== 0x0a) continue;
737
+ if (line === source.origin_line) return blob.subarray(start, index);
738
+ start = index + 1;
739
+ line += 1;
740
+ }
741
+ if (start < blob.length && line === source.origin_line) return blob.subarray(start);
742
+ throw new Error('line outside blob');
743
+ }
744
+
745
+ function markdownCheckboxState(lineBytes) {
746
+ if (lineBytes.length >= 3 && lineBytes[0] === 0xef && lineBytes[1] === 0xbb && lineBytes[2] === 0xbf) return null;
747
+ let line;
748
+ try { line = new TextDecoder('utf-8', { fatal: true }).decode(lineBytes); } catch { return null; }
749
+ const match = /^[\t ]*(?:[-+*]|\d+[A-Za-z]?\.|\d+\))[\t ]+\[([ xX])\](?:[\t ]+.*?)?\r?$/u.exec(line);
750
+ if (match === null) return null;
751
+ return match[1] === ' ' ? 'unchecked' : 'checked';
752
+ }
753
+
754
+ function liveReplacementPreservesListStructure(lineBytes, replacement) {
755
+ let line;
756
+ try { line = new TextDecoder('utf-8', { fatal: true }).decode(lineBytes); } catch { return false; }
757
+ const source = /^([\t ]*)([-+*]|\d+[A-Za-z]?\.|\d+\))[\t ]+\[[ xX]\](?:[\t ]+.*)?\r?$/u.exec(line);
758
+ const target = /^([\t ]*)([-+*]|\d+[A-Za-z]?\.|\d+\))[\t ]+.+$/u.exec(replacement);
759
+ return source !== null && target !== null && source[1] === target[1] && source[2] === target[2];
760
+ }
761
+
762
+ function importSourceVerifier(repoRoot, hard, cache = null) {
763
+ return (descriptor) => {
764
+ if (!validateTodoImportSource(descriptor)) fail('STORE_INCONSISTENT', 'import_source_descriptor_invalid');
765
+ try {
766
+ pinnedSourceLine(repoRoot, descriptor, cache);
767
+ return true;
768
+ } catch {
769
+ if (hard) fail('STORE_INCONSISTENT', 'import_source_unverified');
770
+ return false;
771
+ }
772
+ };
773
+ }
774
+
775
+ function narrativeAnchorSource(value) {
776
+ return exactRecord(value, [
777
+ 'task_id', 'origin_plan_ref', 'origin_line', 'source_commit', 'checkbox_state',
778
+ ]) && isTodoIdentifier(value.task_id)
779
+ && ['checked', 'unchecked', 'absent', 'ambiguous'].includes(value.checkbox_state)
780
+ && validateTodoImportSource({
781
+ schema: 'lattice.todo_import_source.v1', origin_plan_ref: value.origin_plan_ref,
782
+ origin_line: value.origin_line, source_commit: value.source_commit,
783
+ });
784
+ }
785
+
786
+ function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
787
+ if (sources === undefined) return planInput;
788
+ if (planInput?.schema !== 'lattice.todo_plan.v2' || !Array.isArray(sources)
789
+ || sources.length > TODO_LIMITS.tasksPerPlan || !sources.every(narrativeAnchorSource)) {
790
+ throw new TypeError('imported narrative anchor sources are invalid');
791
+ }
792
+ const byTask = new Map(sources.map((source) => [source.task_id, source]));
793
+ if (byTask.size !== sources.length
794
+ || sources.some(({ task_id: taskId }) => !planInput.tasks.some((task) => task.task_id === taskId))) {
795
+ throw new TypeError('imported narrative anchor sources do not match plan tasks');
796
+ }
797
+ return {
798
+ ...planInput,
799
+ tasks: planInput.tasks.map((task) => {
800
+ const source = byTask.get(task.task_id);
801
+ let narrativeAnchor = null;
802
+ if (source !== undefined && task.narrative_ref === source.origin_plan_ref
803
+ && ['checked', 'unchecked'].includes(source.checkbox_state)) {
804
+ try {
805
+ const line = pinnedSourceLine(repoRoot, source);
806
+ if (markdownCheckboxState(line) === source.checkbox_state) {
807
+ narrativeAnchor = {
808
+ origin_plan_ref: source.origin_plan_ref,
809
+ origin_line: source.origin_line,
810
+ source_commit: source.source_commit,
811
+ source_line_digest: sha256Bytes(line),
812
+ };
813
+ }
814
+ } catch {
815
+ narrativeAnchor = null;
816
+ }
817
+ }
818
+ return { ...task, narrative_anchor: narrativeAnchor };
819
+ }),
820
+ };
821
+ }
822
+
823
+ function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null) {
824
+ if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema)) return;
825
+ const trusted = new Map((['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(trustedPlan?.schema)
826
+ ? trustedPlan.tasks : [])
827
+ .map((task) => [task.task_id, task.narrative_anchor]));
828
+ for (const task of plan.tasks) {
829
+ const anchor = task.narrative_anchor;
830
+ if (anchor === null) continue;
831
+ const previous = trusted.get(task.task_id);
832
+ if (previous !== undefined
833
+ && canonicalizeTodoArtifact(previous) === canonicalizeTodoArtifact(anchor)) continue;
834
+ try {
835
+ const line = pinnedSourceLine(repoRoot, anchor);
836
+ if (sha256Bytes(line) !== anchor.source_line_digest || markdownCheckboxState(line) === null) {
837
+ throw new Error('anchor mismatch');
838
+ }
839
+ } catch {
840
+ fail('STORE_INCONSISTENT', 'narrative_anchor_unverified', {
841
+ plan_key: plan.plan_key, task_id: task.task_id,
842
+ });
843
+ }
844
+ }
845
+ }
846
+
847
+ export async function readTodoStore(options = {}) {
848
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
849
+ const barrier = await exactFileOrNull(path.resolve(repoRoot, SOURCE_CUTOVER_BARRIER_REF));
850
+ if (barrier !== null) {
851
+ let value;
852
+ try { value = JSON.parse(decodeUtf8(barrier, 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid')); }
853
+ catch (error) {
854
+ if (error instanceof TodoStoreError) throw error;
855
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid');
856
+ }
857
+ const revisionRecovery = value?.schema === 'lattice.todo_source_cutover_barrier.v1'
858
+ && value.revision_digest === options.allowSourceCutoverRevisionDigest;
859
+ const revisionSetRecovery = value?.schema === 'lattice.todo_source_cutover_set_barrier.v1'
860
+ && value.revision_set_digest === options.allowSourceCutoverRevisionSetDigest;
861
+ if (options.sourceCutoverRecoveryCapability !== SOURCE_CUTOVER_RECOVERY_CAPABILITY
862
+ || (!revisionRecovery && !revisionSetRecovery)) {
863
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_recovery_required');
864
+ }
865
+ }
866
+ const pinnedSourceCache = { commits: new Set(), blobs: new Map() };
867
+ const manifest = await readArtifact(repoRoot, MANIFEST_REF, {
868
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
869
+ });
870
+ const loaded = [];
871
+ for (const descriptor of manifest.members) {
872
+ const plan = await readArtifact(repoRoot, descriptor.plan_ref, {
873
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoPlan,
874
+ });
875
+ if (plan.project_id !== manifest.project_id || plan.plan_key !== descriptor.plan_key
876
+ || plan.plan_version !== descriptor.active_plan_version || plan.topology_digest !== descriptor.topology_digest) {
877
+ fail('STORE_INCONSISTENT', 'manifest_plan_binding_mismatch');
878
+ }
879
+ const journal = await readJournal(repoRoot, descriptor.journal_ref);
880
+ if (journal.events.at(-1).event_digest !== descriptor.journal_head_digest) {
881
+ fail('STORE_INCONSISTENT', 'manifest_journal_head_mismatch');
882
+ }
883
+ let revision = null;
884
+ const genesis = journal.events[0];
885
+ if (['lattice.todo_event.v2', 'lattice.todo_event.v4'].includes(genesis.schema)) {
886
+ const revisionRef = path.posix.join(path.posix.dirname(descriptor.plan_ref), 'revision.json');
887
+ const phaseRevision = genesis.schema === 'lattice.todo_event.v4';
888
+ revision = await readArtifact(repoRoot, revisionRef, {
889
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes,
890
+ validate: (value) => {
891
+ rejectUnsupportedRevisionSchema(value, phaseRevision ? {
892
+ family: 'phase_todo_revision', supportedSchemas: PHASE_TODO_REVISION_SCHEMAS,
893
+ } : {
894
+ family: 'todo_revision', supportedSchemas: TODO_REVISION_SCHEMAS,
895
+ });
896
+ return phaseRevision ? validatePhaseTodoRevision(value) : validateTodoRevision(value);
897
+ },
898
+ });
899
+ const migrationProjection = genesis.state_migration.map((entry) => ({
900
+ from_task_id: entry.from_task_id, to_task_id: entry.to_task_id,
901
+ state_policy: entry.state_policy,
902
+ }));
903
+ if (canonicalizeTodoArtifact(revision.desired_plan) !== canonicalizeTodoArtifact(plan)
904
+ || revision.revision_digest !== genesis.revision_digest
905
+ || revision.predecessor.plan_digest !== genesis.payload.predecessor_plan_digest
906
+ || revision.predecessor.journal_head_digest !== genesis.previous_digest
907
+ || canonicalizeTodoArtifact(revision.task_migration)
908
+ !== canonicalizeTodoArtifact(migrationProjection)) {
909
+ fail('STORE_INCONSISTENT', 'revision_genesis_binding_mismatch');
910
+ }
911
+ if (manifest.schema === 'lattice.todo_manifest.v2'
912
+ && descriptor.active_revision_digest !== genesis.revision_digest) {
913
+ fail('STORE_INCONSISTENT', 'manifest_revision_binding_mismatch');
914
+ }
915
+ if (genesis.schema === 'lattice.todo_event.v2'
916
+ && revision.reconciliation.reconciliation_digest !== genesis.reconciliation_digest) {
917
+ fail('STORE_INCONSISTENT', 'revision_genesis_binding_mismatch');
918
+ }
919
+ if (genesis.schema === 'lattice.todo_event.v4') {
920
+ const phaseProjection = revision.phase_migration
921
+ .filter(({ to_phase_id }) => to_phase_id !== 'removed')
922
+ .map(({ to_phase_id, state_policy }) => ({ phase_id: to_phase_id, state_policy }))
923
+ .sort((left, right) => left.phase_id.localeCompare(right.phase_id));
924
+ const genesisProjection = genesis.phase_state_migration
925
+ .map(({ phase_id, state_policy }) => ({ phase_id, state_policy }));
926
+ if (canonicalizeTodoArtifact(phaseProjection) !== canonicalizeTodoArtifact(genesisProjection)) {
927
+ fail('STORE_INCONSISTENT', 'revision_genesis_binding_mismatch');
928
+ }
929
+ }
930
+ }
931
+ if (manifest.schema === 'lattice.todo_manifest.v2' && revision === null
932
+ && descriptor.active_revision_digest !== plan.plan_digest) {
933
+ fail('STORE_INCONSISTENT', 'manifest_revision_binding_mismatch');
934
+ }
935
+ const verifyEvidence = evidenceVerifier(manifest, repoRoot, options.forWrite === true);
936
+ const verifyImportSource = importSourceVerifier(repoRoot, options.forWrite === true, pinnedSourceCache);
937
+ const tasks = replay(plan, journal.events, {
938
+ now: options.now ? new Date(options.now) : new Date(),
939
+ verifyEvidence: options.forWrite === true ? verifyEvidence : undefined,
940
+ verifyImportSource: options.forWrite === true ? verifyImportSource : undefined,
941
+ });
942
+ const expectedSnapshot = snapshotFor(plan, journal.events, structuredClone(tasks));
943
+ // Read-time evidence failure is annotation, never store rejection.
944
+ for (const task of tasks) if (task.evidence !== null) {
945
+ const verified = validateTodoImportSource(task.evidence)
946
+ ? verifyImportSource(task.evidence) : verifyEvidence(task.evidence);
947
+ if (!verified) task.evidence_unverified = true;
948
+ }
949
+ let snapshot = null;
950
+ let snapshotStale = false;
951
+ try {
952
+ snapshot = await readSnapshotArtifact(repoRoot, descriptor.snapshot_ref);
953
+ snapshotStale = snapshot === null || canonicalizeTodoArtifact(snapshot) !== canonicalizeTodoArtifact(expectedSnapshot);
954
+ } catch (error) {
955
+ if (error instanceof TodoStoreError && error.code === 'SNAPSHOT_INVALID'
956
+ && !['unsafe_artifact_path', 'path_alias_or_escape', 'path_outside_store'].includes(error.detail.reason)) snapshotStale = true;
957
+ else throw error;
958
+ }
959
+ if (options.forWrite === true && snapshotStale) fail('STORE_WRITE_REFUSED', 'snapshot_stale');
960
+ loaded.push({ descriptor, plan, revision, journal, snapshot: snapshotStale ? expectedSnapshot : snapshot,
961
+ tasks, snapshot_stale: snapshotStale });
962
+ }
963
+ validateMergedGraph(loaded);
964
+ return {
965
+ schema: 'lattice.todo_store_read.v1', project_id: manifest.project_id, manifest,
966
+ members: loaded, snapshot_stale: loaded.some((member) => member.snapshot_stale),
967
+ };
968
+ }
969
+
970
+ export async function readTodoStoreStable(options = {}) {
971
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
972
+ const maximumAttempts = options.maximumAttempts ?? 4;
973
+ if (!Number.isSafeInteger(maximumAttempts) || maximumAttempts < 1 || maximumAttempts > 16) {
974
+ throw new TypeError('maximumAttempts must be 1..16');
975
+ }
976
+ for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
977
+ const before = await readArtifact(repoRoot, MANIFEST_REF, {
978
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
979
+ });
980
+ try {
981
+ const store = await readTodoStore(options);
982
+ const after = await readArtifact(repoRoot, MANIFEST_REF, {
983
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
984
+ });
985
+ if (before.manifest_digest === after.manifest_digest
986
+ && store.manifest.manifest_digest === after.manifest_digest) return store;
987
+ } catch (error) {
988
+ if (!(error instanceof TodoStoreError)) throw error;
989
+ const after = await readArtifact(repoRoot, MANIFEST_REF, {
990
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
991
+ });
992
+ const transientWriteWindow = error.code === 'STORE_INCONSISTENT'
993
+ && ['manifest_journal_head_mismatch', 'manifest_plan_binding_mismatch']
994
+ .includes(error.detail.reason);
995
+ if (before.manifest_digest === after.manifest_digest && !transientWriteWindow) throw error;
996
+ }
997
+ if (attempt < maximumAttempts) {
998
+ await new Promise((resolve) => setTimeout(resolve, Math.min(16, 2 ** attempt)));
999
+ }
1000
+ }
1001
+ fail('STORE_BUSY', 'stable_read_exhausted', { attempts: maximumAttempts });
1002
+ }
1003
+
1004
+ async function atomicWrite(absolute, bytes) {
1005
+ await mkdir(path.dirname(absolute), { recursive: true });
1006
+ const temporary = path.join(path.dirname(absolute), `.${path.basename(absolute)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
1007
+ let handle;
1008
+ try {
1009
+ handle = await open(temporary, 'wx', 0o600);
1010
+ await handle.writeFile(bytes); await handle.sync(); await handle.close(); handle = null;
1011
+ await rename(temporary, absolute);
1012
+ const directory = await open(path.dirname(absolute), 'r');
1013
+ try { await directory.sync(); } finally { await directory.close(); }
1014
+ } finally {
1015
+ if (handle) await handle.close();
1016
+ await rm(temporary, { force: true });
1017
+ }
1018
+ }
1019
+
1020
+ async function atomicWriteMode(absolute, bytes, mode) {
1021
+ await mkdir(path.dirname(absolute), { recursive: true });
1022
+ const temporary = path.join(path.dirname(absolute),
1023
+ `.${path.basename(absolute)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
1024
+ let handle;
1025
+ try {
1026
+ handle = await open(temporary, 'wx', mode);
1027
+ await handle.writeFile(bytes); await handle.chmod(mode);
1028
+ await handle.sync(); await handle.close(); handle = null;
1029
+ await rename(temporary, absolute);
1030
+ await fsyncDirectory(path.dirname(absolute));
1031
+ } finally {
1032
+ if (handle) await handle.close();
1033
+ await rm(temporary, { force: true });
1034
+ }
1035
+ }
1036
+
1037
+ async function fsyncDirectory(absolute) {
1038
+ const directory = await open(absolute, 'r');
1039
+ try { await directory.sync(); } finally { await directory.close(); }
1040
+ }
1041
+
1042
+ async function withLock(repoRoot, callback) {
1043
+ const root = path.join(repoRoot, STORE_ROOT_REF);
1044
+ await mkdir(root, { recursive: true });
1045
+ const lockRef = path.join(root, '.write.lock');
1046
+ let handle;
1047
+ try { handle = await open(lockRef, 'wx', 0o600); }
1048
+ catch (error) { if (error?.code === 'EEXIST') fail('STORE_WRITE_CONFLICT', 'store_locked'); throw error; }
1049
+ try { return await callback(); }
1050
+ finally { await handle.close(); await rm(lockRef, { force: true }); }
1051
+ }
1052
+
1053
+ export async function rebuildTodoSnapshot(options = {}) {
1054
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
1055
+ return withLock(repoRoot, async () => {
1056
+ const store = await readTodoStore({ repoRoot, now: options.now });
1057
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
1058
+ if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');
1059
+ await atomicWrite(path.resolve(repoRoot, member.descriptor.snapshot_ref), canonicalLine(member.snapshot));
1060
+ return member.snapshot;
1061
+ });
1062
+ }
1063
+
1064
+ function nextEvent(input, storeMember) {
1065
+ const previous = storeMember.journal.events.at(-1);
1066
+ const payload = input.kind === 'done' && exactRecord(input.payload, ['evidence'])
1067
+ ? { done_mode: 'authored', imported: false, evidence: input.payload.evidence }
1068
+ : input.payload;
1069
+ const event = {
1070
+ schema: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(storeMember.plan.schema)
1071
+ ? 'lattice.todo_event.v3' : 'lattice.todo_event.v1', project_id: storeMember.plan.project_id,
1072
+ plan_key: storeMember.plan.plan_key, plan_version: storeMember.plan.plan_version,
1073
+ sequence: previous.sequence + 1, previous_digest: previous.event_digest,
1074
+ kind: input.kind, task_id: input.task_id ?? null,
1075
+ ...(['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(storeMember.plan.schema)
1076
+ ? { phase_id: input.phase_id ?? null } : {}),
1077
+ actor: input.actor, recorded_at: input.recorded_at,
1078
+ provenance: input.provenance ?? null, payload, event_digest: '',
1079
+ };
1080
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1081
+ if (!validateTodoEvent(event)) throw new TypeError('todo event input violates its declared schema');
1082
+ return event;
1083
+ }
1084
+
1085
+ function resolveTargetedEvent(input, storeMember) {
1086
+ if (input.kind === 'reopen' && exactRecord(input.payload, [
1087
+ 'reason', 'override_reason',
1088
+ ])) {
1089
+ const target = [...storeMember.journal.events].reverse().find((event) => (
1090
+ event.kind === 'done' && event.task_id === input.task_id
1091
+ ));
1092
+ if (target === undefined) fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
1093
+ return {
1094
+ ...input,
1095
+ payload: { ...input.payload, target_done_digest: target.event_digest },
1096
+ };
1097
+ }
1098
+ if (input.kind === 'done' && exactRecord(input.payload, [
1099
+ 'done_mode', 'imported', 'evidence',
1100
+ ]) && input.payload.done_mode === 'evidence_promotion' && input.payload.imported === true) {
1101
+ const target = [...storeMember.journal.events].reverse().find((event) => (
1102
+ event.kind === 'done' && event.task_id === input.task_id
1103
+ ));
1104
+ if (target === undefined) fail('STORE_INCONSISTENT', 'invalid_evidence_promotion');
1105
+ return {
1106
+ ...input,
1107
+ payload: { ...input.payload, target_done_digest: target.event_digest },
1108
+ };
1109
+ }
1110
+ if (input.kind === 'phase_reopen' && exactRecord(input.payload, ['reason', 'override_reason'])) {
1111
+ const target = [...storeMember.journal.events].reverse().find((event) => (
1112
+ ['phase_accept', 'phase_reject'].includes(event.kind) && event.phase_id === input.phase_id
1113
+ ));
1114
+ if (target === undefined) fail('STORE_INCONSISTENT', 'phase_reopen_binding_invalid');
1115
+ return { ...input, payload: { ...input.payload, target_decision_digest: target.event_digest } };
1116
+ }
1117
+ return input;
1118
+ }
1119
+
1120
+ export async function appendTodoEvent(options = {}) {
1121
+ requireWriter(options.writer, 'g5-authoring');
1122
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
1123
+ return withLock(repoRoot, async () => {
1124
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
1125
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
1126
+ if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');
1127
+ const input = resolveTargetedEvent({
1128
+ ...options.event,
1129
+ recorded_at: options.event.recorded_at ?? new Date().toISOString(),
1130
+ }, member);
1131
+ const event = nextEvent(input, member);
1132
+ if ((event.kind === 'start' && event.payload.start_mode === 'historical_import')
1133
+ || (event.kind === 'done' && event.payload.done_mode === 'historical_import')) {
1134
+ fail('STORE_WRITE_CONFLICT', 'historical_import_writer_required');
1135
+ }
1136
+ // Validate the prospective history, including hard evidence and transitions, before any write.
1137
+ validateMergedTransition(store, member, event);
1138
+ replay(member.plan, [...member.journal.events, event], {
1139
+ now: options.now ? new Date(options.now) : new Date(),
1140
+ verifyEvidence: evidenceVerifier(store.manifest, repoRoot, true),
1141
+ verifyImportSource: importSourceVerifier(repoRoot, true),
1142
+ });
1143
+ const eventBytes = canonicalLine(event);
1144
+ const activeRef = member.descriptor.journal_ref;
1145
+ const activeAbsolute = path.resolve(repoRoot, activeRef);
1146
+ if (member.journal.activeBytes.length + eventBytes.length > TODO_LIMITS.journalSegmentBytes) {
1147
+ const activeEvents = member.journal.segments.at(-1).events;
1148
+ const previousSegmentDigest = member.journal.segments.length > 1
1149
+ ? member.journal.segments.at(-2).events.at(-1).event_digest : '0'.repeat(64);
1150
+ const segmentDigest = sha256Bytes(member.journal.activeBytes);
1151
+ const name = `${String(activeEvents[0].sequence).padStart(12, '0')}-${String(activeEvents.at(-1).sequence).padStart(12, '0')}-${previousSegmentDigest}-${segmentDigest}.jsonl`;
1152
+ await atomicWrite(path.join(path.dirname(activeAbsolute), 'sealed', name), member.journal.activeBytes);
1153
+ await atomicWrite(activeAbsolute, eventBytes);
1154
+ } else {
1155
+ await atomicWrite(activeAbsolute, Buffer.concat([member.journal.activeBytes, eventBytes]));
1156
+ }
1157
+ const tasks = replay(member.plan, [...member.journal.events, event], {
1158
+ now: options.now ? new Date(options.now) : new Date(),
1159
+ verifyEvidence: evidenceVerifier(store.manifest, repoRoot, false),
1160
+ verifyImportSource: importSourceVerifier(repoRoot, false),
1161
+ });
1162
+ const snapshot = snapshotFor(member.plan, [...member.journal.events, event], tasks);
1163
+ await atomicWrite(path.resolve(repoRoot, member.descriptor.snapshot_ref), canonicalLine(snapshot));
1164
+ member.descriptor.journal_head_digest = event.event_digest;
1165
+ store.manifest.manifest_digest = todoSelfDigest(store.manifest, 'manifest_digest');
1166
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(store.manifest));
1167
+ return { event, snapshot };
1168
+ });
1169
+ }
1170
+
1171
+ export function buildTodoPlan(input) {
1172
+ const plan = { ...input, topology_digest: '', plan_digest: '' };
1173
+ plan.topology_digest = digestTodoArtifact({
1174
+ project_id: plan.project_id, plan_key: plan.plan_key, plan_version: plan.plan_version,
1175
+ tasks: plan.tasks, hard_dependencies: plan.hard_dependencies, joins: plan.joins,
1176
+ ...(['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema) ? { phases: plan.phases } : {}),
1177
+ ...(plan.schema === 'lattice.todo_plan.v5'
1178
+ ? { phase_accept_dependencies: plan.phase_accept_dependencies } : {}),
1179
+ });
1180
+ plan.plan_digest = todoSelfDigest(plan, 'plan_digest');
1181
+ if (!validateTodoPlan(plan)) throw new TypeError('todo plan input violates its declared schema');
1182
+ return plan;
1183
+ }
1184
+
1185
+ export function buildPlanGenesis(plan, input) {
1186
+ const event = {
1187
+ schema: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema)
1188
+ ? 'lattice.todo_event.v3' : 'lattice.todo_event.v1',
1189
+ project_id: plan.project_id, plan_key: plan.plan_key,
1190
+ plan_version: plan.plan_version, sequence: 0, previous_digest: input.previous_digest ?? null,
1191
+ kind: 'plan_genesis', task_id: null,
1192
+ ...(['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema) ? { phase_id: null } : {}),
1193
+ actor: input.actor, recorded_at: input.recorded_at,
1194
+ provenance: input.provenance ?? null,
1195
+ payload: { plan_digest: plan.plan_digest, topology_digest: plan.topology_digest,
1196
+ predecessor_plan_digest: plan.predecessor_plan_digest, task_migration: input.task_migration ?? [],
1197
+ ...(input.historical_import === true ? { historical_import: true } : {}) },
1198
+ event_digest: '',
1199
+ };
1200
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1201
+ if (!validateTodoEvent(event)) throw new TypeError('genesis input violates its declared schema');
1202
+ return event;
1203
+ }
1204
+
1205
+ export function buildRevisionGenesis(plan, input) {
1206
+ const event = {
1207
+ schema: 'lattice.todo_event.v2', project_id: plan.project_id, plan_key: plan.plan_key,
1208
+ plan_version: plan.plan_version, sequence: 0, previous_digest: input.previous_digest,
1209
+ kind: 'plan_genesis', task_id: null, actor: input.actor, recorded_at: input.recorded_at,
1210
+ provenance: input.provenance ?? null,
1211
+ payload: { plan_digest: plan.plan_digest, topology_digest: plan.topology_digest,
1212
+ predecessor_plan_digest: plan.predecessor_plan_digest,
1213
+ task_migration: input.state_migration.map(({ from_task_id, to_task_id }) => ({
1214
+ from_task_id, to_task_id,
1215
+ })) },
1216
+ reconciliation_state: 'reconciled', revision_digest: input.revision_digest,
1217
+ reconciliation_digest: input.reconciliation_digest,
1218
+ state_migration: input.state_migration, event_digest: '',
1219
+ };
1220
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1221
+ if (!validateTodoEvent(event)) throw new TypeError('revision genesis violates lattice.todo_event.v2');
1222
+ return event;
1223
+ }
1224
+
1225
+ export function buildPhaseRevisionGenesis(plan, input) {
1226
+ const event = {
1227
+ schema: 'lattice.todo_event.v4', project_id: plan.project_id, plan_key: plan.plan_key,
1228
+ plan_version: plan.plan_version, sequence: 0, previous_digest: input.previous_digest,
1229
+ kind: 'plan_genesis', task_id: null, phase_id: null, actor: input.actor,
1230
+ recorded_at: input.recorded_at, provenance: input.provenance ?? null,
1231
+ payload: { plan_digest: plan.plan_digest, topology_digest: plan.topology_digest,
1232
+ predecessor_plan_digest: plan.predecessor_plan_digest,
1233
+ task_migration: input.state_migration.map(({ from_task_id, to_task_id }) => ({
1234
+ from_task_id, to_task_id,
1235
+ })) },
1236
+ revision_digest: input.revision_digest, state_migration: input.state_migration,
1237
+ phase_state_migration: input.phase_state_migration, event_digest: '',
1238
+ };
1239
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1240
+ if (!validateTodoEvent(event)) throw new TypeError('phase revision genesis violates lattice.todo_event.v4');
1241
+ return event;
1242
+ }
1243
+
1244
+ function buildHistoricalDone(plan, previous, input, genesis) {
1245
+ const event = {
1246
+ schema: 'lattice.todo_event.v1', project_id: plan.project_id, plan_key: plan.plan_key,
1247
+ plan_version: plan.plan_version, sequence: previous.sequence + 1, previous_digest: previous.event_digest,
1248
+ kind: 'done', task_id: input.task_id, actor: input.actor ?? genesis.actor,
1249
+ recorded_at: input.recorded_at ?? genesis.recorded_at, provenance: input.provenance ?? null,
1250
+ payload: { done_mode: 'historical_import', imported: true, status: 'done',
1251
+ completed_at: input.completed_at, evidence: input.evidence }, event_digest: '',
1252
+ };
1253
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1254
+ if (!validateTodoEvent(event)) throw new TypeError('historical done input violates lattice.todo_event.v1');
1255
+ return event;
1256
+ }
1257
+
1258
+ function buildHistoricalStart(plan, previous, input, genesis) {
1259
+ const event = {
1260
+ schema: 'lattice.todo_event.v1', project_id: plan.project_id, plan_key: plan.plan_key,
1261
+ plan_version: plan.plan_version, sequence: previous.sequence + 1, previous_digest: previous.event_digest,
1262
+ kind: 'start', task_id: input.task_id, actor: input.actor ?? genesis.actor,
1263
+ recorded_at: input.recorded_at ?? genesis.recorded_at, provenance: input.provenance ?? null,
1264
+ payload: { start_mode: 'historical_import', imported: true, status: 'in-progress',
1265
+ started_at: input.started_at, evidence: input.evidence }, event_digest: '',
1266
+ };
1267
+ event.event_digest = todoSelfDigest(event, 'event_digest');
1268
+ if (!validateTodoEvent(event)) throw new TypeError('historical start input violates lattice.todo_event.v1');
1269
+ return event;
1270
+ }
1271
+
1272
+ function historicalImportInputs(value, timestampKey) {
1273
+ const required = ['task_id', timestampKey, 'evidence'];
1274
+ const allowed = new Set([...required, 'actor', 'recorded_at', 'provenance']);
1275
+ return Array.isArray(value) && value.length <= TODO_LIMITS.tasksPerPlan
1276
+ && value.every((entry) => entry !== null && typeof entry === 'object' && !Array.isArray(entry)
1277
+ && Object.getPrototypeOf(entry) === Object.prototype
1278
+ && required.every((key) => Object.hasOwn(entry, key))
1279
+ && Object.keys(entry).every((key) => allowed.has(key)));
1280
+ }
1281
+
1282
+ function prepareImportedArtifacts(repoRoot, options, projectId, memberPlans) {
1283
+ const planInput = materializeImportedNarrativeAnchors(
1284
+ repoRoot, options.plan, options.narrativeAnchorSources,
1285
+ );
1286
+ const plan = buildTodoPlan(planInput);
1287
+ if (plan.project_id !== projectId || plan.predecessor_plan_digest !== null) {
1288
+ throw new TypeError('imported plan must be a project-local genesis plan');
1289
+ }
1290
+ verifyPlanNarrativeAnchors(repoRoot, plan);
1291
+ const genesis = buildPlanGenesis(plan, { ...options.genesis, historical_import: true });
1292
+ const events = [genesis];
1293
+ const inProgressTasks = options.inProgressTasks ?? [];
1294
+ const completedTasks = options.completedTasks ?? [];
1295
+ if (!historicalImportInputs(inProgressTasks, 'started_at')
1296
+ || !historicalImportInputs(completedTasks, 'completed_at')) {
1297
+ fail('STORE_INCONSISTENT', 'historical_import_disposition_invalid');
1298
+ }
1299
+ const inProgressIds = new Set(inProgressTasks.map(({ task_id: taskId }) => taskId));
1300
+ const completedIds = new Set(completedTasks.map(({ task_id: taskId }) => taskId));
1301
+ if (inProgressIds.size !== inProgressTasks.length
1302
+ || completedIds.size !== completedTasks.length
1303
+ || [...inProgressIds].some((taskId) => completedIds.has(taskId))) {
1304
+ fail('STORE_INCONSISTENT', 'historical_import_disposition_conflict');
1305
+ }
1306
+ for (const input of inProgressTasks) events.push(buildHistoricalStart(plan, events.at(-1), input, genesis));
1307
+ for (const input of completedTasks) events.push(buildHistoricalDone(plan, events.at(-1), input, genesis));
1308
+ const verifyImportSource = importSourceVerifier(repoRoot, true);
1309
+ const tasks = replay(plan, events, {
1310
+ now: options.now ? new Date(options.now) : new Date(), verifyImportSource,
1311
+ });
1312
+ validateMergedGraph([...memberPlans, { plan }]);
1313
+ return { plan, genesis, events, tasks, snapshot: snapshotFor(plan, events, tasks) };
1314
+ }
1315
+
1316
+ async function bootstrapImportedPlan(repoRoot, options) {
1317
+ const initialization = options.initializeIfMissing;
1318
+ if (!exactRecord(initialization, ['projectId', 'repositories'])
1319
+ || initialization.projectId !== options.plan?.project_id) {
1320
+ throw new TypeError('historical import initialization input invalid');
1321
+ }
1322
+ const prepared = prepareImportedArtifacts(repoRoot, options, initialization.projectId, []);
1323
+ const { plan, genesis, events, snapshot } = prepared;
1324
+ const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
1325
+ const planRef = `${base}/plan.json`;
1326
+ const journalRef = `${base}/journal/active.jsonl`;
1327
+ const snapshotRef = `${base}/snapshot.json`;
1328
+ const descriptor = { plan_key: plan.plan_key, active_plan_version: plan.plan_version,
1329
+ plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
1330
+ topology_digest: plan.topology_digest, journal_head_digest: events.at(-1).event_digest };
1331
+ const manifest = { schema: 'lattice.todo_manifest.v1', project_id: initialization.projectId,
1332
+ repositories: initialization.repositories, members: [descriptor], manifest_digest: '' };
1333
+ manifest.manifest_digest = todoSelfDigest(manifest, 'manifest_digest');
1334
+ if (!validateTodoManifest(manifest)) throw new TypeError('manifest input violates lattice.todo_manifest.v1');
1335
+
1336
+ const stage = path.join(repoRoot, `.lattice-todo-bootstrap-${process.pid}-${randomBytes(6).toString('hex')}`);
1337
+ const latticeRoot = path.join(repoRoot, '.lattice');
1338
+ const storeRoot = path.join(repoRoot, STORE_ROOT_REF);
1339
+ let createdLatticeRoot = false;
1340
+ let activated = false;
1341
+ try {
1342
+ await mkdir(stage, { mode: 0o700 });
1343
+ const stagedBase = path.join(stage, 'plans', plan.plan_key, plan.plan_version);
1344
+ await atomicWrite(path.join(stagedBase, 'plan.json'), canonicalLine(plan));
1345
+ await atomicWrite(path.join(stagedBase, 'journal', 'active.jsonl'), Buffer.concat(events.map(canonicalLine)));
1346
+ await atomicWrite(path.join(stagedBase, 'snapshot.json'), canonicalLine(snapshot));
1347
+ await atomicWrite(path.join(stage, 'manifest.json'), canonicalLine(manifest));
1348
+ await protocolStage(options, 'bootstrap_staged');
1349
+
1350
+ try {
1351
+ const state = await lstat(latticeRoot);
1352
+ if (state.isSymbolicLink() || !state.isDirectory()) fail('STORE_INCONSISTENT', 'unsafe_lattice_root');
1353
+ } catch (error) {
1354
+ if (error instanceof TodoStoreError) throw error;
1355
+ if (error?.code !== 'ENOENT') throw error;
1356
+ await mkdir(latticeRoot, { mode: 0o700 });
1357
+ createdLatticeRoot = true;
1358
+ await fsyncDirectory(repoRoot);
1359
+ }
1360
+ await protocolStage(options, 'bootstrap_parent_prepared');
1361
+ try { await rename(stage, storeRoot); }
1362
+ catch (error) {
1363
+ if (['EEXIST', 'ENOTEMPTY'].includes(error?.code)) {
1364
+ fail('STORE_WRITE_CONFLICT', 'store_bootstrap_raced');
1365
+ }
1366
+ throw error;
1367
+ }
1368
+ activated = true;
1369
+ await fsyncDirectory(latticeRoot);
1370
+ await protocolStage(options, 'bootstrap_activated');
1371
+ return { plan, genesis, events, snapshot, descriptor };
1372
+ } finally {
1373
+ if (!activated) await rm(stage, { recursive: true, force: true });
1374
+ if (!activated && createdLatticeRoot) {
1375
+ try { await rmdir(latticeRoot); }
1376
+ catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error?.code)) throw error; }
1377
+ }
1378
+ }
1379
+ }
1380
+
1381
+ async function protocolStage(options, stage) {
1382
+ if (typeof options.onProtocolStage === 'function') await options.onProtocolStage(stage);
1383
+ }
1384
+
1385
+ /** G4-only atomic import, with optional all-or-nothing store bootstrap. */
1386
+ export async function appendImportedPlan(options = {}) {
1387
+ requireWriter(options.writer, 'g4-migration');
1388
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
1389
+ if (options.initializeIfMissing !== undefined) {
1390
+ try { await lstat(path.join(repoRoot, STORE_ROOT_REF)); }
1391
+ catch (error) {
1392
+ if (error?.code === 'ENOENT') return bootstrapImportedPlan(repoRoot, options);
1393
+ throw error;
1394
+ }
1395
+ }
1396
+ return withLock(repoRoot, async () => {
1397
+ // 1. Validate canonical manifest bytes and every current member before preparing output.
1398
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
1399
+ const expectedManifestDigest = store.manifest.manifest_digest;
1400
+ await protocolStage(options, 'manifest_validated');
1401
+
1402
+ // 2. A plan key is never overwritten, merged, or treated as an idempotent success.
1403
+ const existing = store.members.find(({ descriptor }) => descriptor.plan_key === options.plan?.plan_key);
1404
+ if (existing) {
1405
+ const imported = existing.journal.events[0]?.payload.historical_import === true;
1406
+ fail('STORE_WRITE_CONFLICT', imported ? 'plan_key_already_imported' : 'plan_key_already_exists');
1407
+ }
1408
+ await protocolStage(options, 'plan_key_absent');
1409
+
1410
+ const { plan, genesis, events, snapshot } = prepareImportedArtifacts(
1411
+ repoRoot, options, store.project_id, store.members.map(({ plan: memberPlan }) => ({ plan: memberPlan })),
1412
+ );
1413
+
1414
+ const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
1415
+ const planRef = `${base}/plan.json`;
1416
+ const journalRef = `${base}/journal/active.jsonl`;
1417
+ const snapshotRef = `${base}/snapshot.json`;
1418
+ const transactionRef = `${STORE_ROOT_REF}/transactions/${plan.plan_key}-${plan.plan_version}`;
1419
+ const transactionAbsolute = path.resolve(repoRoot, transactionRef);
1420
+ const finalBaseAbsolute = path.resolve(repoRoot, base);
1421
+ // These paths can only be leftovers from a transaction whose plan key is still absent.
1422
+ await rm(transactionAbsolute, { recursive: true, force: true });
1423
+ await rm(finalBaseAbsolute, { recursive: true, force: true });
1424
+
1425
+ // 3. All future member artifacts are durable while still outside manifest membership.
1426
+ const stagedPlan = path.join(transactionAbsolute, 'plan.json');
1427
+ const stagedJournal = path.join(transactionAbsolute, 'active.jsonl');
1428
+ const stagedSnapshot = path.join(transactionAbsolute, 'snapshot.json');
1429
+ await atomicWrite(stagedPlan, canonicalLine(plan));
1430
+ await atomicWrite(stagedJournal, Buffer.concat(events.map(canonicalLine)));
1431
+ await atomicWrite(stagedSnapshot, canonicalLine(snapshot));
1432
+ await protocolStage(options, 'staging_fsynced');
1433
+
1434
+ // 4. Re-read canonical manifest bytes under the lock and compare the captured digest.
1435
+ const currentManifest = await readArtifact(repoRoot, MANIFEST_REF, {
1436
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
1437
+ });
1438
+ if (currentManifest.manifest_digest !== expectedManifestDigest) {
1439
+ fail('STORE_WRITE_CONFLICT', 'manifest_digest_changed');
1440
+ }
1441
+ await protocolStage(options, 'manifest_cas_matched');
1442
+
1443
+ // 5. Pre-activation paths remain invisible until the final manifest rename.
1444
+ await mkdir(path.dirname(path.resolve(repoRoot, planRef)), { recursive: true });
1445
+ await mkdir(path.dirname(path.resolve(repoRoot, journalRef)), { recursive: true });
1446
+ await rename(stagedPlan, path.resolve(repoRoot, planRef));
1447
+ await rename(stagedJournal, path.resolve(repoRoot, journalRef));
1448
+ await rename(stagedSnapshot, path.resolve(repoRoot, snapshotRef));
1449
+ await fsyncDirectory(path.dirname(path.resolve(repoRoot, planRef)));
1450
+ await fsyncDirectory(path.dirname(path.resolve(repoRoot, journalRef)));
1451
+ await protocolStage(options, 'pre_activation_renamed');
1452
+
1453
+ const descriptor = { plan_key: plan.plan_key, active_plan_version: plan.plan_version,
1454
+ plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
1455
+ topology_digest: plan.topology_digest, journal_head_digest: events.at(-1).event_digest };
1456
+ currentManifest.members.push(descriptor);
1457
+ currentManifest.members.sort((left, right) => left.plan_key < right.plan_key ? -1 : left.plan_key > right.plan_key ? 1 : 0);
1458
+ currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
1459
+ if (!validateTodoManifest(currentManifest)) throw new TypeError('import activation manifest invalid');
1460
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
1461
+ await protocolStage(options, 'manifest_activated');
1462
+ await rm(transactionAbsolute, { recursive: true, force: true });
1463
+ return { plan, genesis, events, snapshot, descriptor };
1464
+ });
1465
+ }
1466
+
1467
+ export async function initializeTodoStore(options = {}) {
1468
+ requireWriter(options.writer, 'g4-migration');
1469
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
1470
+ if (!Array.isArray(options.plans) || options.plans.length === 0) throw new TypeError('plans required');
1471
+ return withLock(repoRoot, async () => {
1472
+ try { await lstat(path.resolve(repoRoot, MANIFEST_REF)); fail('STORE_WRITE_CONFLICT', 'store_already_exists'); }
1473
+ catch (error) { if (error instanceof TodoStoreError) throw error; if (error?.code !== 'ENOENT') throw error; }
1474
+ const members = [];
1475
+ for (const entry of options.plans) {
1476
+ const plan = buildTodoPlan(entry.plan);
1477
+ verifyPlanNarrativeAnchors(repoRoot, plan);
1478
+ const genesis = buildPlanGenesis(plan, entry.genesis);
1479
+ const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
1480
+ const planRef = `${base}/plan.json`; const journalRef = `${base}/journal/active.jsonl`; const snapshotRef = `${base}/snapshot.json`;
1481
+ await atomicWrite(path.resolve(repoRoot, planRef), canonicalLine(plan));
1482
+ await atomicWrite(path.resolve(repoRoot, journalRef), canonicalLine(genesis));
1483
+ const snapshot = snapshotFor(plan, [genesis], replay(plan, [genesis], { now: options.now ? new Date(options.now) : new Date() }));
1484
+ await atomicWrite(path.resolve(repoRoot, snapshotRef), canonicalLine(snapshot));
1485
+ members.push({ plan_key: plan.plan_key, active_plan_version: plan.plan_version, plan_ref: planRef,
1486
+ journal_ref: journalRef, snapshot_ref: snapshotRef, topology_digest: plan.topology_digest,
1487
+ journal_head_digest: genesis.event_digest });
1488
+ }
1489
+ members.sort((left, right) => left.plan_key < right.plan_key ? -1 : left.plan_key > right.plan_key ? 1 : 0);
1490
+ const manifest = { schema: 'lattice.todo_manifest.v1', project_id: options.projectId,
1491
+ repositories: options.repositories, members, manifest_digest: '' };
1492
+ manifest.manifest_digest = todoSelfDigest(manifest, 'manifest_digest');
1493
+ if (!validateTodoManifest(manifest)) throw new TypeError('manifest input violates lattice.todo_manifest.v1');
1494
+ // Activation is last: a crash before this point leaves the new store/version unpublished.
1495
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(manifest));
1496
+ return readTodoStore({ repoRoot, now: options.now });
1497
+ });
1498
+ }
1499
+
1500
+ /** G5-only initial authoring transaction. Store membership appears by one final directory rename. */
1501
+ export async function initializeAuthoredTodoStore(options = {}) {
1502
+ requireWriter(options.writer, 'g5-authoring');
1503
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
1504
+ const optionKeys = [
1505
+ 'repoRoot', 'writer', 'projectId', 'repositories', 'plan', 'genesis',
1506
+ ];
1507
+ if (!(exactRecord(options, optionKeys)
1508
+ || (exactRecord(options, [...optionKeys, 'onProtocolStage'])
1509
+ && typeof options.onProtocolStage === 'function'))
1510
+ || options.projectId !== options.plan?.project_id
1511
+ || options.plan?.predecessor_plan_digest !== null) {
1512
+ throw new TypeError('authored store initialization input invalid');
1513
+ }
1514
+ const plan = buildTodoPlan(options.plan);
1515
+ verifyPlanNarrativeAnchors(repoRoot, plan);
1516
+ const genesis = buildPlanGenesis(plan, options.genesis);
1517
+ const tasks = replay(plan, [genesis]);
1518
+ validateMergedGraph([{ plan }]);
1519
+ const snapshot = snapshotFor(plan, [genesis], tasks);
1520
+ const base = `plans/${plan.plan_key}/${plan.plan_version}`;
1521
+ const planRef = `${STORE_ROOT_REF}/${base}/plan.json`;
1522
+ const journalRef = `${STORE_ROOT_REF}/${base}/journal/active.jsonl`;
1523
+ const snapshotRef = `${STORE_ROOT_REF}/${base}/snapshot.json`;
1524
+ const descriptor = {
1525
+ plan_key: plan.plan_key, active_plan_version: plan.plan_version,
1526
+ plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
1527
+ topology_digest: plan.topology_digest, journal_head_digest: genesis.event_digest,
1528
+ };
1529
+ const manifest = {
1530
+ schema: 'lattice.todo_manifest.v1', project_id: options.projectId,
1531
+ repositories: options.repositories, members: [descriptor], manifest_digest: '',
1532
+ };
1533
+ manifest.manifest_digest = todoSelfDigest(manifest, 'manifest_digest');
1534
+ if (!validateTodoManifest(manifest)) throw new TypeError('manifest input violates lattice.todo_manifest.v1');
1535
+
1536
+ const stage = path.join(repoRoot, `.lattice-todo-authoring-${process.pid}-${randomBytes(6).toString('hex')}`);
1537
+ const latticeRoot = path.join(repoRoot, '.lattice');
1538
+ const storeRoot = path.join(repoRoot, STORE_ROOT_REF);
1539
+ let createdLatticeRoot = false;
1540
+ let renamed = false;
1541
+ let activated = false;
1542
+ let stageIdentity = null;
1543
+ let latticeIdentity = null;
1544
+ const sameIdentity = (left, right) => left !== null && right !== null
1545
+ && left.dev === right.dev && left.ino === right.ino;
1546
+ try {
1547
+ try { await lstat(storeRoot); fail('STORE_WRITE_CONFLICT', 'store_already_exists'); }
1548
+ catch (error) {
1549
+ if (error instanceof TodoStoreError) throw error;
1550
+ if (error?.code === 'ENOTDIR') fail('STORE_INCONSISTENT', 'unsafe_lattice_root');
1551
+ if (error?.code !== 'ENOENT') throw error;
1552
+ }
1553
+ await mkdir(stage, { mode: 0o700 });
1554
+ stageIdentity = await lstat(stage);
1555
+ await atomicWrite(path.join(stage, base, 'plan.json'), canonicalLine(plan));
1556
+ await atomicWrite(path.join(stage, base, 'journal', 'active.jsonl'), canonicalLine(genesis));
1557
+ await atomicWrite(path.join(stage, base, 'snapshot.json'), canonicalLine(snapshot));
1558
+ await atomicWrite(path.join(stage, 'manifest.json'), canonicalLine(manifest));
1559
+
1560
+ try {
1561
+ const state = await lstat(latticeRoot);
1562
+ if (state.isSymbolicLink() || !state.isDirectory()) fail('STORE_INCONSISTENT', 'unsafe_lattice_root');
1563
+ } catch (error) {
1564
+ if (error instanceof TodoStoreError) throw error;
1565
+ if (error?.code !== 'ENOENT') throw error;
1566
+ await mkdir(latticeRoot, { mode: 0o700 });
1567
+ createdLatticeRoot = true;
1568
+ await fsyncDirectory(repoRoot);
1569
+ }
1570
+ latticeIdentity = await lstat(latticeRoot);
1571
+ const stagedBeforeRename = await lstat(stage);
1572
+ const parentBeforeRename = await lstat(latticeRoot);
1573
+ if (!sameIdentity(stageIdentity, stagedBeforeRename)
1574
+ || !sameIdentity(latticeIdentity, parentBeforeRename)
1575
+ || stagedBeforeRename.isSymbolicLink() || !stagedBeforeRename.isDirectory()
1576
+ || parentBeforeRename.isSymbolicLink() || !parentBeforeRename.isDirectory()) {
1577
+ fail('STORE_WRITE_CONFLICT', 'authoring_activation_path_changed');
1578
+ }
1579
+ try { await rename(stage, storeRoot); }
1580
+ catch (error) {
1581
+ if (['EEXIST', 'ENOTEMPTY'].includes(error?.code)) fail('STORE_WRITE_CONFLICT', 'store_bootstrap_raced');
1582
+ throw error;
1583
+ }
1584
+ renamed = true;
1585
+ await fsyncDirectory(latticeRoot);
1586
+ if (typeof options.onProtocolStage === 'function') await options.onProtocolStage('authoring_renamed');
1587
+ const activatedStore = await lstat(storeRoot);
1588
+ const activatedParent = await lstat(latticeRoot);
1589
+ if (!sameIdentity(stageIdentity, activatedStore)
1590
+ || !sameIdentity(latticeIdentity, activatedParent)
1591
+ || activatedStore.isSymbolicLink() || !activatedStore.isDirectory()
1592
+ || activatedParent.isSymbolicLink() || !activatedParent.isDirectory()) {
1593
+ fail('STORE_WRITE_CONFLICT', 'authoring_activation_path_changed');
1594
+ }
1595
+ const store = await readTodoStore({ repoRoot });
1596
+ const member = store.members[0];
1597
+ if (store.project_id !== options.projectId || store.members.length !== 1
1598
+ || store.snapshot_stale !== false || member?.journal?.events?.length !== 1
1599
+ || canonicalizeTodoArtifact(store.manifest) !== canonicalizeTodoArtifact(manifest)
1600
+ || canonicalizeTodoArtifact(member?.descriptor) !== canonicalizeTodoArtifact(descriptor)
1601
+ || canonicalizeTodoArtifact(member?.plan) !== canonicalizeTodoArtifact(plan)
1602
+ || canonicalizeTodoArtifact(member?.journal?.events?.[0]) !== canonicalizeTodoArtifact(genesis)
1603
+ || canonicalizeTodoArtifact(member?.snapshot) !== canonicalizeTodoArtifact(snapshot)) {
1604
+ fail('STORE_WRITE_CONFLICT', 'authoring_activation_verification_failed');
1605
+ }
1606
+ activated = true;
1607
+ return store;
1608
+ } finally {
1609
+ if (renamed && !activated) {
1610
+ try {
1611
+ const currentParent = await lstat(latticeRoot);
1612
+ const currentStore = await lstat(storeRoot);
1613
+ if (sameIdentity(latticeIdentity, currentParent) && sameIdentity(stageIdentity, currentStore)
1614
+ && !currentParent.isSymbolicLink() && currentParent.isDirectory()
1615
+ && !currentStore.isSymbolicLink() && currentStore.isDirectory()) {
1616
+ await rm(storeRoot, { recursive: true, force: true });
1617
+ await fsyncDirectory(latticeRoot);
1618
+ }
1619
+ } catch (error) {
1620
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
1621
+ }
1622
+ }
1623
+ if (!activated) await rm(stage, { recursive: true, force: true });
1624
+ if (!activated && createdLatticeRoot) {
1625
+ try { await rmdir(latticeRoot); }
1626
+ catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error?.code)) throw error; }
1627
+ }
1628
+ }
1629
+ }
1630
+
1631
+ async function sourceItemBytes(repoRoot, sourceRef) {
1632
+ const parsed = parseTodoSourceRef(sourceRef);
1633
+ if (parsed === null) fail('RECONCILIATION_INCOMPLETE', 'source_ref_invalid', { source_ref: sourceRef });
1634
+ const canonicalRoot = await realpath(repoRoot);
1635
+ const absolute = path.resolve(canonicalRoot, parsed.path);
1636
+ if (!absolute.startsWith(`${canonicalRoot}${path.sep}`)) {
1637
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_outside_repo', { source_ref: sourceRef });
1638
+ }
1639
+ let current = canonicalRoot;
1640
+ for (const part of path.relative(canonicalRoot, absolute).split(path.sep)) {
1641
+ current = path.join(current, part);
1642
+ let state;
1643
+ try { state = await lstat(current); }
1644
+ catch { fail('RECONCILIATION_INCOMPLETE', 'source_path_missing', { source_ref: sourceRef }); }
1645
+ if (state.isSymbolicLink()) {
1646
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_symlink', { source_ref: sourceRef });
1647
+ }
1648
+ }
1649
+ const state = await lstat(absolute);
1650
+ if (!state.isFile() || await realpath(absolute) !== absolute) {
1651
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_not_regular', { source_ref: sourceRef });
1652
+ }
1653
+ const bytes = await readFile(absolute);
1654
+ decodeUtf8(bytes, 'RECONCILIATION_INCOMPLETE', 'source_invalid_utf8');
1655
+ const lines = [];
1656
+ let start = 0;
1657
+ for (let index = 0; index <= bytes.length; index += 1) {
1658
+ if (index === bytes.length || bytes[index] === 0x0a) {
1659
+ lines.push(bytes.subarray(start, index));
1660
+ start = index + 1;
1661
+ }
1662
+ }
1663
+ const line = lines[parsed.line - 1];
1664
+ if (line === undefined) fail('RECONCILIATION_INCOMPLETE', 'source_line_missing', { source_ref: sourceRef });
1665
+ return line;
1666
+ }
1667
+
1668
+ async function verifyRevisionSources(repoRoot, inventory) {
1669
+ for (const entry of [...inventory.active, ...inventory.excluded_tombstones]) {
1670
+ const line = await sourceItemBytes(repoRoot, entry.source_ref);
1671
+ if (sha256Bytes(line) !== entry.source_digest) {
1672
+ fail('RECONCILIATION_INCOMPLETE', 'source_digest_mismatch', { source_ref: entry.source_ref });
1673
+ }
1674
+ if (markdownCheckboxState(line) === null) {
1675
+ fail('RECONCILIATION_INCOMPLETE', 'source_item_not_todo', { source_ref: entry.source_ref });
1676
+ }
1677
+ }
1678
+ }
1679
+
1680
+ export async function verifyTodoRevisionSources(options = {}) {
1681
+ const revision = options.revision;
1682
+ if (!validateTodoRevision(revision)) fail('REVISION_INVALID', 'revision_schema_or_digest_invalid');
1683
+ await verifyRevisionSources(path.resolve(options.repoRoot ?? process.cwd()), revision.source_inventory);
1684
+ return true;
1685
+ }
1686
+
1687
+ export async function verifyPhaseTodoRevisionSources(options = {}) {
1688
+ const revision = options.revision;
1689
+ if (!validatePhaseTodoRevision(revision)) {
1690
+ fail('REVISION_INVALID', 'phase_revision_schema_or_digest_invalid');
1691
+ }
1692
+ if (revision.schema !== 'lattice.phase_todo_revision.v3') {
1693
+ fail('REVISION_INVALID', 'phase_revision_source_inventory_unsupported');
1694
+ }
1695
+ await verifyRevisionSources(path.resolve(options.repoRoot ?? process.cwd()), revision.source_inventory);
1696
+ return true;
1697
+ }
1698
+
1699
+ function mappedNodeRef(ref, plan, idMap) {
1700
+ if (ref.project_id !== plan.project_id || ref.plan_key !== plan.plan_key) return ref;
1701
+ return { ...ref, task_id: idMap.get(ref.task_id) ?? ref.task_id };
1702
+ }
1703
+
1704
+ function localTaskRef(ref, plan, taskId) {
1705
+ return ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
1706
+ && ref.task_id === taskId;
1707
+ }
1708
+
1709
+ function taskSemantics(plan, taskId, idMap, { reconciliationMetadata = false } = {}) {
1710
+ const task = plan.tasks.find(({ task_id }) => task_id === taskId);
1711
+ if (!task) return null;
1712
+ const mapId = (id) => id === null ? null : idMap.get(id) ?? id;
1713
+ const normalizedTask = reconciliationMetadata ? {
1714
+ task_id: mapId(task.task_id), title: task.title, lane: task.lane,
1715
+ compile_binding: task.compile_binding,
1716
+ } : {
1717
+ task_id: mapId(task.task_id), title: task.title, lane: task.lane,
1718
+ narrative_ref: task.narrative_ref, narrative_anchor: task.narrative_anchor ?? null,
1719
+ compile_binding: task.compile_binding, parent_task_id: mapId(task.parent_task_id ?? null),
1720
+ };
1721
+ const edges = plan.hard_dependencies
1722
+ .filter(({ from, to }) => localTaskRef(from, plan, taskId) || localTaskRef(to, plan, taskId))
1723
+ .map(({ from, to }) => ({ from: mappedNodeRef(from, plan, idMap), to: mappedNodeRef(to, plan, idMap) }))
1724
+ .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1);
1725
+ const joins = plan.joins
1726
+ .filter(({ after, before }) => localTaskRef(before, plan, taskId)
1727
+ || after.some((ref) => localTaskRef(ref, plan, taskId)))
1728
+ .map((join) => ({ ...join,
1729
+ after: join.after.map((ref) => mappedNodeRef(ref, plan, idMap))
1730
+ .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1),
1731
+ before: mappedNodeRef(join.before, plan, idMap),
1732
+ })).sort((left, right) => left.id < right.id ? -1 : 1);
1733
+ const phaseAcceptDependencies = plan.schema === 'lattice.todo_plan.v5'
1734
+ ? plan.phase_accept_dependencies
1735
+ .filter(({ to }) => localTaskRef(to, plan, taskId))
1736
+ .map(({ from, to }) => ({ from, to: mappedNodeRef(to, plan, idMap) }))
1737
+ .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1)
1738
+ : [];
1739
+ return { task: normalizedTask, hard_dependencies: edges, joins, phase_accept_dependencies: phaseAcceptDependencies };
1740
+ }
1741
+
1742
+ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
1743
+ { reconciliationMetadata = false } = {}) {
1744
+ const task = plan.tasks.find(({ task_id: id }) => id === taskId);
1745
+ if (task === undefined) return null;
1746
+ const mapTaskRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
1747
+ ? { ...ref, task_id: taskIdMap.get(ref.task_id) ?? ref.task_id } : ref;
1748
+ const mapPhaseRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
1749
+ ? { ...ref, phase_id: phaseIdMap.get(ref.phase_id) ?? ref.phase_id } : ref;
1750
+ const mappedTask = reconciliationMetadata ? {
1751
+ task_id: taskIdMap.get(task.task_id) ?? task.task_id, title: task.title, lane: task.lane,
1752
+ compile_binding: task.compile_binding,
1753
+ phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1754
+ } : { ...task,
1755
+ task_id: taskIdMap.get(task.task_id) ?? task.task_id,
1756
+ phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1757
+ parent_task_id: task.parent_task_id === null ? null
1758
+ : taskIdMap.get(task.parent_task_id) ?? task.parent_task_id,
1759
+ };
1760
+ const incoming = [];
1761
+ const outgoing = [];
1762
+ for (const edge of plan.hard_dependencies) {
1763
+ const mapped = { kind: 'hard', from: mapTaskRef(edge.from), to: mapTaskRef(edge.to) };
1764
+ if (localTaskRef(edge.to, plan, taskId)) incoming.push(mapped);
1765
+ if (localTaskRef(edge.from, plan, taskId)) outgoing.push(mapped);
1766
+ }
1767
+ for (const join of plan.joins) {
1768
+ const to = mapTaskRef(join.before);
1769
+ for (const after of join.after) {
1770
+ const tuple = { kind: 'join', join_id: join.id, from: mapTaskRef(after), to };
1771
+ if (localTaskRef(join.before, plan, taskId)) incoming.push(tuple);
1772
+ if (localTaskRef(after, plan, taskId)) outgoing.push(tuple);
1773
+ }
1774
+ }
1775
+ if (plan.schema === 'lattice.todo_plan.v5') for (const edge of plan.phase_accept_dependencies) {
1776
+ if (localTaskRef(edge.to, plan, taskId)) incoming.push({ kind: 'phase_accept',
1777
+ from: mapPhaseRef(edge.from), to: mapTaskRef(edge.to) });
1778
+ }
1779
+ const sort = (entries) => entries.map(canonicalizeTodoArtifact).sort();
1780
+ return { task: mappedTask, incoming: sort(incoming), outgoing: sort(outgoing) };
1781
+ }
1782
+
1783
+ function validatePhaseV3Carry(previous, revision, migration, idMap, state) {
1784
+ const reconciliationMetadata = migration.state_policy === 'carry_reconciled_metadata';
1785
+ const phaseIdMap = new Map(revision.phase_migration
1786
+ .filter(({ from_phase_id, to_phase_id }) => from_phase_id !== null && to_phase_id !== 'removed')
1787
+ .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
1788
+ const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap,
1789
+ { reconciliationMetadata });
1790
+ const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id,
1791
+ new Map(), new Map(),
1792
+ { reconciliationMetadata });
1793
+ if (canonicalizeTodoArtifact(before.task) !== canonicalizeTodoArtifact(after.task)
1794
+ || canonicalizeTodoArtifact(before.incoming) !== canonicalizeTodoArtifact(after.incoming)) {
1795
+ fail('REVISION_INVALID', 'carry_semantics_changed', { from_task_id: migration.from_task_id });
1796
+ }
1797
+ const successorOutgoing = new Set(after.outgoing);
1798
+ if (!before.outgoing.every((edge) => successorOutgoing.has(edge))) {
1799
+ fail('REVISION_INVALID', 'carry_outgoing_edge_removed', { from_task_id: migration.from_task_id });
1800
+ }
1801
+ }
1802
+
1803
+ function stateMigrationFor(previous, revision) {
1804
+ const oldIds = previous.plan.tasks.map(({ task_id }) => task_id);
1805
+ const migrationIds = revision.task_migration.map(({ from_task_id }) => from_task_id);
1806
+ if (canonicalizeTodoArtifact([...oldIds].sort()) !== canonicalizeTodoArtifact([...migrationIds].sort())) {
1807
+ fail('REVISION_INVALID', 'predecessor_task_migration_incomplete');
1808
+ }
1809
+ const idMap = new Map(revision.task_migration
1810
+ .filter(({ to_task_id }) => to_task_id !== 'removed')
1811
+ .map(({ from_task_id, to_task_id }) => [from_task_id, to_task_id]));
1812
+ const states = new Map(previous.tasks.map((state) => [state.task_id, state]));
1813
+ return revision.task_migration.map((migration) => {
1814
+ const carriesState = ['carry', 'carry_reconciled_metadata'].includes(migration.state_policy);
1815
+ if (!carriesState) return { ...migration, state: null };
1816
+ const reconciliationMetadata = migration.state_policy === 'carry_reconciled_metadata';
1817
+ const state = states.get(migration.from_task_id);
1818
+ if (!state) fail('STORE_INCONSISTENT', 'predecessor_task_state_missing');
1819
+ if (revision.schema === 'lattice.phase_todo_revision.v3') {
1820
+ validatePhaseV3Carry(previous, revision, migration, idMap, state);
1821
+ } else {
1822
+ const before = taskSemantics(previous.plan, migration.from_task_id, idMap,
1823
+ { reconciliationMetadata });
1824
+ const after = taskSemantics(revision.desired_plan, migration.to_task_id, new Map(),
1825
+ { reconciliationMetadata });
1826
+ if (canonicalizeTodoArtifact(before) !== canonicalizeTodoArtifact(after)) {
1827
+ fail('REVISION_INVALID', 'carry_semantics_changed', { from_task_id: migration.from_task_id });
1828
+ }
1829
+ }
1830
+ return { ...migration, state: {
1831
+ status: state.status, started_at: state.started_at, done_at: state.done_at,
1832
+ blocked_reason: state.blocked_reason, evidence: state.evidence, imported: state.imported,
1833
+ } };
1834
+ });
1835
+ }
1836
+
1837
+ function phaseStateMigrationFor(previous, revision) {
1838
+ const predecessorPhases = ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(previous.plan.schema)
1839
+ ? previous.plan.phases : [];
1840
+ const sourceIds = revision.phase_migration
1841
+ .filter(({ from_phase_id }) => from_phase_id !== null).map(({ from_phase_id }) => from_phase_id);
1842
+ if (canonicalizeTodoArtifact([...sourceIds].sort())
1843
+ !== canonicalizeTodoArtifact(predecessorPhases.map(({ phase_id }) => phase_id).sort())) {
1844
+ fail('REVISION_INVALID', 'predecessor_phase_migration_incomplete');
1845
+ }
1846
+ const idMap = new Map(revision.phase_migration
1847
+ .filter(({ from_phase_id, to_phase_id }) => from_phase_id !== null && to_phase_id !== 'removed')
1848
+ .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
1849
+ const taskIdMap = new Map(revision.task_migration
1850
+ .filter(({ to_task_id }) => to_task_id !== 'removed')
1851
+ .map(({ from_task_id, to_task_id }) => [from_task_id, to_task_id]));
1852
+ const previousStates = new Map((previous.snapshot.phases ?? []).map((state) => [state.phase_id, state]));
1853
+ return revision.phase_migration
1854
+ .filter(({ to_phase_id }) => to_phase_id !== 'removed')
1855
+ .map((migration) => {
1856
+ if (migration.state_policy === 'reset') {
1857
+ return { phase_id: migration.to_phase_id, state_policy: 'reset', state: null };
1858
+ }
1859
+ const before = predecessorPhases.find(({ phase_id }) => phase_id === migration.from_phase_id);
1860
+ const after = revision.desired_plan.phases.find(({ phase_id }) => phase_id === migration.to_phase_id);
1861
+ const mappedBefore = { ...before, phase_id: migration.to_phase_id,
1862
+ predecessor_phase_ids: before.predecessor_phase_ids.map((id) => idMap.get(id) ?? id).sort() };
1863
+ const beforeTaskIds = previous.plan.tasks
1864
+ .filter(({ phase_id }) => phase_id === migration.from_phase_id)
1865
+ .map(({ task_id }) => taskIdMap.get(task_id) ?? 'removed').filter((id) => id !== 'removed').sort();
1866
+ const afterTaskIds = revision.desired_plan.tasks
1867
+ .filter(({ phase_id }) => phase_id === migration.to_phase_id).map(({ task_id }) => task_id).sort();
1868
+ if (canonicalizeTodoArtifact({ phase: mappedBefore, task_ids: beforeTaskIds })
1869
+ !== canonicalizeTodoArtifact({ phase: after, task_ids: afterTaskIds })) {
1870
+ fail('REVISION_INVALID', 'phase_carry_semantics_changed', { from_phase_id: migration.from_phase_id });
1871
+ }
1872
+ const state = previousStates.get(migration.from_phase_id);
1873
+ if (state === undefined) fail('STORE_INCONSISTENT', 'predecessor_phase_state_missing');
1874
+ return { phase_id: migration.to_phase_id, state_policy: 'carry', state: {
1875
+ status: state.status, review_event_digest: state.review_event_digest,
1876
+ decision_event_digest: state.decision_event_digest, decision_evidence: state.decision_evidence,
1877
+ } };
1878
+ }).sort((left, right) => left.phase_id.localeCompare(right.phase_id));
1879
+ }
1880
+
1881
+ function phaseRevisionResult(revision, genesis, recovered = false) {
1882
+ const result = {
1883
+ schema: 'lattice.phase_todo_revise_result.v1', project_id: revision.project_id,
1884
+ plan_key: revision.plan_key, predecessor_plan_digest: revision.predecessor.plan_digest,
1885
+ predecessor_journal_head_digest: revision.predecessor.journal_head_digest,
1886
+ plan_version: revision.desired_plan.plan_version, plan_digest: revision.desired_plan.plan_digest,
1887
+ topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest,
1888
+ revision_digest: revision.revision_digest, recovered, result_digest: '',
1889
+ };
1890
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1891
+ return result;
1892
+ }
1893
+
1894
+ function sourceCutoverCleanupBinding(revision, stagingRef) {
1895
+ return { schema: 'lattice.source_cutover_cleanup_binding.v1',
1896
+ revision_digest: revision.revision_digest, staging_ref: stagingRef,
1897
+ cleanup_state: 'cleanup_complete' };
1898
+ }
1899
+
1900
+ function sourceCutoverArchiveRootList(revision, entries) {
1901
+ return { schema: 'lattice.source_cutover_archive_root_list.v1', roots: [{
1902
+ archive_ref: revision.source_cutover_batch.archive_ref,
1903
+ entry_digests: entries.map(({ entry_digest: digest }) => digest),
1904
+ }] };
1905
+ }
1906
+
1907
+ async function buildPhaseV3SourceReceipt(repoRoot, revision, transactionRef) {
1908
+ const stagingRef = `${transactionRef}/source-originals.md`;
1909
+ const entries = [];
1910
+ for (const [operationIndex, operation] of revision.source_cutover_batch.operations.entries()) {
1911
+ const publishedBytes = await sourceItemBytes(repoRoot, operation.source_ref);
1912
+ const archiveRef = todoCutoverArchiveSourceRef(revision.source_cutover_batch, operationIndex);
1913
+ const archivedBytes = await sourceItemBytes(repoRoot, archiveRef);
1914
+ const expectedPublishedBytes = phaseV3PublishedSourceBytes(operation, archivedBytes);
1915
+ const entry = { operation_index: operationIndex, task_id: operation.task_id,
1916
+ disposition: operation.disposition, source_ref: operation.source_ref,
1917
+ staging_ref: `${stagingRef}#L${operationIndex + 1}`, published_ref: operation.source_ref,
1918
+ archive_ref: archiveRef, replacement: operation.live_replacement,
1919
+ staged_source_bytes_digest: operation.source_digest,
1920
+ published_source_bytes_digest: sha256Bytes(publishedBytes),
1921
+ archived_source_bytes_digest: sha256Bytes(archivedBytes), entry_digest: '' };
1922
+ if (entry.published_source_bytes_digest !== sha256Bytes(expectedPublishedBytes)
1923
+ || entry.archived_source_bytes_digest !== operation.source_digest) {
1924
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_receipt_bytes_mismatch');
1925
+ }
1926
+ entry.entry_digest = todoSelfDigest(entry, 'entry_digest');
1927
+ entries.push(entry);
1928
+ }
1929
+ const cleanupBinding = sourceCutoverCleanupBinding(revision, stagingRef);
1930
+ const receipt = { schema: 'lattice.source_cutover_receipt.v1', project_id: revision.project_id,
1931
+ plan_key: revision.plan_key, plan_version: revision.desired_plan.plan_version,
1932
+ revision_digest: revision.revision_digest,
1933
+ source_cutover_batch_digest: revision.source_cutover_batch.batch_digest, entries,
1934
+ archive_root_list_digest: digestTodoArtifact(sourceCutoverArchiveRootList(revision, entries)),
1935
+ published_state: 'source_and_archive_published',
1936
+ cleanup_binding_digest: digestTodoArtifact(cleanupBinding), receipt_digest: '' };
1937
+ receipt.receipt_digest = todoSelfDigest(receipt, 'receipt_digest');
1938
+ return receipt;
1939
+ }
1940
+
1941
+ function phaseV3PublishedSourceBytes(operation, archivedBytes) {
1942
+ return Buffer.from(`${operation.live_replacement}${archivedBytes.at(-1) === 0x0d ? '\r' : ''}`,
1943
+ 'utf8');
1944
+ }
1945
+
1946
+ function validSourceCutoverReceipt(value, revision) {
1947
+ if (!exactRecord(value, ['schema', 'project_id', 'plan_key', 'plan_version', 'revision_digest',
1948
+ 'source_cutover_batch_digest', 'entries', 'archive_root_list_digest', 'published_state',
1949
+ 'cleanup_binding_digest', 'receipt_digest'])
1950
+ || value.schema !== 'lattice.source_cutover_receipt.v1'
1951
+ || value.project_id !== revision.project_id || value.plan_key !== revision.plan_key
1952
+ || value.plan_version !== revision.desired_plan.plan_version
1953
+ || value.revision_digest !== revision.revision_digest
1954
+ || value.source_cutover_batch_digest !== revision.source_cutover_batch.batch_digest
1955
+ || value.published_state !== 'source_and_archive_published'
1956
+ || !Array.isArray(value.entries)
1957
+ || value.entries.length !== revision.source_cutover_batch.operations.length
1958
+ || value.receipt_digest !== todoSelfDigest(value, 'receipt_digest')) return false;
1959
+ for (const [index, entry] of value.entries.entries()) {
1960
+ const operation = revision.source_cutover_batch.operations[index];
1961
+ if (!exactRecord(entry, ['operation_index', 'task_id', 'disposition', 'source_ref',
1962
+ 'staging_ref', 'published_ref', 'archive_ref', 'replacement',
1963
+ 'staged_source_bytes_digest', 'published_source_bytes_digest',
1964
+ 'archived_source_bytes_digest', 'entry_digest'])
1965
+ || entry.operation_index !== index || entry.task_id !== operation.task_id
1966
+ || entry.disposition !== operation.disposition || entry.source_ref !== operation.source_ref
1967
+ || entry.published_ref !== operation.source_ref
1968
+ || entry.archive_ref !== todoCutoverArchiveSourceRef(revision.source_cutover_batch, index)
1969
+ || entry.replacement !== operation.live_replacement
1970
+ || entry.staged_source_bytes_digest !== operation.source_digest
1971
+ || !isTodoDigest(entry.published_source_bytes_digest)
1972
+ || entry.archived_source_bytes_digest !== operation.source_digest
1973
+ || entry.entry_digest !== todoSelfDigest(entry, 'entry_digest')) return false;
1974
+ }
1975
+ const expectedRootDigest = digestTodoArtifact(sourceCutoverArchiveRootList(revision, value.entries));
1976
+ const stagingRef = value.entries[0].staging_ref.replace(/#L[1-9]\d*$/u, '');
1977
+ const expectedStagingRef = `${STORE_ROOT_REF}/transactions/phase-v3/${revision.plan_key}/${revision.desired_plan.plan_version}/source-originals.md`;
1978
+ return stagingRef === expectedStagingRef
1979
+ && value.entries.every((entry, index) => entry.staging_ref === `${stagingRef}#L${index + 1}`)
1980
+ && value.archive_root_list_digest === expectedRootDigest
1981
+ && value.cleanup_binding_digest
1982
+ === digestTodoArtifact(sourceCutoverCleanupBinding(revision, stagingRef));
1983
+ }
1984
+
1985
+ async function verifyPhaseV3SourceReceipt(repoRoot, revision, receipt) {
1986
+ if (!validSourceCutoverReceipt(receipt, revision)) return false;
1987
+ const stagingRef = receipt.entries[0].staging_ref.replace(/#L[1-9]\d*$/u, '');
1988
+ if (await exactFileOrNull(path.resolve(repoRoot, stagingRef)) !== null) return false;
1989
+ for (const [index, entry] of receipt.entries.entries()) {
1990
+ const source = parseTodoSourceRef(entry.published_ref);
1991
+ const archive = parseTodoSourceRef(entry.archive_ref);
1992
+ if (source === null || archive === null) return false;
1993
+ await safeRepoFile(repoRoot, source.path);
1994
+ await safeRepoFile(repoRoot, archive.path);
1995
+ const publishedBytes = await sourceItemBytes(repoRoot, entry.published_ref);
1996
+ const archivedBytes = await sourceItemBytes(repoRoot, entry.archive_ref);
1997
+ if (sha256Bytes(publishedBytes) !== entry.published_source_bytes_digest
1998
+ || sha256Bytes(archivedBytes) !== entry.archived_source_bytes_digest
1999
+ || entry.published_source_bytes_digest !== sha256Bytes(phaseV3PublishedSourceBytes(
2000
+ revision.source_cutover_batch.operations[index], archivedBytes))) return false;
2001
+ }
2002
+ return true;
2003
+ }
2004
+
2005
+ function validPhaseRevisionCommitReceipt(value, revision, descriptor, genesis, sourceReceipt) {
2006
+ return exactRecord(value, ['schema', 'project_id', 'plan_key', 'plan_version', 'revision_digest',
2007
+ 'committed_member_digest', 'active_plan_digest', 'journal_genesis_digest',
2008
+ 'reconciliation_digest', 'source_cutover_receipt_digest', 'committed_at', 'receipt_digest'])
2009
+ && value.schema === 'lattice.phase_revision_commit_receipt.v1'
2010
+ && value.project_id === revision.project_id && value.plan_key === revision.plan_key
2011
+ && value.plan_version === revision.desired_plan.plan_version
2012
+ && value.revision_digest === revision.revision_digest
2013
+ && value.committed_member_digest === digestTodoArtifact(descriptor)
2014
+ && value.active_plan_digest === revision.desired_plan.plan_digest
2015
+ && value.journal_genesis_digest === genesis.event_digest
2016
+ && value.reconciliation_digest === revision.reconciliation.reconciliation_digest
2017
+ && value.source_cutover_receipt_digest === sourceReceipt.receipt_digest
2018
+ && isStrictTodoTimestamp(value.committed_at)
2019
+ && value.receipt_digest === todoSelfDigest(value, 'receipt_digest');
2020
+ }
2021
+
2022
+ function revisionResult(revision, genesis, { recovered = false } = {}) {
2023
+ const result = {
2024
+ schema: revision.schema === 'lattice.todo_revision.v2'
2025
+ ? 'lattice.todo_revise_result.v2' : 'lattice.todo_revise_result.v1',
2026
+ project_id: revision.project_id,
2027
+ plan_key: revision.plan_key, predecessor_plan_digest: revision.predecessor.plan_digest,
2028
+ predecessor_journal_head_digest: revision.predecessor.journal_head_digest,
2029
+ plan_version: revision.desired_plan.plan_version, plan_digest: revision.desired_plan.plan_digest,
2030
+ topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest,
2031
+ revision_digest: revision.revision_digest,
2032
+ reconciliation_digest: revision.reconciliation.reconciliation_digest,
2033
+ ...(revision.schema === 'lattice.todo_revision.v2' ? { source_cutover: {
2034
+ batch_id: revision.source_cutover_batch.batch_id,
2035
+ operation_count: revision.source_cutover_batch.operations.length,
2036
+ archive_ref: revision.source_cutover_batch.archive_ref,
2037
+ recovered,
2038
+ } } : {}),
2039
+ result_digest: '',
2040
+ };
2041
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2042
+ return result;
2043
+ }
2044
+
2045
+ async function exactFileOrNull(absolute) {
2046
+ try {
2047
+ const state = await lstat(absolute);
2048
+ if (state.isSymbolicLink() || !state.isFile()) fail('REVISION_CONFLICT', 'revision_artifact_unsafe');
2049
+ return readFile(absolute);
2050
+ } catch (error) {
2051
+ if (error instanceof TodoStoreError) throw error;
2052
+ if (error?.code === 'ENOENT') return null;
2053
+ throw error;
2054
+ }
2055
+ }
2056
+
2057
+ async function ensureSafeStoreDirectory(repoRoot, target) {
2058
+ const requestedStoreRoot = path.resolve(repoRoot, STORE_ROOT_REF);
2059
+ const requested = path.resolve(target);
2060
+ if (requested !== requestedStoreRoot && !requested.startsWith(`${requestedStoreRoot}${path.sep}`)) {
2061
+ fail('REVISION_CONFLICT', 'revision_directory_outside_store');
2062
+ }
2063
+ const canonicalRepoRoot = await realpath(repoRoot);
2064
+ const storeRoot = path.resolve(canonicalRepoRoot, STORE_ROOT_REF);
2065
+ const absolute = path.resolve(storeRoot, path.relative(requestedStoreRoot, requested));
2066
+ let current = storeRoot;
2067
+ const parts = path.relative(storeRoot, absolute).split(path.sep).filter(Boolean);
2068
+ for (const part of ['', ...parts]) {
2069
+ if (part !== '') current = path.join(current, part);
2070
+ let state;
2071
+ try { state = await lstat(current); }
2072
+ catch (error) {
2073
+ if (error?.code !== 'ENOENT') throw error;
2074
+ await mkdir(current, { mode: 0o700 });
2075
+ await fsyncDirectory(path.dirname(current));
2076
+ state = await lstat(current);
2077
+ }
2078
+ if (state.isSymbolicLink() || !state.isDirectory() || await realpath(current) !== current) {
2079
+ fail('REVISION_CONFLICT', 'revision_directory_unsafe');
2080
+ }
2081
+ }
2082
+ }
2083
+
2084
+ function parseRevisionMarker(bytes) {
2085
+ return parseCanonicalJsonLine(bytes, {
2086
+ code: 'REVISION_CONFLICT', reason: 'revision_marker_invalid',
2087
+ maxBytes: TODO_LIMITS.snapshotBytes,
2088
+ validate: (value) => exactRecord(value, ['schema', 'revision', 'genesis'])
2089
+ && value.schema === 'lattice.todo_revision_transaction.v1'
2090
+ && validateTodoRevision(value.revision) && validateTodoEvent(value.genesis),
2091
+ });
2092
+ }
2093
+
2094
+ function parsePhaseRevisionMarker(bytes) {
2095
+ return parseCanonicalJsonLine(bytes, {
2096
+ code: 'REVISION_CONFLICT', reason: 'phase_revision_marker_invalid',
2097
+ maxBytes: TODO_LIMITS.snapshotBytes,
2098
+ validate: (value) => exactRecord(value, ['schema', 'revision', 'genesis'])
2099
+ && value.schema === 'lattice.phase_todo_revision_transaction.v1'
2100
+ && validatePhaseTodoRevision(value.revision) && validateTodoEvent(value.genesis),
2101
+ });
2102
+ }
2103
+
2104
+ function parseRevisionSetMarker(bytes) {
2105
+ return parseCanonicalJsonLine(bytes, {
2106
+ code: 'REVISION_CONFLICT', reason: 'revision_set_marker_invalid',
2107
+ maxBytes: TODO_LIMITS.snapshotBytes,
2108
+ validate: (value) => exactRecord(value, [
2109
+ 'schema', 'project_id', 'revision_set_digest', 'entries',
2110
+ ]) && ['lattice.todo_revision_set_transaction.v1',
2111
+ 'lattice.todo_revision_set_transaction.v2'].includes(value.schema)
2112
+ && isTodoIdentifier(value.project_id) && isTodoDigest(value.revision_set_digest)
2113
+ && Array.isArray(value.entries) && value.entries.length > 0
2114
+ && value.entries.every((entry) => {
2115
+ const phase = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2']
2116
+ .includes(entry?.revision?.schema);
2117
+ return exactRecord(entry, phase && value.schema.endsWith('.v2') ? [
2118
+ 'revision', 'state_migration', 'phase_state_migration', 'genesis',
2119
+ ] : ['revision', 'state_migration', 'genesis'])
2120
+ && (phase ? validatePhaseTodoRevision(entry.revision) : validateTodoRevision(entry.revision))
2121
+ && Array.isArray(entry.state_migration)
2122
+ && (!phase || Array.isArray(entry.phase_state_migration))
2123
+ && validateTodoEvent(entry.genesis);
2124
+ }),
2125
+ });
2126
+ }
2127
+
2128
+ async function rejectCompetingRevisionTransaction(repoRoot, revision) {
2129
+ const root = path.join(repoRoot, STORE_ROOT_REF, 'transactions', 'revisions', revision.plan_key);
2130
+ let names;
2131
+ try {
2132
+ const state = await lstat(root);
2133
+ if (state.isSymbolicLink() || !state.isDirectory()) {
2134
+ fail('REVISION_CONFLICT', 'revision_transaction_root_unsafe');
2135
+ }
2136
+ names = await readdir(root);
2137
+ } catch (error) {
2138
+ if (error instanceof TodoStoreError) throw error;
2139
+ if (error?.code === 'ENOENT') return;
2140
+ throw error;
2141
+ }
2142
+ for (const name of names) {
2143
+ if (!isTodoIdentifier(name)) fail('REVISION_CONFLICT', 'revision_transaction_entry_invalid');
2144
+ const markerBytes = await exactFileOrNull(path.join(root, name, 'marker.json'));
2145
+ if (markerBytes === null) fail('REVISION_CONFLICT', 'revision_marker_missing');
2146
+ const marker = parseRevisionMarker(markerBytes);
2147
+ if (canonicalizeTodoArtifact(marker.revision.predecessor)
2148
+ === canonicalizeTodoArtifact(revision.predecessor)
2149
+ && marker.revision.revision_digest !== revision.revision_digest) {
2150
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2151
+ }
2152
+ }
2153
+ }
2154
+
2155
+ async function rejectCompetingPhaseV3Transaction(repoRoot, revision) {
2156
+ const root = path.join(repoRoot, STORE_ROOT_REF, 'transactions', 'phase-v3', revision.plan_key);
2157
+ let names;
2158
+ try {
2159
+ const state = await lstat(root);
2160
+ const canonicalRepoRoot = await realpath(repoRoot);
2161
+ const canonicalRoot = path.resolve(canonicalRepoRoot, path.relative(repoRoot, root));
2162
+ if (state.isSymbolicLink() || !state.isDirectory() || await realpath(root) !== canonicalRoot) {
2163
+ fail('REVISION_CONFLICT', 'revision_transaction_root_unsafe');
2164
+ }
2165
+ names = await readdir(root);
2166
+ } catch (error) {
2167
+ if (error instanceof TodoStoreError) throw error;
2168
+ if (error?.code === 'ENOENT') return;
2169
+ throw error;
2170
+ }
2171
+ for (const name of names) {
2172
+ if (!isTodoIdentifier(name)) fail('REVISION_CONFLICT', 'revision_transaction_entry_invalid');
2173
+ const directory = path.join(root, name);
2174
+ const state = await lstat(directory);
2175
+ const canonicalDirectory = path.join(await realpath(root), name);
2176
+ if (state.isSymbolicLink() || !state.isDirectory()
2177
+ || await realpath(directory) !== canonicalDirectory) {
2178
+ fail('REVISION_CONFLICT', 'revision_transaction_entry_invalid');
2179
+ }
2180
+ const markerBytes = await exactFileOrNull(path.join(directory, 'marker.json'));
2181
+ if (markerBytes === null) {
2182
+ if (name !== revision.desired_plan.plan_version) {
2183
+ fail('REVISION_CONFLICT', 'revision_marker_missing');
2184
+ }
2185
+ for (const artifact of await readdir(directory, { withFileTypes: true })) {
2186
+ if (!artifact.isFile() || artifact.isSymbolicLink()
2187
+ || !/^\.marker\.json\.\d+\.[0-9a-f]{12}\.tmp$/u.test(artifact.name)) {
2188
+ fail('REVISION_CONFLICT', 'revision_marker_missing');
2189
+ }
2190
+ }
2191
+ continue;
2192
+ }
2193
+ const marker = parsePhaseRevisionMarker(markerBytes);
2194
+ if (canonicalizeTodoArtifact(marker.revision.predecessor)
2195
+ === canonicalizeTodoArtifact(revision.predecessor)
2196
+ && marker.revision.revision_digest !== revision.revision_digest) {
2197
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ async function publishRevisionArtifact(staged, finalAbsolute, expected) {
2203
+ const finalBytes = await exactFileOrNull(finalAbsolute);
2204
+ if (finalBytes !== null) {
2205
+ if (!finalBytes.equals(expected)) fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2206
+ return;
2207
+ }
2208
+ let stagedBytes = await exactFileOrNull(staged);
2209
+ if (stagedBytes === null) {
2210
+ await atomicWrite(staged, expected);
2211
+ stagedBytes = expected;
2212
+ }
2213
+ if (!stagedBytes.equals(expected)) fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2214
+ await mkdir(path.dirname(finalAbsolute), { recursive: true });
2215
+ await rename(staged, finalAbsolute);
2216
+ await fsyncDirectory(path.dirname(finalAbsolute));
2217
+ }
2218
+
2219
+ function splitSourceLines(bytes) {
2220
+ const lines = [];
2221
+ let start = 0;
2222
+ for (let index = 0; index <= bytes.length; index += 1) {
2223
+ if (index === bytes.length || bytes[index] === 0x0a) {
2224
+ lines.push({ start, end: index, bytes: bytes.subarray(start, index) });
2225
+ start = index + 1;
2226
+ }
2227
+ }
2228
+ return lines;
2229
+ }
2230
+
2231
+ async function safeRepoFile(repoRoot, ref, { missing = false } = {}) {
2232
+ const canonicalRoot = await realpath(repoRoot);
2233
+ const absolute = path.resolve(canonicalRoot, ref);
2234
+ if (!absolute.startsWith(`${canonicalRoot}${path.sep}`)) {
2235
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_outside_repo', { ref });
2236
+ }
2237
+ let current = canonicalRoot;
2238
+ const parts = path.relative(canonicalRoot, absolute).split(path.sep).filter(Boolean);
2239
+ for (const part of parts.slice(0, -1)) {
2240
+ current = path.join(current, part);
2241
+ let state;
2242
+ try { state = await lstat(current); }
2243
+ catch (error) {
2244
+ if (missing && error?.code === 'ENOENT') return { absolute, missingParent: current };
2245
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_missing', { ref });
2246
+ }
2247
+ if (state.isSymbolicLink() || !state.isDirectory() || await realpath(current) !== current) {
2248
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_symlink', { ref });
2249
+ }
2250
+ }
2251
+ let state;
2252
+ try { state = await lstat(absolute); }
2253
+ catch (error) {
2254
+ if (missing && error?.code === 'ENOENT') return { absolute, missing: true };
2255
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_missing', { ref });
2256
+ }
2257
+ if (state.isSymbolicLink() || !state.isFile() || state.nlink !== 1
2258
+ || await realpath(absolute) !== absolute) {
2259
+ fail('RECONCILIATION_INCOMPLETE', 'source_path_not_regular', { ref });
2260
+ }
2261
+ return { absolute, state, missing: false };
2262
+ }
2263
+
2264
+ async function ensureSafeRepoParent(repoRoot, absolute) {
2265
+ const canonicalRoot = await realpath(repoRoot);
2266
+ if (!absolute.startsWith(`${canonicalRoot}${path.sep}`)) {
2267
+ fail('REVISION_CONFLICT', 'source_path_outside_repo');
2268
+ }
2269
+ let current = canonicalRoot;
2270
+ for (const part of path.relative(canonicalRoot, path.dirname(absolute)).split(path.sep).filter(Boolean)) {
2271
+ current = path.join(current, part);
2272
+ try {
2273
+ const state = await lstat(current);
2274
+ if (state.isSymbolicLink() || !state.isDirectory() || await realpath(current) !== current) {
2275
+ fail('REVISION_CONFLICT', 'source_directory_unsafe');
2276
+ }
2277
+ } catch (error) {
2278
+ if (error instanceof TodoStoreError) throw error;
2279
+ if (error?.code !== 'ENOENT') throw error;
2280
+ await mkdir(current, { mode: 0o700 });
2281
+ await fsyncDirectory(path.dirname(current));
2282
+ }
2283
+ }
2284
+ }
2285
+
2286
+ function sourceCutoverArchiveBytes(revision, originals) {
2287
+ const header = [
2288
+ '# Lattice ToDo archive',
2289
+ `Plan: ${revision.plan_key}`,
2290
+ `Batch: ${revision.source_cutover_batch.batch_id}`,
2291
+ `Revision: ${revision.revision_digest}`,
2292
+ '',
2293
+ ].join('\n');
2294
+ return Buffer.concat([Buffer.from(`${header}\n`, 'utf8'),
2295
+ ...originals.flatMap((line) => [line, Buffer.from('\n')])]);
2296
+ }
2297
+
2298
+ async function buildSourceCutoverImages(repoRoot, revision) {
2299
+ const batch = revision.source_cutover_batch;
2300
+ const archiveState = await safeRepoFile(repoRoot, batch.archive_ref, { missing: true });
2301
+ if (!archiveState.missing && !archiveState.missingParent) {
2302
+ fail('REVISION_CONFLICT', 'source_cutover_archive_exists', { archive_ref: batch.archive_ref });
2303
+ }
2304
+ const groups = new Map();
2305
+ const originals = [];
2306
+ for (const operation of batch.operations) {
2307
+ const parsed = parseTodoSourceRef(operation.source_ref);
2308
+ const state = await safeRepoFile(repoRoot, parsed.path);
2309
+ let group = groups.get(parsed.path);
2310
+ if (!group) {
2311
+ const before = await readFile(state.absolute);
2312
+ decodeUtf8(before, 'RECONCILIATION_INCOMPLETE', 'source_invalid_utf8');
2313
+ group = { ref: parsed.path, absolute: state.absolute, before,
2314
+ mode: state.state.mode & 0o7777, replacements: [] };
2315
+ groups.set(parsed.path, group);
2316
+ }
2317
+ const line = splitSourceLines(group.before)[parsed.line - 1]?.bytes;
2318
+ if (line === undefined) fail('RECONCILIATION_INCOMPLETE', 'source_line_missing', {
2319
+ source_ref: operation.source_ref,
2320
+ });
2321
+ if (sha256Bytes(line) !== operation.source_digest) {
2322
+ fail('RECONCILIATION_INCOMPLETE', 'source_digest_mismatch', { source_ref: operation.source_ref });
2323
+ }
2324
+ if (markdownCheckboxState(line) === null) {
2325
+ fail('RECONCILIATION_INCOMPLETE', 'source_item_not_todo', { source_ref: operation.source_ref });
2326
+ }
2327
+ if (!liveReplacementPreservesListStructure(line, operation.live_replacement)) {
2328
+ fail('RECONCILIATION_INCOMPLETE', 'live_replacement_breaks_list_structure', {
2329
+ source_ref: operation.source_ref,
2330
+ });
2331
+ }
2332
+ originals.push(line);
2333
+ const replacement = Buffer.from(`${operation.live_replacement}${line.at(-1) === 0x0d ? '\r' : ''}`, 'utf8');
2334
+ group.replacements.push({ line: parsed.line, replacement });
2335
+ }
2336
+ const files = [...groups.values()].sort((left, right) => left.ref.localeCompare(right.ref));
2337
+ for (const file of files) {
2338
+ const lines = splitSourceLines(file.before);
2339
+ let after = file.before;
2340
+ for (const replacement of file.replacements.sort((left, right) => right.line - left.line)) {
2341
+ const target = lines[replacement.line - 1];
2342
+ after = Buffer.concat([after.subarray(0, target.start), replacement.replacement,
2343
+ after.subarray(target.end)]);
2344
+ }
2345
+ file.after = after;
2346
+ }
2347
+ return { files, archive: { ref: batch.archive_ref, absolute: archiveState.absolute,
2348
+ bytes: sourceCutoverArchiveBytes(revision, originals) } };
2349
+ }
2350
+
2351
+ async function stageSourceCutover(transaction, revision, images) {
2352
+ const files = [];
2353
+ for (const [index, file] of images.files.entries()) {
2354
+ const beforeName = `source-${index}-before.bin`;
2355
+ const afterName = `source-${index}-after.bin`;
2356
+ await atomicWrite(path.join(transaction, beforeName), file.before);
2357
+ await atomicWrite(path.join(transaction, afterName), file.after);
2358
+ files.push({ ref: file.ref, before: beforeName, after: afterName, mode: file.mode,
2359
+ before_digest: sha256Bytes(file.before), after_digest: sha256Bytes(file.after) });
2360
+ }
2361
+ await atomicWrite(path.join(transaction, 'source-archive.bin'), images.archive.bytes);
2362
+ const descriptor = {
2363
+ schema: 'lattice.todo_source_cutover_stage.v1', revision_digest: revision.revision_digest,
2364
+ archive_ref: images.archive.ref, archive_digest: sha256Bytes(images.archive.bytes), files,
2365
+ };
2366
+ await atomicWrite(path.join(transaction, 'source-cutover.json'), canonicalLine(descriptor));
2367
+ return descriptor;
2368
+ }
2369
+
2370
+ async function loadSourceCutoverStage(repoRoot, transaction, revision) {
2371
+ const bytes = await exactFileOrNull(path.join(transaction, 'source-cutover.json'));
2372
+ if (bytes === null) fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_missing');
2373
+ let descriptor;
2374
+ try { descriptor = JSON.parse(decodeUtf8(bytes, 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'stage_invalid')); }
2375
+ catch (error) {
2376
+ if (error instanceof TodoStoreError) throw error;
2377
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'stage_invalid');
2378
+ }
2379
+ if (!bytes.equals(canonicalLine(descriptor))) {
2380
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'stage_invalid');
2381
+ }
2382
+ const expectedRefs = [...new Set(revision.source_cutover_batch.operations.map((operation) => (
2383
+ parseTodoSourceRef(operation.source_ref).path)))].sort();
2384
+ if (!exactRecord(descriptor, [
2385
+ 'schema', 'revision_digest', 'archive_ref', 'archive_digest', 'files',
2386
+ ]) || descriptor.schema !== 'lattice.todo_source_cutover_stage.v1'
2387
+ || descriptor.revision_digest !== revision.revision_digest
2388
+ || descriptor.archive_ref !== revision.source_cutover_batch.archive_ref
2389
+ || !isTodoDigest(descriptor.archive_digest) || !Array.isArray(descriptor.files)
2390
+ || descriptor.files.length !== expectedRefs.length) {
2391
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'stage_invalid');
2392
+ }
2393
+ const files = [];
2394
+ for (const [index, file] of descriptor.files.entries()) {
2395
+ const expectedBefore = `source-${index}-before.bin`;
2396
+ const expectedAfter = `source-${index}-after.bin`;
2397
+ if (!exactRecord(file, [
2398
+ 'ref', 'before', 'after', 'mode', 'before_digest', 'after_digest',
2399
+ ]) || file.ref !== expectedRefs[index] || file.before !== expectedBefore
2400
+ || file.after !== expectedAfter || !isTodoDigest(file.before_digest)
2401
+ || !isTodoDigest(file.after_digest)) {
2402
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2403
+ }
2404
+ const beforeAbsolute = path.resolve(transaction, file.before);
2405
+ const afterAbsolute = path.resolve(transaction, file.after);
2406
+ if (path.dirname(beforeAbsolute) !== transaction || path.dirname(afterAbsolute) !== transaction) {
2407
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2408
+ }
2409
+ const before = await exactFileOrNull(beforeAbsolute);
2410
+ const after = await exactFileOrNull(afterAbsolute);
2411
+ if (before === null || after === null || sha256Bytes(before) !== file.before_digest
2412
+ || sha256Bytes(after) !== file.after_digest) {
2413
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2414
+ }
2415
+ if (!Number.isSafeInteger(file.mode) || file.mode < 0 || file.mode > 0o7777) {
2416
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2417
+ }
2418
+ const state = await safeRepoFile(repoRoot, file.ref);
2419
+ if ((state.state.mode & 0o7777) !== file.mode) {
2420
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2421
+ }
2422
+ const lines = splitSourceLines(before);
2423
+ let expectedAfterBytes = before;
2424
+ const replacements = [];
2425
+ for (const operation of revision.source_cutover_batch.operations) {
2426
+ const source = parseTodoSourceRef(operation.source_ref);
2427
+ if (source.path !== file.ref) continue;
2428
+ const line = lines[source.line - 1];
2429
+ if (line === undefined || sha256Bytes(line.bytes) !== operation.source_digest
2430
+ || markdownCheckboxState(line.bytes) === null
2431
+ || !liveReplacementPreservesListStructure(line.bytes, operation.live_replacement)) {
2432
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2433
+ }
2434
+ replacements.push({ line: source.line, start: line.start, end: line.end,
2435
+ bytes: phaseV3PublishedSourceBytes(operation, line.bytes) });
2436
+ }
2437
+ for (const replacement of replacements.sort((left, right) => right.line - left.line)) {
2438
+ expectedAfterBytes = Buffer.concat([expectedAfterBytes.subarray(0, replacement.start),
2439
+ replacement.bytes, expectedAfterBytes.subarray(replacement.end)]);
2440
+ }
2441
+ if (!after.equals(expectedAfterBytes)) {
2442
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2443
+ }
2444
+ files.push({ ...file, absolute: state.absolute, beforeAbsolute, afterAbsolute, before, after });
2445
+ }
2446
+ const archiveBytes = await exactFileOrNull(path.join(transaction, 'source-archive.bin'));
2447
+ const originalByRef = new Map(files.map((file) => [file.ref, splitSourceLines(file.before)]));
2448
+ const originals = revision.source_cutover_batch.operations.map((operation) => {
2449
+ const source = parseTodoSourceRef(operation.source_ref);
2450
+ return originalByRef.get(source.path)?.[source.line - 1]?.bytes;
2451
+ });
2452
+ if (archiveBytes === null || sha256Bytes(archiveBytes) !== descriptor.archive_digest
2453
+ || originals.some((line) => line === undefined)
2454
+ || !archiveBytes.equals(sourceCutoverArchiveBytes(revision, originals))) {
2455
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2456
+ }
2457
+ const archiveAbsolute = path.resolve(await realpath(repoRoot), descriptor.archive_ref);
2458
+ return { descriptor, files, archive: { absolute: archiveAbsolute, bytes: archiveBytes } };
2459
+ }
2460
+
2461
+ async function publishSourceCutover(repoRoot, staged) {
2462
+ await ensureSafeRepoParent(repoRoot, staged.archive.absolute);
2463
+ const existingArchive = await exactFileOrNull(staged.archive.absolute);
2464
+ if (existingArchive === null) await atomicWriteMode(staged.archive.absolute, staged.archive.bytes, 0o644);
2465
+ else if (!existingArchive.equals(staged.archive.bytes)) {
2466
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'archive_bytes_conflict');
2467
+ }
2468
+ for (const file of staged.files) {
2469
+ const current = await readFile(file.absolute);
2470
+ if (current.equals(file.after)) continue;
2471
+ if (!current.equals(file.before)) {
2472
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_bytes_conflict', { source_ref: file.ref });
2473
+ }
2474
+ await atomicWriteMode(file.absolute, file.after, file.mode);
2475
+ }
2476
+ }
2477
+
2478
+ async function rollbackSourceCutover(staged, barrierAbsolute, { removeBarrier = true } = {}) {
2479
+ for (const file of staged.files) {
2480
+ const current = await readFile(file.absolute);
2481
+ if (current.equals(file.after)) await atomicWriteMode(file.absolute, file.before, file.mode);
2482
+ else if (!current.equals(file.before)) return false;
2483
+ }
2484
+ const archive = await exactFileOrNull(staged.archive.absolute);
2485
+ if (archive !== null) {
2486
+ if (!archive.equals(staged.archive.bytes)) return false;
2487
+ await rm(staged.archive.absolute);
2488
+ await fsyncDirectory(path.dirname(staged.archive.absolute));
2489
+ }
2490
+ if (removeBarrier) {
2491
+ await rm(barrierAbsolute, { force: true });
2492
+ await fsyncDirectory(path.dirname(barrierAbsolute));
2493
+ }
2494
+ return true;
2495
+ }
2496
+
2497
+ async function cleanupPhaseV3Staging(transaction, staged) {
2498
+ const known = new Set(['source-archive.bin', 'source-cutover.json', 'source-originals.md']);
2499
+ await rm(path.join(transaction, 'source-cutover.json'), { force: true });
2500
+ await fsyncDirectory(transaction);
2501
+ for (const file of staged.files ?? []) {
2502
+ if (path.dirname(file.beforeAbsolute) !== transaction || path.dirname(file.afterAbsolute) !== transaction) {
2503
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');
2504
+ }
2505
+ await rm(file.beforeAbsolute, { force: true });
2506
+ await rm(file.afterAbsolute, { force: true });
2507
+ }
2508
+ for (const name of await readdir(transaction)) {
2509
+ if (known.has(name) || /^source-\d+-(?:before|after)\.bin$/u.test(name)) {
2510
+ await rm(path.join(transaction, name), { force: true });
2511
+ }
2512
+ }
2513
+ await fsyncDirectory(transaction);
2514
+ }
2515
+
2516
+ async function predecessorSourceInventory(repoRoot, previous) {
2517
+ if (previous.revision?.source_inventory !== undefined) return previous.revision.source_inventory;
2518
+ const versionsRoot = path.join(repoRoot, STORE_ROOT_REF, 'plans', previous.plan.plan_key);
2519
+ const plansByDigest = new Map();
2520
+ for (const entry of await readdir(versionsRoot, { withFileTypes: true })) {
2521
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
2522
+ const planBytes = await exactFileOrNull(path.join(versionsRoot, entry.name, 'plan.json'));
2523
+ if (planBytes === null) continue;
2524
+ let plan;
2525
+ try { plan = JSON.parse(decodeUtf8(planBytes, 'STORE_INCONSISTENT', 'plan_invalid')); }
2526
+ catch { continue; }
2527
+ if (!planBytes.equals(canonicalLine(plan)) || !validateTodoPlan(plan)
2528
+ || plan.plan_key !== previous.plan.plan_key) continue;
2529
+ plansByDigest.set(plan.plan_digest, { plan, directory: path.join(versionsRoot, entry.name) });
2530
+ }
2531
+ const visited = new Set();
2532
+ let digest = previous.plan.predecessor_plan_digest;
2533
+ while (digest !== null && !visited.has(digest) && visited.size < 512) {
2534
+ visited.add(digest);
2535
+ const candidate = plansByDigest.get(digest);
2536
+ if (candidate === undefined) break;
2537
+ const revisionBytes = await exactFileOrNull(path.join(candidate.directory, 'revision.json'));
2538
+ if (revisionBytes !== null) {
2539
+ let revision;
2540
+ try { revision = JSON.parse(decodeUtf8(revisionBytes,
2541
+ 'STORE_INCONSISTENT', 'revision_invalid')); } catch { revision = null; }
2542
+ if (revision !== null && revisionBytes.equals(canonicalLine(revision))
2543
+ && (validateTodoRevision(revision) || validatePhaseTodoRevision(revision))
2544
+ && revision.project_id === candidate.plan.project_id
2545
+ && revision.plan_key === candidate.plan.plan_key
2546
+ && canonicalizeTodoArtifact(revision.desired_plan)
2547
+ === canonicalizeTodoArtifact(candidate.plan)
2548
+ && revision.source_inventory !== undefined) return revision.source_inventory;
2549
+ }
2550
+ digest = candidate.plan.predecessor_plan_digest;
2551
+ }
2552
+ return { active: [], excluded_tombstones: [] };
2553
+ }
2554
+
2555
+ function validatePhaseV3SourceInventoryDiff(previousInventory, revision) {
2556
+ const desired = revision.source_inventory;
2557
+ const previousActive = new Map(previousInventory.active.map((entry) => [entry.task_id, entry]));
2558
+ const desiredTombstoneKeys = new Set(desired.excluded_tombstones.map((entry) => (
2559
+ `${entry.source_ref}\0${entry.source_digest}`)));
2560
+ for (const entry of previousInventory.active) {
2561
+ const active = desired.active.find(({ task_id }) => task_id === entry.task_id);
2562
+ const continued = active?.source_ref === entry.source_ref && active.source_digest === entry.source_digest;
2563
+ if (!continued && !desiredTombstoneKeys.has(`${entry.source_ref}\0${entry.source_digest}`)) {
2564
+ fail('REVISION_INVALID', 'predecessor_source_silently_dropped', { task_id: entry.task_id });
2565
+ }
2566
+ }
2567
+ const previousTombstoneKeys = new Set(previousInventory.excluded_tombstones.map((entry) => (
2568
+ `${entry.source_ref}\0${entry.source_digest}`)));
2569
+ const previousSourceKeys = new Set([...previousTombstoneKeys,
2570
+ ...previousInventory.active.map((entry) => `${entry.source_ref}\0${entry.source_digest}`)]);
2571
+ if ([...previousTombstoneKeys].some((key) => !desiredTombstoneKeys.has(key))) {
2572
+ fail('REVISION_INVALID', 'predecessor_source_silently_dropped');
2573
+ }
2574
+ const expected = [
2575
+ ...desired.active.filter((entry) => {
2576
+ const old = previousActive.get(entry.task_id);
2577
+ return old?.source_ref !== entry.source_ref || old.source_digest !== entry.source_digest;
2578
+ }).map((entry) => `active\0${entry.task_id}\0${entry.source_ref}\0${entry.source_digest}`),
2579
+ ...desired.excluded_tombstones.filter((entry) => (
2580
+ !previousSourceKeys.has(`${entry.source_ref}\0${entry.source_digest}`)
2581
+ )).map((entry) => `excluded\0null\0${entry.source_ref}\0${entry.source_digest}`),
2582
+ ].sort();
2583
+ const actual = revision.source_cutover_batch.operations.map((operation, index) => (
2584
+ `${operation.disposition}\0${operation.task_id}\0${todoCutoverArchiveSourceRef(
2585
+ revision.source_cutover_batch, index)}\0${operation.source_digest}`)).sort();
2586
+ if (canonicalizeTodoArtifact(actual) !== canonicalizeTodoArtifact(expected)) {
2587
+ fail('REVISION_INVALID', 'source_cutover_inventory_diff_mismatch');
2588
+ }
2589
+ }
2590
+
2591
+ async function applyPhaseTodoRevisionV3(options, revision, repoRoot) {
2592
+ return withLock(repoRoot, async () => {
2593
+ const barrierAbsolute = path.resolve(repoRoot, SOURCE_CUTOVER_BARRIER_REF);
2594
+ const barrierBytes = await exactFileOrNull(barrierAbsolute);
2595
+ let recovering = false;
2596
+ if (barrierBytes !== null) {
2597
+ let barrier;
2598
+ try { barrier = JSON.parse(decodeUtf8(barrierBytes,
2599
+ 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid')); }
2600
+ catch (error) {
2601
+ if (error instanceof TodoStoreError) throw error;
2602
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid');
2603
+ }
2604
+ if (barrier?.schema !== 'lattice.todo_source_cutover_barrier.v1'
2605
+ || barrier.revision_digest !== revision.revision_digest) {
2606
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_recovery_required');
2607
+ }
2608
+ recovering = true;
2609
+ }
2610
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now,
2611
+ ...(recovering ? { allowSourceCutoverRevisionDigest: revision.revision_digest,
2612
+ sourceCutoverRecoveryCapability: SOURCE_CUTOVER_RECOVERY_CAPABILITY } : {}) });
2613
+ const previous = store.members.find(({ descriptor }) => descriptor.plan_key === revision.plan_key);
2614
+ if (!previous || revision.project_id !== store.project_id) fail('STORE_INCONSISTENT', 'plan_not_active');
2615
+ const base = `${STORE_ROOT_REF}/plans/${revision.plan_key}/${revision.desired_plan.plan_version}`;
2616
+ const revisionRef = `${base}/revision.json`; const planRef = `${base}/plan.json`;
2617
+ const journalRef = `${base}/journal/active.jsonl`; const snapshotRef = `${base}/snapshot.json`;
2618
+ const sourceReceiptRef = `${base}/source-cutover-receipt.json`;
2619
+ const commitReceiptRef = `${base}/phase-revision-commit-receipt.json`;
2620
+ const transactionRef = `${STORE_ROOT_REF}/transactions/phase-v3/${revision.plan_key}/${revision.desired_plan.plan_version}`;
2621
+ const transaction = path.resolve(repoRoot, transactionRef);
2622
+ const activeGenesis = previous.journal.events[0];
2623
+ if (previous.plan.plan_digest === revision.desired_plan.plan_digest
2624
+ && activeGenesis.schema === 'lattice.todo_event.v4'
2625
+ && activeGenesis.revision_digest === revision.revision_digest) {
2626
+ const sourceReceiptBytes = await exactFileOrNull(path.resolve(repoRoot, sourceReceiptRef));
2627
+ const commitReceiptBytes = await exactFileOrNull(path.resolve(repoRoot, commitReceiptRef));
2628
+ if (sourceReceiptBytes === null || commitReceiptBytes === null
2629
+ || previous.descriptor.active_revision_digest !== revision.revision_digest) {
2630
+ fail('STORE_INCONSISTENT', 'phase_revision_receipt_missing');
2631
+ }
2632
+ const sourceReceipt = JSON.parse(decodeUtf8(sourceReceiptBytes,
2633
+ 'STORE_INCONSISTENT', 'phase_revision_receipt_invalid'));
2634
+ const commitReceipt = JSON.parse(decodeUtf8(commitReceiptBytes,
2635
+ 'STORE_INCONSISTENT', 'phase_revision_receipt_invalid'));
2636
+ if (!sourceReceiptBytes.equals(canonicalLine(sourceReceipt))
2637
+ || !commitReceiptBytes.equals(canonicalLine(commitReceipt))
2638
+ || !await verifyPhaseV3SourceReceipt(repoRoot, revision, sourceReceipt)
2639
+ || !validPhaseRevisionCommitReceipt(commitReceipt, revision, previous.descriptor,
2640
+ activeGenesis, sourceReceipt)) fail('STORE_INCONSISTENT', 'phase_revision_receipt_invalid');
2641
+ await rm(barrierAbsolute, { force: true });
2642
+ await rm(transaction, { recursive: true, force: true });
2643
+ return commitReceipt;
2644
+ }
2645
+ if (previous.plan.plan_digest !== revision.predecessor.plan_digest
2646
+ || previous.plan.plan_version !== revision.predecessor.plan_version
2647
+ || previous.journal.events.at(-1).event_digest !== revision.predecessor.journal_head_digest) {
2648
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2649
+ }
2650
+ const predecessorGenesis = previous.journal.events[0];
2651
+ const predecessorReconciliationDigest = predecessorGenesis.schema === 'lattice.todo_event.v2'
2652
+ ? predecessorGenesis.reconciliation_digest
2653
+ : previous.revision?.reconciliation?.reconciliation_digest
2654
+ ?? todoLegacyReconciliationDigest({ planDigest: previous.plan.plan_digest,
2655
+ journalHeadDigest: previous.journal.events.at(-1).event_digest });
2656
+ if (revision.reconciliation.predecessor_reconciliation_digest
2657
+ !== predecessorReconciliationDigest) fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2658
+ validatePhaseV3SourceInventoryDiff(await predecessorSourceInventory(repoRoot, previous), revision);
2659
+ await rejectCompetingPhaseV3Transaction(repoRoot, revision);
2660
+ verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
2661
+ const stateMigration = stateMigrationFor(previous, revision);
2662
+ const phaseStateMigration = phaseStateMigrationFor(previous, revision);
2663
+ validateMergedGraph(store.members.map((member) => member === previous
2664
+ ? { ...member, plan: revision.desired_plan } : member));
2665
+ const candidateGenesis = buildPhaseRevisionGenesis(revision.desired_plan, {
2666
+ previous_digest: revision.predecessor.journal_head_digest, actor: options.actor,
2667
+ recorded_at: options.recordedAt, provenance: options.provenance ?? null,
2668
+ revision_digest: revision.revision_digest, state_migration: stateMigration,
2669
+ phase_state_migration: phaseStateMigration,
2670
+ });
2671
+ await ensureSafeStoreDirectory(repoRoot, transaction);
2672
+ for (const ref of [revisionRef, planRef, journalRef, snapshotRef,
2673
+ sourceReceiptRef, commitReceiptRef]) {
2674
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, ref)));
2675
+ }
2676
+ const markerAbsolute = path.join(transaction, 'marker.json');
2677
+ const markerBytes = await exactFileOrNull(markerAbsolute);
2678
+ let genesis = candidateGenesis;
2679
+ if (markerBytes === null) {
2680
+ await atomicWrite(markerAbsolute, canonicalLine({
2681
+ schema: 'lattice.phase_todo_revision_transaction.v1', revision, genesis,
2682
+ }));
2683
+ } else {
2684
+ const marker = parsePhaseRevisionMarker(markerBytes);
2685
+ if (canonicalizeTodoArtifact(marker.revision) !== canonicalizeTodoArtifact(revision)
2686
+ || canonicalizeTodoArtifact(marker.genesis.state_migration)
2687
+ !== canonicalizeTodoArtifact(stateMigration)
2688
+ || canonicalizeTodoArtifact(marker.genesis.phase_state_migration)
2689
+ !== canonicalizeTodoArtifact(phaseStateMigration)) {
2690
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2691
+ }
2692
+ genesis = marker.genesis;
2693
+ }
2694
+ await protocolStage(options, 'phase_v3_marker_durable');
2695
+ const tasks = replay(revision.desired_plan, [genesis], {
2696
+ now: options.now ? new Date(options.now) : new Date(),
2697
+ verifyEvidence: evidenceVerifier(store.manifest, repoRoot, true),
2698
+ verifyImportSource: importSourceVerifier(repoRoot, true),
2699
+ });
2700
+ const snapshot = snapshotFor(revision.desired_plan, [genesis], tasks);
2701
+ for (const [stage, ref, bytes] of [
2702
+ ['phase_v3_revision_durable', revisionRef, canonicalLine(revision)],
2703
+ ['phase_v3_plan_durable', planRef, canonicalLine(revision.desired_plan)],
2704
+ ['phase_v3_genesis_durable', journalRef, canonicalLine(genesis)],
2705
+ ['phase_v3_snapshot_durable', snapshotRef, canonicalLine(snapshot)],
2706
+ ]) {
2707
+ await publishRevisionArtifact(path.join(transaction, path.basename(ref)),
2708
+ path.resolve(repoRoot, ref), bytes);
2709
+ await protocolStage(options, stage);
2710
+ }
2711
+ let sourceReceiptBytes = await exactFileOrNull(path.resolve(repoRoot, sourceReceiptRef));
2712
+ let sourceReceipt;
2713
+ if (sourceReceiptBytes === null) {
2714
+ let staged;
2715
+ if (recovering) {
2716
+ try { staged = await loadSourceCutoverStage(repoRoot, transaction, revision); }
2717
+ catch (error) {
2718
+ if (!(error instanceof TodoStoreError)
2719
+ || error.code !== 'SOURCE_CUTOVER_RECOVERY_REQUIRED'
2720
+ || error.detail?.reason !== 'source_cutover_stage_missing') throw error;
2721
+ staged = { descriptor: { files: [] }, files: [] };
2722
+ }
2723
+ } else {
2724
+ const images = await buildSourceCutoverImages(repoRoot, revision);
2725
+ const originals = await Promise.all(revision.source_cutover_batch.operations
2726
+ .map((operation) => sourceItemBytes(repoRoot, operation.source_ref)));
2727
+ await atomicWrite(path.join(transaction, 'source-originals.md'),
2728
+ Buffer.concat(originals.flatMap((bytes) => [bytes, Buffer.from('\n')])));
2729
+ await stageSourceCutover(transaction, revision, images);
2730
+ staged = await loadSourceCutoverStage(repoRoot, transaction, revision);
2731
+ await atomicWrite(barrierAbsolute, canonicalLine({
2732
+ schema: 'lattice.todo_source_cutover_barrier.v1', revision_digest: revision.revision_digest,
2733
+ }));
2734
+ await protocolStage(options, 'phase_v3_cutover_barrier_durable');
2735
+ }
2736
+ if (staged.descriptor.files.length > 0) await publishSourceCutover(repoRoot, staged);
2737
+ await protocolStage(options, 'phase_v3_source_published');
2738
+ try {
2739
+ await verifyRevisionSources(repoRoot, revision.source_inventory);
2740
+ sourceReceipt = await buildPhaseV3SourceReceipt(repoRoot, revision, transactionRef);
2741
+ } catch (error) {
2742
+ if (!(error instanceof TodoStoreError)) throw error;
2743
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_postimage_invalid');
2744
+ }
2745
+ await cleanupPhaseV3Staging(transaction, staged);
2746
+ await protocolStage(options, 'phase_v3_source_cleanup');
2747
+ await publishRevisionArtifact(path.join(transaction, 'source-cutover-receipt.json'),
2748
+ path.resolve(repoRoot, sourceReceiptRef), canonicalLine(sourceReceipt));
2749
+ await protocolStage(options, 'phase_v3_source_receipt_durable');
2750
+ } else {
2751
+ sourceReceipt = JSON.parse(decodeUtf8(sourceReceiptBytes,
2752
+ 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_receipt_invalid'));
2753
+ if (!sourceReceiptBytes.equals(canonicalLine(sourceReceipt))
2754
+ || !await verifyPhaseV3SourceReceipt(repoRoot, revision, sourceReceipt)) {
2755
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_receipt_invalid');
2756
+ }
2757
+ }
2758
+ const currentManifest = await readArtifact(repoRoot, MANIFEST_REF, {
2759
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
2760
+ });
2761
+ if (currentManifest.manifest_digest !== store.manifest.manifest_digest) {
2762
+ fail('STORE_WRITE_CONFLICT', 'manifest_digest_changed');
2763
+ }
2764
+ const members = currentManifest.members.map((member) => ({ ...member,
2765
+ active_revision_digest: member.plan_key === revision.plan_key ? revision.revision_digest
2766
+ : store.members.find(({ descriptor }) => descriptor.plan_key === member.plan_key)
2767
+ ?.revision?.revision_digest ?? store.members.find(({ descriptor }) => (
2768
+ descriptor.plan_key === member.plan_key))?.plan.plan_digest }));
2769
+ const descriptor = members.find(({ plan_key: key }) => key === revision.plan_key);
2770
+ Object.assign(descriptor, { active_plan_version: revision.desired_plan.plan_version,
2771
+ plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
2772
+ topology_digest: revision.desired_plan.topology_digest,
2773
+ journal_head_digest: genesis.event_digest, active_revision_digest: revision.revision_digest });
2774
+ const commitReceipt = { schema: 'lattice.phase_revision_commit_receipt.v1',
2775
+ project_id: revision.project_id, plan_key: revision.plan_key,
2776
+ plan_version: revision.desired_plan.plan_version, revision_digest: revision.revision_digest,
2777
+ committed_member_digest: digestTodoArtifact(descriptor),
2778
+ active_plan_digest: revision.desired_plan.plan_digest,
2779
+ journal_genesis_digest: genesis.event_digest,
2780
+ reconciliation_digest: revision.reconciliation.reconciliation_digest,
2781
+ source_cutover_receipt_digest: sourceReceipt.receipt_digest,
2782
+ committed_at: genesis.recorded_at, receipt_digest: '' };
2783
+ commitReceipt.receipt_digest = todoSelfDigest(commitReceipt, 'receipt_digest');
2784
+ await publishRevisionArtifact(path.join(transaction, 'phase-revision-commit-receipt.json'),
2785
+ path.resolve(repoRoot, commitReceiptRef), canonicalLine(commitReceipt));
2786
+ await protocolStage(options, 'phase_v3_commit_receipt_durable');
2787
+ const nextManifest = { schema: 'lattice.todo_manifest.v2', project_id: currentManifest.project_id,
2788
+ repositories: currentManifest.repositories, members, manifest_digest: '' };
2789
+ nextManifest.manifest_digest = todoSelfDigest(nextManifest, 'manifest_digest');
2790
+ if (!validateTodoManifest(nextManifest)) fail('REVISION_INVALID', 'manifest_v2_invalid');
2791
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(nextManifest));
2792
+ await protocolStage(options, 'phase_v3_manifest_activated');
2793
+ await rm(barrierAbsolute, { force: true });
2794
+ await fsyncDirectory(path.dirname(barrierAbsolute));
2795
+ await rm(transaction, { recursive: true, force: true });
2796
+ return commitReceipt;
2797
+ });
2798
+ }
2799
+
2800
+ /** Native successor transaction for first-class Phase plans; no Markdown source inventory is involved. */
2801
+ export async function applyPhaseTodoRevision(options = {}) {
2802
+ requireWriter(options.writer, 'g5-authoring');
2803
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
2804
+ const revision = options.revision;
2805
+ if (!validatePhaseTodoRevision(revision)) fail('REVISION_INVALID', 'phase_revision_schema_or_digest_invalid');
2806
+ if (revision.schema === 'lattice.phase_todo_revision.v3') {
2807
+ return applyPhaseTodoRevisionV3(options, revision, repoRoot);
2808
+ }
2809
+ return withLock(repoRoot, async () => {
2810
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
2811
+ const previous = store.members.find(({ descriptor }) => descriptor.plan_key === revision.plan_key);
2812
+ if (!previous || revision.project_id !== store.project_id) fail('STORE_INCONSISTENT', 'plan_not_active');
2813
+ const transaction = path.join(repoRoot, STORE_ROOT_REF, 'transactions', 'phase-revisions',
2814
+ revision.plan_key, revision.desired_plan.plan_version);
2815
+ const markerAbsolute = path.join(transaction, 'marker.json');
2816
+ const activeGenesis = previous.journal.events[0];
2817
+ if (previous.plan.plan_digest === revision.desired_plan.plan_digest
2818
+ && activeGenesis.schema === 'lattice.todo_event.v4'
2819
+ && activeGenesis.revision_digest === revision.revision_digest) {
2820
+ await rm(transaction, { recursive: true, force: true });
2821
+ return phaseRevisionResult(revision, activeGenesis, true);
2822
+ }
2823
+ if (previous.plan.plan_digest !== revision.predecessor.plan_digest
2824
+ || previous.plan.plan_version !== revision.predecessor.plan_version
2825
+ || previous.journal.events.at(-1).event_digest !== revision.predecessor.journal_head_digest) {
2826
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2827
+ }
2828
+ verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
2829
+ const stateMigration = stateMigrationFor(previous, revision);
2830
+ const phaseStateMigration = phaseStateMigrationFor(previous, revision);
2831
+ const prospective = store.members.map((member) => member === previous
2832
+ ? { ...member, plan: revision.desired_plan } : member);
2833
+ validateMergedGraph(prospective);
2834
+ const candidateGenesis = buildPhaseRevisionGenesis(revision.desired_plan, {
2835
+ previous_digest: revision.predecessor.journal_head_digest,
2836
+ actor: options.actor, recorded_at: options.recordedAt, provenance: options.provenance ?? null,
2837
+ revision_digest: revision.revision_digest, state_migration: stateMigration,
2838
+ phase_state_migration: phaseStateMigration,
2839
+ });
2840
+ await ensureSafeStoreDirectory(repoRoot, transaction);
2841
+ const markerBytes = await exactFileOrNull(markerAbsolute);
2842
+ let genesis = candidateGenesis;
2843
+ if (markerBytes !== null) {
2844
+ const marker = parsePhaseRevisionMarker(markerBytes);
2845
+ if (canonicalizeTodoArtifact(marker.revision) !== canonicalizeTodoArtifact(revision)
2846
+ || canonicalizeTodoArtifact(marker.genesis.state_migration)
2847
+ !== canonicalizeTodoArtifact(stateMigration)
2848
+ || canonicalizeTodoArtifact(marker.genesis.phase_state_migration)
2849
+ !== canonicalizeTodoArtifact(phaseStateMigration)) {
2850
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2851
+ }
2852
+ genesis = marker.genesis;
2853
+ } else {
2854
+ await atomicWrite(markerAbsolute, canonicalLine({
2855
+ schema: 'lattice.phase_todo_revision_transaction.v1', revision, genesis,
2856
+ }));
2857
+ }
2858
+ await protocolStage(options, 'phase_revision_marker_durable');
2859
+ const base = `${STORE_ROOT_REF}/plans/${revision.plan_key}/${revision.desired_plan.plan_version}`;
2860
+ const revisionRef = `${base}/revision.json`; const planRef = `${base}/plan.json`;
2861
+ const journalRef = `${base}/journal/active.jsonl`; const snapshotRef = `${base}/snapshot.json`;
2862
+ const tasks = replay(revision.desired_plan, [genesis], {
2863
+ now: options.now ? new Date(options.now) : new Date(),
2864
+ verifyEvidence: evidenceVerifier(store.manifest, repoRoot, true),
2865
+ verifyImportSource: importSourceVerifier(repoRoot, true),
2866
+ });
2867
+ const snapshot = snapshotFor(revision.desired_plan, [genesis], tasks);
2868
+ const artifacts = [
2869
+ ['phase_revision_input_durable', revisionRef, canonicalLine(revision)],
2870
+ ['phase_revision_plan_durable', planRef, canonicalLine(revision.desired_plan)],
2871
+ ['phase_revision_genesis_durable', journalRef, canonicalLine(genesis)],
2872
+ ['phase_revision_snapshot_durable', snapshotRef, canonicalLine(snapshot)],
2873
+ ];
2874
+ for (const [stage, ref, bytes] of artifacts) {
2875
+ const absolute = path.resolve(repoRoot, ref);
2876
+ const existing = await exactFileOrNull(absolute);
2877
+ if (existing === null) await atomicWrite(absolute, bytes);
2878
+ else if (!existing.equals(bytes)) fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2879
+ await protocolStage(options, stage);
2880
+ }
2881
+ const currentManifest = await readArtifact(repoRoot, MANIFEST_REF, {
2882
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
2883
+ });
2884
+ if (currentManifest.manifest_digest !== store.manifest.manifest_digest) {
2885
+ fail('STORE_WRITE_CONFLICT', 'manifest_digest_changed');
2886
+ }
2887
+ const descriptor = currentManifest.members.find(({ plan_key }) => plan_key === revision.plan_key);
2888
+ Object.assign(descriptor, { active_plan_version: revision.desired_plan.plan_version,
2889
+ plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
2890
+ topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest });
2891
+ currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
2892
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
2893
+ await protocolStage(options, 'phase_revision_manifest_activated');
2894
+ await rm(transaction, { recursive: true, force: true });
2895
+ return phaseRevisionResult(revision, genesis, false);
2896
+ });
2897
+ }
2898
+
2899
+ /** G5 revision transaction: exact successor, state migration, and manifest-CAS activation. */
2900
+ export async function applyTodoRevision(options = {}) {
2901
+ requireWriter(options.writer, 'g5-authoring');
2902
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
2903
+ const revision = options.revision;
2904
+ if (!validateTodoRevision(revision)) fail('REVISION_INVALID', 'revision_schema_or_digest_invalid');
2905
+ return withLock(repoRoot, async () => {
2906
+ const barrierAbsolute = path.resolve(repoRoot, SOURCE_CUTOVER_BARRIER_REF);
2907
+ const barrierBytes = await exactFileOrNull(barrierAbsolute);
2908
+ let recovering = false;
2909
+ if (barrierBytes !== null) {
2910
+ let barrier;
2911
+ try { barrier = JSON.parse(decodeUtf8(barrierBytes, 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid')); }
2912
+ catch (error) {
2913
+ if (error instanceof TodoStoreError) throw error;
2914
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid');
2915
+ }
2916
+ if (revision.schema !== 'lattice.todo_revision.v2'
2917
+ || barrier?.schema !== 'lattice.todo_source_cutover_barrier.v1'
2918
+ || barrier.revision_digest !== revision.revision_digest) {
2919
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_recovery_required');
2920
+ }
2921
+ recovering = true;
2922
+ }
2923
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now,
2924
+ ...(recovering ? { allowSourceCutoverRevisionDigest: revision.revision_digest,
2925
+ sourceCutoverRecoveryCapability: SOURCE_CUTOVER_RECOVERY_CAPABILITY } : {}) });
2926
+ const previous = store.members.find(({ descriptor }) => descriptor.plan_key === revision.plan_key);
2927
+ if (!previous || revision.project_id !== store.project_id) fail('STORE_INCONSISTENT', 'plan_not_active');
2928
+ const activeGenesis = previous.journal.events[0];
2929
+ if (previous.plan.plan_digest === revision.desired_plan.plan_digest
2930
+ && activeGenesis.schema === 'lattice.todo_event.v2'
2931
+ && activeGenesis.revision_digest === revision.revision_digest) {
2932
+ const transaction = path.join(repoRoot, STORE_ROOT_REF, 'transactions', 'revisions',
2933
+ revision.plan_key, revision.desired_plan.plan_version);
2934
+ if (revision.schema === 'lattice.todo_revision.v2' && recovering) {
2935
+ const staged = await loadSourceCutoverStage(repoRoot, transaction, revision);
2936
+ await publishSourceCutover(repoRoot, staged);
2937
+ await rm(barrierAbsolute, { force: true });
2938
+ await fsyncDirectory(path.dirname(barrierAbsolute));
2939
+ }
2940
+ await rm(transaction, { recursive: true, force: true });
2941
+ return revisionResult(revision, activeGenesis, { recovered: recovering });
2942
+ }
2943
+ if (activeGenesis.schema === 'lattice.todo_event.v2'
2944
+ && activeGenesis.payload.predecessor_plan_digest === revision.predecessor.plan_digest
2945
+ && activeGenesis.previous_digest === revision.predecessor.journal_head_digest) {
2946
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2947
+ }
2948
+ if (previous.plan.plan_digest !== revision.predecessor.plan_digest
2949
+ || previous.plan.plan_version !== revision.predecessor.plan_version
2950
+ || previous.journal.events.at(-1).event_digest !== revision.predecessor.journal_head_digest) {
2951
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2952
+ }
2953
+ const predecessorReconciliationDigest = activeGenesis.schema === 'lattice.todo_event.v2'
2954
+ ? activeGenesis.reconciliation_digest
2955
+ : todoLegacyReconciliationDigest({
2956
+ planDigest: previous.plan.plan_digest,
2957
+ journalHeadDigest: previous.journal.events.at(-1).event_digest,
2958
+ });
2959
+ if (revision.reconciliation.predecessor_reconciliation_digest
2960
+ !== predecessorReconciliationDigest) fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2961
+
2962
+ await rejectCompetingRevisionTransaction(repoRoot, revision);
2963
+ const cutoverImages = revision.schema === 'lattice.todo_revision.v2' && !recovering
2964
+ ? await buildSourceCutoverImages(repoRoot, revision) : null;
2965
+ if (revision.schema === 'lattice.todo_revision.v1') {
2966
+ await verifyRevisionSources(repoRoot, revision.source_inventory);
2967
+ }
2968
+ verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
2969
+ const stateMigration = stateMigrationFor(previous, revision);
2970
+ const prospective = store.members.map((member) => member === previous
2971
+ ? { ...member, plan: revision.desired_plan } : member);
2972
+ validateMergedGraph(prospective);
2973
+
2974
+ const candidateGenesis = buildRevisionGenesis(revision.desired_plan, {
2975
+ previous_digest: revision.predecessor.journal_head_digest,
2976
+ actor: options.actor, recorded_at: options.recordedAt,
2977
+ provenance: options.provenance ?? null, state_migration: stateMigration,
2978
+ revision_digest: revision.revision_digest,
2979
+ reconciliation_digest: revision.reconciliation.reconciliation_digest,
2980
+ });
2981
+ const base = `${STORE_ROOT_REF}/plans/${revision.plan_key}/${revision.desired_plan.plan_version}`;
2982
+ const revisionRef = `${base}/revision.json`;
2983
+ const planRef = `${base}/plan.json`;
2984
+ const journalRef = `${base}/journal/active.jsonl`;
2985
+ const snapshotRef = `${base}/snapshot.json`;
2986
+ const transactionRef = `${STORE_ROOT_REF}/transactions/revisions/${revision.plan_key}/${revision.desired_plan.plan_version}`;
2987
+ const transaction = path.resolve(repoRoot, transactionRef);
2988
+ const markerAbsolute = path.join(transaction, 'marker.json');
2989
+ await ensureSafeStoreDirectory(repoRoot, transaction);
2990
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, planRef)));
2991
+ const markerBytes = await exactFileOrNull(markerAbsolute);
2992
+ let genesis = candidateGenesis;
2993
+ if (markerBytes !== null) {
2994
+ const marker = parseRevisionMarker(markerBytes);
2995
+ if (canonicalizeTodoArtifact(marker.revision) !== canonicalizeTodoArtifact(revision)
2996
+ || canonicalizeTodoArtifact(marker.genesis.state_migration)
2997
+ !== canonicalizeTodoArtifact(stateMigration)) {
2998
+ fail('REVISION_CONFLICT', 'revision_bytes_conflict');
2999
+ }
3000
+ genesis = marker.genesis;
3001
+ } else {
3002
+ await atomicWrite(markerAbsolute, canonicalLine({
3003
+ schema: 'lattice.todo_revision_transaction.v1', revision, genesis,
3004
+ }));
3005
+ }
3006
+ await protocolStage(options, 'revision_marker_durable');
3007
+
3008
+ const verifyEvidence = evidenceVerifier(store.manifest, repoRoot, true);
3009
+ const verifyImportSource = importSourceVerifier(repoRoot, true);
3010
+ const tasks = replay(revision.desired_plan, [genesis], {
3011
+ now: options.now ? new Date(options.now) : new Date(), verifyEvidence, verifyImportSource,
3012
+ });
3013
+ const snapshot = snapshotFor(revision.desired_plan, [genesis], tasks);
3014
+ const planBytes = canonicalLine(revision.desired_plan);
3015
+ const journalBytes = canonicalLine(genesis);
3016
+ const snapshotBytes = canonicalLine(snapshot);
3017
+ await publishRevisionArtifact(path.join(transaction, 'revision.json'),
3018
+ path.resolve(repoRoot, revisionRef), canonicalLine(revision));
3019
+ await protocolStage(options, 'revision_input_durable');
3020
+ await publishRevisionArtifact(path.join(transaction, 'plan.json'), path.resolve(repoRoot, planRef), planBytes);
3021
+ await protocolStage(options, 'revision_plan_durable');
3022
+ await publishRevisionArtifact(path.join(transaction, 'active.jsonl'), path.resolve(repoRoot, journalRef), journalBytes);
3023
+ await protocolStage(options, 'revision_genesis_durable');
3024
+ await publishRevisionArtifact(path.join(transaction, 'snapshot.json'), path.resolve(repoRoot, snapshotRef), snapshotBytes);
3025
+ await protocolStage(options, 'revision_snapshot_durable');
3026
+
3027
+ let stagedCutover = null;
3028
+ if (revision.schema === 'lattice.todo_revision.v2') {
3029
+ if (recovering) stagedCutover = await loadSourceCutoverStage(repoRoot, transaction, revision);
3030
+ else {
3031
+ const descriptor = await stageSourceCutover(transaction, revision, cutoverImages);
3032
+ stagedCutover = await loadSourceCutoverStage(repoRoot, transaction, revision);
3033
+ if (descriptor.revision_digest !== revision.revision_digest) {
3034
+ fail('REVISION_CONFLICT', 'source_cutover_stage_invalid');
3035
+ }
3036
+ await protocolStage(options, 'source_cutover_staged');
3037
+ await atomicWrite(barrierAbsolute, canonicalLine({
3038
+ schema: 'lattice.todo_source_cutover_barrier.v1',
3039
+ revision_digest: revision.revision_digest,
3040
+ }));
3041
+ await protocolStage(options, 'source_cutover_barrier_durable');
3042
+ }
3043
+ await publishSourceCutover(repoRoot, stagedCutover);
3044
+ await protocolStage(options, 'source_cutover_published');
3045
+ }
3046
+
3047
+ try {
3048
+ const currentManifest = await readArtifact(repoRoot, MANIFEST_REF, {
3049
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
3050
+ });
3051
+ if (currentManifest.manifest_digest !== store.manifest.manifest_digest) {
3052
+ fail('STORE_WRITE_CONFLICT', 'manifest_digest_changed');
3053
+ }
3054
+ const descriptor = currentManifest.members.find(({ plan_key }) => plan_key === revision.plan_key);
3055
+ if (!descriptor || descriptor.active_plan_version !== revision.predecessor.plan_version
3056
+ || descriptor.journal_head_digest !== revision.predecessor.journal_head_digest) {
3057
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
3058
+ }
3059
+ Object.assign(descriptor, {
3060
+ active_plan_version: revision.desired_plan.plan_version, plan_ref: planRef,
3061
+ journal_ref: journalRef, snapshot_ref: snapshotRef,
3062
+ topology_digest: revision.desired_plan.topology_digest,
3063
+ journal_head_digest: genesis.event_digest,
3064
+ });
3065
+ currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
3066
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
3067
+ await protocolStage(options, 'revision_manifest_activated');
3068
+ } catch (error) {
3069
+ if (stagedCutover !== null && error instanceof TodoStoreError) {
3070
+ const rolledBack = await rollbackSourceCutover(stagedCutover, barrierAbsolute);
3071
+ if (!rolledBack) fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'rollback_incomplete');
3072
+ }
3073
+ throw error;
3074
+ }
3075
+ if (revision.schema === 'lattice.todo_revision.v2') {
3076
+ await rm(barrierAbsolute, { force: true });
3077
+ await fsyncDirectory(path.dirname(barrierAbsolute));
3078
+ await protocolStage(options, 'source_cutover_cleanup');
3079
+ }
3080
+ await rm(transaction, { recursive: true, force: true });
3081
+ return revisionResult(revision, genesis, { recovered: recovering });
3082
+ });
3083
+ }
3084
+
3085
+ function revisionSetResult(projectId, revisionSetDigest, entries, { recovered }) {
3086
+ const result = {
3087
+ schema: 'lattice.todo_revision_set_result.v1',
3088
+ project_id: projectId,
3089
+ revision_set_digest: revisionSetDigest,
3090
+ members: entries.map(({ revision, genesis }) => ({
3091
+ plan_key: revision.plan_key,
3092
+ plan_version: revision.desired_plan.plan_version,
3093
+ revision_digest: revision.revision_digest,
3094
+ journal_head_digest: genesis.event_digest,
3095
+ })),
3096
+ recovered,
3097
+ result_digest: '',
3098
+ };
3099
+ result.result_digest = todoSelfDigest(result, 'result_digest');
3100
+ return result;
3101
+ }
3102
+
3103
+ /**
3104
+ * Multiple plan successors are prepared independently but become active through one manifest replace.
3105
+ * Set v2 additionally binds all Markdown cutovers behind one recovery barrier.
3106
+ */
3107
+ export async function applyTodoRevisionSet(options = {}) {
3108
+ requireWriter(options.writer, 'g5-authoring');
3109
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3110
+ const revisionSet = options.revisionSet ?? null;
3111
+ if (revisionSet !== null && !validateTodoRevisionSet(revisionSet)) {
3112
+ fail('REVISION_SET_INVALID', 'revision_set_schema_invalid');
3113
+ }
3114
+ const revisions = revisionSet?.revisions ?? options.revisions;
3115
+ if (!Array.isArray(revisions) || revisions.length < 2 || revisions.length > 64
3116
+ || revisions.some((revision) => !validateTodoRevision(revision)
3117
+ && !validatePhaseTodoRevision(revision))) {
3118
+ fail('REVISION_SET_INVALID', 'revision_set_schema_invalid');
3119
+ }
3120
+ if (revisionSet === null && revisions.some((revision) => revision.schema !== 'lattice.todo_revision.v1')) {
3121
+ fail('REVISION_SET_INVALID', 'revision_set_v2_required');
3122
+ }
3123
+ if (revisions.some((revision, index) => index > 0
3124
+ && revisions[index - 1].plan_key >= revision.plan_key)) {
3125
+ fail('REVISION_SET_INVALID', 'revision_set_order_invalid');
3126
+ }
3127
+ const projectId = revisions[0].project_id;
3128
+ if (revisions.some((revision) => revision.project_id !== projectId)) {
3129
+ fail('REVISION_SET_INVALID', 'revision_set_project_mismatch');
3130
+ }
3131
+ const revisionSetDigest = revisionSet?.revision_set_digest ?? digestTodoArtifact({
3132
+ schema: 'lattice.todo_revision_set.v1',
3133
+ project_id: projectId,
3134
+ revision_digests: revisions.map(({ plan_key, revision_digest }) => ({ plan_key, revision_digest })),
3135
+ });
3136
+
3137
+ return withLock(repoRoot, async () => {
3138
+ const transactionRef = `${STORE_ROOT_REF}/transactions/revision-sets/${revisionSetDigest}`;
3139
+ const transaction = path.resolve(repoRoot, transactionRef);
3140
+ const barrierAbsolute = path.resolve(repoRoot, SOURCE_CUTOVER_BARRIER_REF);
3141
+ const barrierBytes = await exactFileOrNull(barrierAbsolute);
3142
+ let recovering = false;
3143
+ if (barrierBytes !== null) {
3144
+ let barrier;
3145
+ try { barrier = JSON.parse(decodeUtf8(barrierBytes, 'SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid')); }
3146
+ catch (error) {
3147
+ if (error instanceof TodoStoreError) throw error;
3148
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'barrier_invalid');
3149
+ }
3150
+ if (barrier?.schema !== 'lattice.todo_source_cutover_set_barrier.v1'
3151
+ || barrier.revision_set_digest !== revisionSetDigest) {
3152
+ fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_recovery_required');
3153
+ }
3154
+ recovering = true;
3155
+ }
3156
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now,
3157
+ ...(recovering ? {
3158
+ allowSourceCutoverRevisionSetDigest: revisionSetDigest,
3159
+ sourceCutoverRecoveryCapability: SOURCE_CUTOVER_RECOVERY_CAPABILITY,
3160
+ } : {}) });
3161
+ if (store.project_id !== projectId) fail('REVISION_SET_INVALID', 'revision_set_project_mismatch');
3162
+ const byPlan = new Map(store.members.map((member) => [member.descriptor.plan_key, member]));
3163
+ const activeDesired = revisions.map((revision) => {
3164
+ const member = byPlan.get(revision.plan_key);
3165
+ const genesis = member?.journal.events[0];
3166
+ const expectedGenesisSchema = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2']
3167
+ .includes(revision.schema)
3168
+ ? 'lattice.todo_event.v4' : 'lattice.todo_event.v2';
3169
+ return member !== undefined
3170
+ && member.plan.plan_digest === revision.desired_plan.plan_digest
3171
+ && genesis.schema === expectedGenesisSchema
3172
+ && genesis.revision_digest === revision.revision_digest;
3173
+ });
3174
+ if (activeDesired.every(Boolean)) {
3175
+ const entries = revisions.map((revision) => ({
3176
+ revision,
3177
+ genesis: byPlan.get(revision.plan_key).journal.events[0],
3178
+ }));
3179
+ if (recovering) {
3180
+ for (const entry of entries.filter(({ revision }) => revision.schema === 'lattice.todo_revision.v2')) {
3181
+ const staged = await loadSourceCutoverStage(repoRoot,
3182
+ path.join(transaction, entry.revision.plan_key, 'source'), entry.revision);
3183
+ await publishSourceCutover(repoRoot, staged);
3184
+ }
3185
+ await rm(barrierAbsolute, { force: true });
3186
+ await fsyncDirectory(path.dirname(barrierAbsolute));
3187
+ await rm(transaction, { recursive: true, force: true });
3188
+ }
3189
+ return revisionSetResult(projectId, revisionSetDigest, entries, { recovered: true });
3190
+ }
3191
+ if (activeDesired.some(Boolean)) fail('REVISION_SET_RECOVERY_REQUIRED', 'partial_manifest_activation');
3192
+
3193
+ const prepared = [];
3194
+ for (const revision of revisions) {
3195
+ const previous = byPlan.get(revision.plan_key);
3196
+ if (previous === undefined) fail('STORE_INCONSISTENT', 'plan_not_active');
3197
+ if (previous.plan.plan_digest !== revision.predecessor.plan_digest
3198
+ || previous.plan.plan_version !== revision.predecessor.plan_version
3199
+ || previous.journal.events.at(-1).event_digest !== revision.predecessor.journal_head_digest) {
3200
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
3201
+ }
3202
+ const phaseRevision = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2']
3203
+ .includes(revision.schema);
3204
+ const activeGenesis = previous.journal.events[0];
3205
+ if (!phaseRevision) {
3206
+ const predecessorReconciliationDigest = activeGenesis.schema === 'lattice.todo_event.v2'
3207
+ ? activeGenesis.reconciliation_digest
3208
+ : todoLegacyReconciliationDigest({
3209
+ planDigest: previous.plan.plan_digest,
3210
+ journalHeadDigest: previous.journal.events.at(-1).event_digest,
3211
+ });
3212
+ if (revision.reconciliation.predecessor_reconciliation_digest
3213
+ !== predecessorReconciliationDigest) fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
3214
+ await rejectCompetingRevisionTransaction(repoRoot, revision);
3215
+ }
3216
+ const cutoverImages = revision.schema === 'lattice.todo_revision.v2' && !recovering
3217
+ ? await buildSourceCutoverImages(repoRoot, revision) : null;
3218
+ if (revision.schema === 'lattice.todo_revision.v1') {
3219
+ await verifyRevisionSources(repoRoot, revision.source_inventory);
3220
+ }
3221
+ verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
3222
+ const stateMigration = stateMigrationFor(previous, revision);
3223
+ const phaseStateMigration = phaseRevision ? phaseStateMigrationFor(previous, revision) : null;
3224
+ const commonGenesis = {
3225
+ previous_digest: revision.predecessor.journal_head_digest, actor: options.actor,
3226
+ recorded_at: options.recordedAt, provenance: options.provenance ?? null,
3227
+ state_migration: stateMigration, revision_digest: revision.revision_digest,
3228
+ };
3229
+ const genesis = phaseRevision
3230
+ ? buildPhaseRevisionGenesis(revision.desired_plan, {
3231
+ ...commonGenesis, phase_state_migration: phaseStateMigration,
3232
+ })
3233
+ : buildRevisionGenesis(revision.desired_plan, {
3234
+ ...commonGenesis, reconciliation_digest: revision.reconciliation.reconciliation_digest,
3235
+ });
3236
+ prepared.push({ revision, previous, stateMigration, phaseStateMigration, genesis, cutoverImages });
3237
+ }
3238
+
3239
+ const desiredByPlan = new Map(prepared.map((entry) => [entry.revision.plan_key,
3240
+ entry.revision.desired_plan]));
3241
+ validateMergedGraph(store.members.map((member) => desiredByPlan.has(member.descriptor.plan_key)
3242
+ ? { ...member, plan: desiredByPlan.get(member.descriptor.plan_key) } : member));
3243
+
3244
+ await ensureSafeStoreDirectory(repoRoot, transaction);
3245
+ const mixedPhaseSet = prepared.some(({ revision }) => ([
3246
+ 'lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
3247
+ ].includes(revision.schema)));
3248
+ let marker = {
3249
+ schema: mixedPhaseSet
3250
+ ? 'lattice.todo_revision_set_transaction.v2' : 'lattice.todo_revision_set_transaction.v1',
3251
+ project_id: projectId,
3252
+ revision_set_digest: revisionSetDigest,
3253
+ entries: prepared.map(({ revision, stateMigration, phaseStateMigration, genesis }) => ({
3254
+ revision, state_migration: stateMigration,
3255
+ ...(phaseStateMigration === null ? {} : { phase_state_migration: phaseStateMigration }), genesis,
3256
+ })),
3257
+ };
3258
+ const markerAbsolute = path.join(transaction, 'marker.json');
3259
+ const existingMarker = await exactFileOrNull(markerAbsolute);
3260
+ if (existingMarker === null) await atomicWrite(markerAbsolute, canonicalLine(marker));
3261
+ else {
3262
+ const durable = parseRevisionSetMarker(existingMarker);
3263
+ const expectedIdentity = marker.entries.map(({ revision, state_migration: stateMigration,
3264
+ phase_state_migration: phaseStateMigration }) => ({
3265
+ revision, state_migration: stateMigration,
3266
+ ...(phaseStateMigration === undefined ? {} : { phase_state_migration: phaseStateMigration }),
3267
+ }));
3268
+ const durableIdentity = durable.entries.map(({ revision, state_migration: stateMigration,
3269
+ phase_state_migration: phaseStateMigration }) => ({
3270
+ revision, state_migration: stateMigration,
3271
+ ...(phaseStateMigration === undefined ? {} : { phase_state_migration: phaseStateMigration }),
3272
+ }));
3273
+ if (durable.project_id !== projectId || durable.revision_set_digest !== revisionSetDigest
3274
+ || canonicalizeTodoArtifact(durableIdentity) !== canonicalizeTodoArtifact(expectedIdentity)) {
3275
+ fail('REVISION_CONFLICT', 'revision_set_bytes_conflict');
3276
+ }
3277
+ marker = durable;
3278
+ for (const [index, entry] of prepared.entries()) entry.genesis = marker.entries[index].genesis;
3279
+ }
3280
+ await protocolStage(options, 'revision_set_marker_durable');
3281
+
3282
+ const verifyEvidence = evidenceVerifier(store.manifest, repoRoot, true);
3283
+ const verifyImportSource = importSourceVerifier(repoRoot, true);
3284
+ for (const entry of prepared) {
3285
+ const { revision, genesis } = entry;
3286
+ const tasks = replay(revision.desired_plan, [genesis], {
3287
+ now: options.now ? new Date(options.now) : new Date(), verifyEvidence, verifyImportSource,
3288
+ });
3289
+ const snapshot = snapshotFor(revision.desired_plan, [genesis], tasks);
3290
+ const base = `${STORE_ROOT_REF}/plans/${revision.plan_key}/${revision.desired_plan.plan_version}`;
3291
+ entry.refs = {
3292
+ revision_ref: `${base}/revision.json`,
3293
+ plan_ref: `${base}/plan.json`,
3294
+ journal_ref: `${base}/journal/active.jsonl`,
3295
+ snapshot_ref: `${base}/snapshot.json`,
3296
+ };
3297
+ const staged = path.join(transaction, revision.plan_key);
3298
+ await publishRevisionArtifact(path.join(staged, 'revision.json'),
3299
+ path.resolve(repoRoot, entry.refs.revision_ref), canonicalLine(revision));
3300
+ await publishRevisionArtifact(path.join(staged, 'plan.json'),
3301
+ path.resolve(repoRoot, entry.refs.plan_ref), canonicalLine(revision.desired_plan));
3302
+ await publishRevisionArtifact(path.join(staged, 'active.jsonl'),
3303
+ path.resolve(repoRoot, entry.refs.journal_ref), canonicalLine(genesis));
3304
+ await publishRevisionArtifact(path.join(staged, 'snapshot.json'),
3305
+ path.resolve(repoRoot, entry.refs.snapshot_ref), canonicalLine(snapshot));
3306
+ }
3307
+ await protocolStage(options, 'revision_set_artifacts_durable');
3308
+
3309
+ const stagedCutovers = [];
3310
+ if (prepared.some(({ revision }) => revision.schema === 'lattice.todo_revision.v2')) {
3311
+ const sourceRefs = new Set();
3312
+ const archiveRefs = new Set();
3313
+ for (const entry of prepared.filter(({ revision }) => revision.schema === 'lattice.todo_revision.v2')) {
3314
+ if (archiveRefs.has(entry.revision.source_cutover_batch.archive_ref)) {
3315
+ fail('REVISION_SET_INVALID', 'source_cutover_archive_conflict');
3316
+ }
3317
+ archiveRefs.add(entry.revision.source_cutover_batch.archive_ref);
3318
+ for (const operation of entry.revision.source_cutover_batch.operations) {
3319
+ if (sourceRefs.has(operation.source_ref)) {
3320
+ fail('REVISION_SET_INVALID', 'source_cutover_source_conflict');
3321
+ }
3322
+ sourceRefs.add(operation.source_ref);
3323
+ }
3324
+ const sourceTransaction = path.join(transaction, entry.revision.plan_key, 'source');
3325
+ if (!recovering) await stageSourceCutover(sourceTransaction, entry.revision, entry.cutoverImages);
3326
+ stagedCutovers.push(await loadSourceCutoverStage(repoRoot, sourceTransaction, entry.revision));
3327
+ }
3328
+ if (!recovering) {
3329
+ await atomicWrite(barrierAbsolute, canonicalLine({
3330
+ schema: 'lattice.todo_source_cutover_set_barrier.v1',
3331
+ revision_set_digest: revisionSetDigest,
3332
+ revision_digests: prepared.filter(({ revision }) => revision.schema === 'lattice.todo_revision.v2')
3333
+ .map(({ revision }) => revision.revision_digest),
3334
+ }));
3335
+ await protocolStage(options, 'revision_set_source_barrier_durable');
3336
+ }
3337
+ for (const [index, staged] of stagedCutovers.entries()) {
3338
+ await publishSourceCutover(repoRoot, staged);
3339
+ await protocolStage(options, `revision_set_source_published_${index}`);
3340
+ }
3341
+ }
3342
+
3343
+ try {
3344
+ const currentManifest = await readArtifact(repoRoot, MANIFEST_REF, {
3345
+ code: 'STORE_INCONSISTENT', maxBytes: TODO_LIMITS.snapshotBytes, validate: validateTodoManifest,
3346
+ });
3347
+ if (currentManifest.manifest_digest !== store.manifest.manifest_digest) {
3348
+ fail('STORE_WRITE_CONFLICT', 'manifest_digest_changed');
3349
+ }
3350
+ for (const entry of prepared) {
3351
+ const descriptor = currentManifest.members.find(({ plan_key }) => plan_key === entry.revision.plan_key);
3352
+ if (descriptor === undefined
3353
+ || descriptor.active_plan_version !== entry.revision.predecessor.plan_version
3354
+ || descriptor.journal_head_digest !== entry.revision.predecessor.journal_head_digest) {
3355
+ fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
3356
+ }
3357
+ Object.assign(descriptor, {
3358
+ active_plan_version: entry.revision.desired_plan.plan_version,
3359
+ plan_ref: entry.refs.plan_ref,
3360
+ journal_ref: entry.refs.journal_ref,
3361
+ snapshot_ref: entry.refs.snapshot_ref,
3362
+ topology_digest: entry.revision.desired_plan.topology_digest,
3363
+ journal_head_digest: entry.genesis.event_digest,
3364
+ });
3365
+ }
3366
+ currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
3367
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
3368
+ await protocolStage(options, 'revision_set_manifest_activated');
3369
+ } catch (error) {
3370
+ if (stagedCutovers.length > 0 && error instanceof TodoStoreError) {
3371
+ let rolledBack = true;
3372
+ for (const staged of [...stagedCutovers].reverse()) {
3373
+ rolledBack = await rollbackSourceCutover(staged, barrierAbsolute, { removeBarrier: false })
3374
+ && rolledBack;
3375
+ }
3376
+ if (!rolledBack) fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'rollback_incomplete');
3377
+ await rm(barrierAbsolute, { force: true });
3378
+ await fsyncDirectory(path.dirname(barrierAbsolute));
3379
+ }
3380
+ throw error;
3381
+ }
3382
+ if (stagedCutovers.length > 0) {
3383
+ await rm(barrierAbsolute, { force: true });
3384
+ await fsyncDirectory(path.dirname(barrierAbsolute));
3385
+ await protocolStage(options, 'revision_set_source_cleanup');
3386
+ }
3387
+ await rm(transaction, { recursive: true, force: true });
3388
+ return revisionSetResult(projectId, revisionSetDigest, prepared, { recovered: recovering });
3389
+ });
3390
+ }
3391
+
3392
+ /** G5 revise primitive: topology changes only by publishing a successor version. */
3393
+ export async function createSuccessorTodoPlan(options = {}) {
3394
+ requireWriter(options.writer, 'g5-authoring');
3395
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3396
+ return withLock(repoRoot, async () => {
3397
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
3398
+ const previous = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
3399
+ if (!previous) fail('STORE_INCONSISTENT', 'plan_not_active');
3400
+ if (options.plan.predecessor_plan_digest !== previous.plan.plan_digest
3401
+ || options.plan.plan_version === previous.plan.plan_version) throw new TypeError('successor predecessor/version binding invalid');
3402
+ const plan = buildTodoPlan(options.plan);
3403
+ if (plan.project_id !== store.project_id || plan.plan_key !== previous.plan.plan_key) {
3404
+ throw new TypeError('successor identity must match active plan');
3405
+ }
3406
+ const schemaRank = new Map([
3407
+ ['lattice.todo_plan.v1', 1], ['lattice.todo_plan.v2', 2], ['lattice.todo_plan.v3', 3],
3408
+ ['lattice.todo_plan.v4', 4], ['lattice.todo_plan.v5', 5],
3409
+ ]);
3410
+ if (schemaRank.get(plan.schema) < schemaRank.get(previous.plan.schema)) {
3411
+ throw new TypeError(previous.plan.schema === 'lattice.todo_plan.v2'
3412
+ && plan.schema === 'lattice.todo_plan.v1'
3413
+ ? 'todo plan schema cannot regress from v2 to v1'
3414
+ : 'todo plan schema cannot regress');
3415
+ }
3416
+ verifyPlanNarrativeAnchors(repoRoot, plan, previous.plan);
3417
+ const migration = options.genesis.task_migration ?? [];
3418
+ const oldIds = previous.plan.tasks.map(({ task_id }) => task_id).sort();
3419
+ const migrationIds = migration.map(({ from_task_id }) => from_task_id).sort();
3420
+ if (canonicalizeTodoArtifact(oldIds) !== canonicalizeTodoArtifact(migrationIds)
3421
+ || new Set(migrationIds).size !== migrationIds.length) throw new TypeError('every predecessor task requires one migration entry');
3422
+ const newIds = new Set(plan.tasks.map(({ task_id }) => task_id));
3423
+ if (migration.some(({ to_task_id }) => to_task_id !== 'removed' && !newIds.has(to_task_id))) {
3424
+ throw new TypeError('task migration targets missing successor task');
3425
+ }
3426
+ const genesis = buildPlanGenesis(plan, {
3427
+ ...options.genesis, previous_digest: previous.journal.events.at(-1).event_digest,
3428
+ });
3429
+ const prospective = store.members.map((member) => member === previous ? { ...member, plan } : member);
3430
+ validateMergedGraph(prospective);
3431
+ const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
3432
+ const planRef = `${base}/plan.json`; const journalRef = `${base}/journal/active.jsonl`; const snapshotRef = `${base}/snapshot.json`;
3433
+ try { await lstat(path.resolve(repoRoot, planRef)); fail('STORE_WRITE_CONFLICT', 'successor_version_exists'); }
3434
+ catch (error) { if (error instanceof TodoStoreError) throw error; if (error?.code !== 'ENOENT') throw error; }
3435
+ const tasks = replay(plan, [genesis], { now: options.now ? new Date(options.now) : new Date() });
3436
+ const snapshot = snapshotFor(plan, [genesis], tasks);
3437
+ // Required order: plan, genesis journal, then manifest activation. Snapshot is
3438
+ // prepared before activation so a newly active version is immediately writable.
3439
+ await atomicWrite(path.resolve(repoRoot, planRef), canonicalLine(plan));
3440
+ await atomicWrite(path.resolve(repoRoot, journalRef), canonicalLine(genesis));
3441
+ await atomicWrite(path.resolve(repoRoot, snapshotRef), canonicalLine(snapshot));
3442
+ Object.assign(previous.descriptor, {
3443
+ active_plan_version: plan.plan_version, plan_ref: planRef, journal_ref: journalRef,
3444
+ snapshot_ref: snapshotRef, topology_digest: plan.topology_digest,
3445
+ journal_head_digest: genesis.event_digest,
3446
+ });
3447
+ store.manifest.manifest_digest = todoSelfDigest(store.manifest, 'manifest_digest');
3448
+ await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(store.manifest));
3449
+ return { plan, genesis, snapshot };
3450
+ });
3451
+ }