akm-cli 0.9.0-rc.8 → 0.9.0

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 (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +73 -88
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +141 -87
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +105 -57
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +37 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +101 -6
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1806
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1234
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -223
  424. package/dist/migrate/legacy/content-migration.js +0 -305
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -72
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -841
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -1,1806 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { createHash, randomUUID } from "node:crypto";
5
- import fs from "node:fs";
6
- import os from "node:os";
7
- import path from "node:path";
8
- import { MAX_CONFIG_FILE_BYTES, MAX_LOCAL_METADATA_BYTES, readTextFileWithLimit, writeFileAtomic, } from "../core/common.js";
9
- import { parseAndValidateConfigText, resetConfigCache, sanitizeConfigForWrite, } from "../core/config/config.js";
10
- import { backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "../core/config/config-io.js";
11
- import { ConfigError } from "../core/errors.js";
12
- import { withMaintenanceStartBarrier } from "../core/maintenance-barrier.js";
13
- import { assertNoArtifactReplacementBlockers, ensureMigrationBackupWithConfigLockHeld, fingerprintMigrationGeneration, getMigrationApplyJournalPath, getMigrationBackupDir, getMigrationBackupRoot, getMigrationRestoreJournalPath, inspectMigrationState, MIGRATION_BACKUP_VERSION, recoverInterruptedRestoreWithLocksHeld, restoreMigrationBackupWithLocksHeld, sameMigrationGeneration, verifyMigrationBackup, } from "../core/migration-backup.js";
14
- import { getConfigPath, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
15
- import { runMigrations as runStateMigrations } from "../core/state/migrations.js";
16
- import { mergeLockEntriesSync } from "../integrations/lockfile.js";
17
- import { migrateConfigSourcesToBundles, migratedLockEntries } from "../migrate/legacy/config-source-migration.js";
18
- import { runContentMigration } from "../migrate/legacy/content-migration.js";
19
- import { getLegacyWorkflowDbPath } from "../migrate/legacy/legacy-paths.js";
20
- import { importLegacyProposalsIntoState } from "../migrate/legacy/proposal-fs-import.js";
21
- import { applyTaskTargetRefMigration, planTaskTargetRefMigration } from "../migrate/legacy/task-target-ref-migration.js";
22
- import { buildCutoverRefMap, cutoverMergeCommitted, deleteWorkflowDb, loadCutoverRefMap, migratePilotTreatmentFiles, quarantineIndexDb, runThreeDbCutover, } from "../migrate/legacy/three-db-cutover.js";
23
- import { FROZEN_WORKFLOW_MIGRATIONS } from "../migrate/legacy/workflow-migrations-bodies.js";
24
- import { requestGc } from "../runtime.js";
25
- import { openDatabaseFinalizing } from "../storage/database.js";
26
- import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
27
- import { EXIT_CODES } from "./shared.js";
28
- const MANUAL_GUIDANCE = "Provide a complete operator-prepared 0.9 config with --config. AKM does not guess profile-to-engine mappings.";
29
- const APPLY_PHASE_ORDER = [
30
- "prepared",
31
- "state-converting",
32
- "state-collapsing",
33
- "state-applied",
34
- "workflow-applied",
35
- "cutover-applied",
36
- "config-applied",
37
- "tasks-prepared",
38
- "tasks-applied",
39
- "pilot-prepared",
40
- "pilot-applied",
41
- "committed",
42
- ];
43
- function isFileFingerprint(value) {
44
- if (value === null)
45
- return true;
46
- if (typeof value !== "object" || value === null || Array.isArray(value))
47
- return false;
48
- const candidate = value;
49
- return (Object.keys(candidate).sort().join(",") === "byteSize,sha256" &&
50
- Number.isSafeInteger(candidate.byteSize) &&
51
- candidate.byteSize >= 0 &&
52
- typeof candidate.sha256 === "string" &&
53
- /^[a-f0-9]{64}$/.test(candidate.sha256));
54
- }
55
- function isGenerationFingerprint(value) {
56
- if (typeof value !== "object" || value === null || Array.isArray(value))
57
- return false;
58
- const generation = value;
59
- if (Object.keys(generation).sort().join(",") !== "config,state,workflow")
60
- return false;
61
- for (const [name, expectedNullSidecars] of [
62
- ["config", true],
63
- ["state", false],
64
- ["workflow", false],
65
- ]) {
66
- const artifact = generation[name];
67
- if (typeof artifact !== "object" || artifact === null || Array.isArray(artifact))
68
- return false;
69
- const files = artifact;
70
- if (Object.keys(files).sort().join(",") !== "main,shm,wal")
71
- return false;
72
- if (!isFileFingerprint(files.main) || !isFileFingerprint(files.wal) || !isFileFingerprint(files.shm))
73
- return false;
74
- if (expectedNullSidecars && (files.wal !== null || files.shm !== null))
75
- return false;
76
- }
77
- return true;
78
- }
79
- function sameArtifactFingerprint(left, right) {
80
- return JSON.stringify(left) === JSON.stringify(right);
81
- }
82
- /**
83
- * Collapse state.db to a SINGLE FILE (DELETE journal) for the rest of the
84
- * apply. A WAL-mode state.db carries `-wal`/`-shm` sidecars that the migration
85
- * generation fingerprint tracks; a later read-only inspect (or a rolled-back
86
- * cutover transaction) mutates them, which would trip the "state changed
87
- * outside the journaled transition" rollback guard and REFUSE the fail-closed
88
- * restore. In single-file mode a rolled-back transaction leaves state.db
89
- * byte-identical, so the cutover's fail-closed rollback works. The runtime
90
- * restores WAL on its next openStateDatabase.
91
- *
92
- * The resulting journal_mode is AUTHORITATIVE, not best-effort (issue #720):
93
- * if state.db could not leave WAL mode — another PROCESS holds it open
94
- * (same-process zombie closes are prevented by openDatabaseFinalizing's
95
- * finalize-on-close guard) — the later cutover's rolled-back transaction
96
- * would mutate `-wal` and the restore would be silently refused. Fail EARLY
97
- * instead: journal.phase is still "state-converting" at the call site, so the
98
- * outer catch restores config+state+workflow from the backup with a clear
99
- * retry message.
100
- */
101
- function collapseStateDbToSingleFile(db) {
102
- const attempt = () => {
103
- try {
104
- const checkpoint = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
105
- if (checkpoint?.busy === 0)
106
- crashInMutationGapForTests("state-checkpoint");
107
- db.exec("PRAGMA journal_mode = DELETE");
108
- }
109
- catch {
110
- // Already single-file / nothing to checkpoint, or blocked — the
111
- // read-back below is the authoritative signal either way.
112
- }
113
- return String(db.prepare("PRAGMA journal_mode").get()?.journal_mode ?? "").toLowerCase();
114
- };
115
- let journalMode = attempt();
116
- if (journalMode === "wal") {
117
- // A zombie-closed sibling connection ANYWHERE in this process (a close()
118
- // with unfinalized prepare() statements outside the openDatabaseFinalizing
119
- // set) also blocks the switch until GC finalizes it. Force a collection
120
- // and retry once before concluding another PROCESS holds the database.
121
- requestGc();
122
- journalMode = attempt();
123
- }
124
- if (journalMode === "wal") {
125
- throw new ConfigError("Cannot convert state.db out of WAL mode for migration — another akm process is holding it open. " +
126
- "Close other akm processes and re-run `akm migrate apply`.", "INVALID_CONFIG_FILE");
127
- }
128
- }
129
- function hasGenerationMarker(dbPath, operationId, phase) {
130
- if (!fs.existsSync(dbPath))
131
- return false;
132
- const db = openDatabaseFinalizing(dbPath, { readonly: true });
133
- try {
134
- if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'").get()) {
135
- return false;
136
- }
137
- const rows = db
138
- .prepare("SELECT operation_id, phase FROM akm_migration_generation WHERE singleton=1 LIMIT 2")
139
- .all();
140
- return rows.length === 1 && rows[0]?.operation_id === operationId && rows[0]?.phase === phase;
141
- }
142
- finally {
143
- db.close();
144
- }
145
- }
146
- function hasGenerationMarkerFromSnapshot(dbPath, operationId, phase) {
147
- if (!fs.existsSync(dbPath))
148
- return false;
149
- const snapshot = createSqliteReadSnapshot(dbPath, "akm-migration-marker-");
150
- try {
151
- return hasGenerationMarker(snapshot.databasePath, operationId, phase);
152
- }
153
- finally {
154
- snapshot.cleanup();
155
- }
156
- }
157
- const STATE_SNAPSHOT_BUFFER_BYTES = 1024 * 1024;
158
- class MigrationSnapshotChangedError extends ConfigError {
159
- }
160
- let migrationSnapshotHookForTests;
161
- export function _setMigrationSnapshotHookForTests(hook) {
162
- migrationSnapshotHookForTests = hook;
163
- }
164
- function updateCanonicalValue(hash, type, value) {
165
- hash.update(`${Buffer.byteLength(type)}:`);
166
- hash.update(type);
167
- if (value === null) {
168
- hash.update("-1:");
169
- return;
170
- }
171
- const byteLength = typeof value === "string" ? Buffer.byteLength(value) : value.byteLength;
172
- hash.update(`${byteLength}:`);
173
- hash.update(value);
174
- }
175
- export function canonicalStateGenerationSha256(db) {
176
- const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
177
- const hash = createHash("sha256");
178
- for (const [name, query] of [
179
- ["application_id", "SELECT CAST(application_id AS TEXT) AS value FROM pragma_application_id"],
180
- ["encoding", "SELECT encoding AS value FROM pragma_encoding"],
181
- ["user_version", "SELECT CAST(user_version AS TEXT) AS value FROM pragma_user_version"],
182
- ]) {
183
- const row = db.prepare(query).get();
184
- if (!row)
185
- throw new ConfigError(`Cannot fingerprint SQLite PRAGMA ${name}.`, "INVALID_CONFIG_FILE");
186
- updateCanonicalValue(hash, "pragma-name", name);
187
- updateCanonicalValue(hash, "pragma-value", row.value);
188
- }
189
- const schemaRows = db
190
- .prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' ORDER BY type COLLATE BINARY, name COLLATE BINARY, tbl_name COLLATE BINARY, sql COLLATE BINARY")
191
- .iterate();
192
- for (const row of schemaRows) {
193
- updateCanonicalValue(hash, "schema-type", row.type);
194
- updateCanonicalValue(hash, "schema-name", row.name);
195
- updateCanonicalValue(hash, "schema-table", row.tbl_name);
196
- updateCanonicalValue(hash, "schema-sql", row.sql);
197
- }
198
- const firstTable = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' ORDER BY name COLLATE BINARY LIMIT 1");
199
- const nextTable = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' AND name COLLATE BINARY > ? ORDER BY name COLLATE BINARY LIMIT 1");
200
- let tableRow = firstTable.get();
201
- while (tableRow) {
202
- const table = tableRow.name;
203
- updateCanonicalValue(hash, "table", table);
204
- const columns = [];
205
- const columnRows = db
206
- .prepare('SELECT CAST(cid AS TEXT) AS cid, name, type, CAST("notnull" AS TEXT) AS not_null, dflt_value, CAST(pk AS TEXT) AS pk, CAST(hidden AS TEXT) AS hidden FROM pragma_table_xinfo(?) ORDER BY cid')
207
- .iterate(table);
208
- for (const column of columnRows) {
209
- columns.push(column.name);
210
- updateCanonicalValue(hash, "column-cid", column.cid);
211
- updateCanonicalValue(hash, "column-name", column.name);
212
- updateCanonicalValue(hash, "column-type", column.type);
213
- updateCanonicalValue(hash, "column-notnull", column.not_null);
214
- updateCanonicalValue(hash, "column-default", column.dflt_value);
215
- updateCanonicalValue(hash, "column-pk", column.pk);
216
- updateCanonicalValue(hash, "column-hidden", column.hidden);
217
- }
218
- const projections = [];
219
- const ordering = [];
220
- const tableFlags = db
221
- .prepare("SELECT CAST(wr AS TEXT) AS without_rowid FROM pragma_table_list WHERE schema='main' AND name=?")
222
- .get(table);
223
- if (!tableFlags) {
224
- throw new ConfigError(`Cannot determine whether SQLite table ${JSON.stringify(table)} has a rowid.`, "INVALID_CONFIG_FILE");
225
- }
226
- const hasImplicitRowid = tableFlags.without_rowid === "0";
227
- const declaredNames = new Set(columns.map((column) => column.toLowerCase()));
228
- const rowidAlias = hasImplicitRowid
229
- ? ["rowid", "_rowid_", "oid"].find((alias) => !declaredNames.has(alias))
230
- : undefined;
231
- if (hasImplicitRowid && !rowidAlias) {
232
- throw new ConfigError(`Cannot fingerprint SQLite table ${JSON.stringify(table)} because all implicit rowid aliases are shadowed.`, "INVALID_CONFIG_FILE");
233
- }
234
- if (rowidAlias) {
235
- const rowidTypeAlias = quoteIdentifier("__akm_implicit_rowid_type");
236
- const rowidValueAlias = quoteIdentifier("__akm_implicit_rowid_value");
237
- projections.push(`'integer' AS ${rowidTypeAlias}`, `CAST(${quoteIdentifier(rowidAlias)} AS TEXT) AS ${rowidValueAlias}`);
238
- ordering.push(`${rowidTypeAlias} COLLATE BINARY`, `${rowidValueAlias} COLLATE BINARY`);
239
- }
240
- for (const [index, column] of columns.entries()) {
241
- const quotedColumn = quoteIdentifier(column);
242
- const typeAlias = quoteIdentifier(`__akm_type_${index}`);
243
- const valueAlias = quoteIdentifier(`__akm_value_${index}`);
244
- projections.push(`typeof(${quotedColumn}) AS ${typeAlias}`, `CASE typeof(${quotedColumn}) WHEN 'integer' THEN CAST(${quotedColumn} AS TEXT) WHEN 'real' THEN printf('%!.17g', ${quotedColumn}) WHEN 'text' THEN CAST(${quotedColumn} AS BLOB) WHEN 'blob' THEN ${quotedColumn} ELSE NULL END AS ${valueAlias}`);
245
- ordering.push(`${typeAlias} COLLATE BINARY`, `${valueAlias} COLLATE BINARY`);
246
- }
247
- const rowStatement = db.prepare(`SELECT ${projections.join(", ")} FROM ${quoteIdentifier(table)} ORDER BY ${ordering.join(", ")}`);
248
- for (const row of rowStatement.iterate()) {
249
- updateCanonicalValue(hash, "row", "");
250
- if (rowidAlias) {
251
- const rowidType = row.__akm_implicit_rowid_type;
252
- const rowidValue = row.__akm_implicit_rowid_value;
253
- if (rowidType !== "integer" || typeof rowidValue !== "string") {
254
- throw new ConfigError("Cannot fingerprint an exact SQLite implicit rowid.", "INVALID_CONFIG_FILE");
255
- }
256
- updateCanonicalValue(hash, rowidType, rowidValue);
257
- }
258
- for (const [index] of columns.entries()) {
259
- const type = row[`__akm_type_${index}`];
260
- const value = row[`__akm_value_${index}`];
261
- if (typeof type !== "string" ||
262
- (value !== null && typeof value !== "string" && !(value instanceof Uint8Array))) {
263
- throw new ConfigError("Cannot fingerprint an unsupported SQLite value.", "INVALID_CONFIG_FILE");
264
- }
265
- updateCanonicalValue(hash, type, value);
266
- }
267
- }
268
- tableRow = nextTable.get(table);
269
- }
270
- return hash.digest("hex");
271
- }
272
- function migrationSnapshotChangedError() {
273
- return new MigrationSnapshotChangedError("A SQLite migration artifact changed while creating a private status snapshot; retry when writes are idle.", "INVALID_CONFIG_FILE");
274
- }
275
- function isSnapshotPathRaceError(error) {
276
- const code = error.code;
277
- return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "EISDIR";
278
- }
279
- function sameOpenStateSnapshotStat(left, right) {
280
- return (left.dev === right.dev &&
281
- left.ino === right.ino &&
282
- left.size === right.size &&
283
- left.mtimeNs === right.mtimeNs &&
284
- left.ctimeNs === right.ctimeNs);
285
- }
286
- function assertOpenStateSnapshotIdentity(source) {
287
- let stat;
288
- try {
289
- stat = fs.lstatSync(source.path, { bigint: true });
290
- }
291
- catch (error) {
292
- if (isSnapshotPathRaceError(error))
293
- throw migrationSnapshotChangedError();
294
- throw error;
295
- }
296
- if (!stat.isFile() || stat.dev !== source.dev || stat.ino !== source.ino)
297
- throw migrationSnapshotChangedError();
298
- }
299
- function assertStateSnapshotPathAbsent(filePath) {
300
- try {
301
- fs.lstatSync(filePath);
302
- }
303
- catch (error) {
304
- if (error.code === "ENOENT")
305
- return;
306
- throw error;
307
- }
308
- throw migrationSnapshotChangedError();
309
- }
310
- function openStateSnapshotSource(filePath, optional = false) {
311
- let pathStat;
312
- try {
313
- pathStat = fs.lstatSync(filePath, { bigint: true });
314
- }
315
- catch (error) {
316
- if (optional && error.code === "ENOENT")
317
- return undefined;
318
- if (isSnapshotPathRaceError(error))
319
- throw migrationSnapshotChangedError();
320
- throw error;
321
- }
322
- if (!pathStat.isFile()) {
323
- throw new ConfigError(`Migration snapshot source is not a regular file: ${filePath}`, "INVALID_CONFIG_FILE");
324
- }
325
- let fd;
326
- try {
327
- fd = fs.openSync(filePath, "r");
328
- }
329
- catch (error) {
330
- if (isSnapshotPathRaceError(error))
331
- throw migrationSnapshotChangedError();
332
- throw error;
333
- }
334
- try {
335
- const fdStat = fs.fstatSync(fd, { bigint: true });
336
- if (!fdStat.isFile() || fdStat.dev !== pathStat.dev || fdStat.ino !== pathStat.ino) {
337
- throw migrationSnapshotChangedError();
338
- }
339
- return { path: filePath, fd, dev: fdStat.dev, ino: fdStat.ino };
340
- }
341
- catch (error) {
342
- fs.closeSync(fd);
343
- throw error;
344
- }
345
- }
346
- function readStateSnapshotSource(source, destinationFd) {
347
- assertOpenStateSnapshotIdentity(source);
348
- const before = fs.fstatSync(source.fd, { bigint: true });
349
- const hash = createHash("sha256");
350
- const buffer = Buffer.allocUnsafe(STATE_SNAPSHOT_BUFFER_BYTES);
351
- let position = 0;
352
- while (true) {
353
- const bytesRead = fs.readSync(source.fd, buffer, 0, buffer.length, position);
354
- if (bytesRead === 0)
355
- break;
356
- hash.update(buffer.subarray(0, bytesRead));
357
- if (destinationFd !== undefined) {
358
- let written = 0;
359
- while (written < bytesRead) {
360
- written += fs.writeSync(destinationFd, buffer, written, bytesRead - written);
361
- }
362
- }
363
- position += bytesRead;
364
- }
365
- const after = fs.fstatSync(source.fd, { bigint: true });
366
- assertOpenStateSnapshotIdentity(source);
367
- if (!sameOpenStateSnapshotStat(before, after) || BigInt(position) !== after.size) {
368
- throw migrationSnapshotChangedError();
369
- }
370
- return { bytes: position, sha256: hash.digest("hex") };
371
- }
372
- function fingerprintStateSnapshotSources(main, wal, walPath) {
373
- const mainFingerprint = readStateSnapshotSource(main);
374
- const walFingerprint = wal ? readStateSnapshotSource(wal) : null;
375
- if (!wal)
376
- assertStateSnapshotPathAbsent(walPath);
377
- return { main: mainFingerprint, wal: walFingerprint };
378
- }
379
- function copyStateSnapshotSource(source, destination) {
380
- const destinationFd = fs.openSync(destination, "wx", 0o600);
381
- try {
382
- const fingerprint = readStateSnapshotSource(source, destinationFd);
383
- fs.fsyncSync(destinationFd);
384
- return fingerprint;
385
- }
386
- finally {
387
- fs.closeSync(destinationFd);
388
- }
389
- }
390
- function copyStateSnapshotSources(main, wal, walPath, databasePath) {
391
- const mainFingerprint = copyStateSnapshotSource(main, databasePath);
392
- const walFingerprint = wal ? copyStateSnapshotSource(wal, `${databasePath}-wal`) : null;
393
- if (!wal)
394
- assertStateSnapshotPathAbsent(walPath);
395
- return { main: mainFingerprint, wal: walFingerprint };
396
- }
397
- function sameStateSnapshotFingerprint(left, right) {
398
- return left === null || right === null ? left === right : left.bytes === right.bytes && left.sha256 === right.sha256;
399
- }
400
- function sameStateSnapshotGeneration(left, right) {
401
- return sameStateSnapshotFingerprint(left.main, right.main) && sameStateSnapshotFingerprint(left.wal, right.wal);
402
- }
403
- function createSqliteReadSnapshot(sourcePath, tempPrefix, applyPhase) {
404
- const walPath = `${sourcePath}-wal`;
405
- const main = openStateSnapshotSource(sourcePath);
406
- if (!main)
407
- throw new ConfigError(`SQLite database not found: ${sourcePath}`, "INVALID_CONFIG_FILE");
408
- let wal;
409
- let snapshotDir;
410
- let mainOpen = true;
411
- let walOpen = false;
412
- try {
413
- wal = openStateSnapshotSource(walPath, true);
414
- walOpen = wal !== undefined;
415
- snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), tempPrefix));
416
- fs.chmodSync(snapshotDir, 0o700);
417
- const databasePath = path.join(snapshotDir, "state.db");
418
- const before = fingerprintStateSnapshotSources(main, wal, walPath);
419
- migrationSnapshotHookForTests?.({ sourcePath, ...(applyPhase ? { applyPhase } : {}) });
420
- const copied = copyStateSnapshotSources(main, wal, walPath, databasePath);
421
- const after = fingerprintStateSnapshotSources(main, wal, walPath);
422
- if (!sameStateSnapshotGeneration(before, copied) || !sameStateSnapshotGeneration(before, after)) {
423
- throw migrationSnapshotChangedError();
424
- }
425
- const completedDir = snapshotDir;
426
- fs.closeSync(main.fd);
427
- mainOpen = false;
428
- if (wal) {
429
- fs.closeSync(wal.fd);
430
- walOpen = false;
431
- }
432
- snapshotDir = undefined;
433
- return {
434
- databasePath,
435
- cleanup: () => fs.rmSync(completedDir, { recursive: true, force: true }),
436
- };
437
- }
438
- finally {
439
- if (mainOpen) {
440
- try {
441
- fs.closeSync(main.fd);
442
- }
443
- catch {
444
- // The original snapshot failure remains authoritative.
445
- }
446
- }
447
- if (walOpen && wal) {
448
- try {
449
- fs.closeSync(wal.fd);
450
- }
451
- catch {
452
- // The original snapshot failure remains authoritative.
453
- }
454
- }
455
- if (snapshotDir)
456
- fs.rmSync(snapshotDir, { recursive: true, force: true });
457
- }
458
- }
459
- function bindStateConvertingMarker(db, operationId) {
460
- db.exec(`
461
- CREATE TABLE IF NOT EXISTS akm_migration_generation (
462
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
463
- operation_id TEXT NOT NULL,
464
- phase TEXT NOT NULL,
465
- generation_sha256 TEXT
466
- )
467
- `);
468
- const columns = new Set(db.prepare("PRAGMA table_info(akm_migration_generation)").all().map((column) => column.name));
469
- if (!columns.has("generation_sha256")) {
470
- db.exec("ALTER TABLE akm_migration_generation ADD COLUMN generation_sha256 TEXT");
471
- }
472
- const generationSha256 = canonicalStateGenerationSha256(db);
473
- db.prepare("INSERT INTO akm_migration_generation(singleton, operation_id, phase, generation_sha256) VALUES (1, ?, 'state-converting', ?) ON CONFLICT(singleton) DO UPDATE SET operation_id=excluded.operation_id, phase=excluded.phase, generation_sha256=excluded.generation_sha256").run(operationId, generationSha256);
474
- }
475
- function readBoundStateGenerationMarker(db, operationId) {
476
- if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'").get()) {
477
- return undefined;
478
- }
479
- const columns = new Set(db.prepare("PRAGMA table_info(akm_migration_generation)").all().map((column) => column.name));
480
- if (!columns.has("generation_sha256"))
481
- return undefined;
482
- const marker = db
483
- .prepare("SELECT operation_id, phase, generation_sha256 FROM akm_migration_generation WHERE singleton=1")
484
- .get();
485
- const generationSha256 = marker?.generation_sha256;
486
- if (!marker || marker.operation_id !== operationId || !generationSha256 || !/^[a-f0-9]{64}$/.test(generationSha256)) {
487
- return undefined;
488
- }
489
- if (marker.phase !== "state-converting" && marker.phase !== "state-applied")
490
- return undefined;
491
- if (canonicalStateGenerationSha256(db) !== generationSha256) {
492
- throw new ConfigError("state.db no longer matches the exact logical generation bound to its state-converting marker.", "INVALID_CONFIG_FILE");
493
- }
494
- return { phase: marker.phase, generationSha256 };
495
- }
496
- function readBoundStateGenerationMarkerAtPath(statePath, operationId) {
497
- if (!fs.existsSync(statePath))
498
- return undefined;
499
- const db = openDatabaseFinalizing(statePath, { readonly: true, create: false });
500
- try {
501
- return readBoundStateGenerationMarker(db, operationId);
502
- }
503
- finally {
504
- db.close();
505
- }
506
- }
507
- function readBoundStateGenerationMarkerFromDisk(operationId) {
508
- const statePath = getStateDbPathInDataDir();
509
- if (!fs.existsSync(statePath))
510
- return undefined;
511
- const snapshot = createSqliteReadSnapshot(statePath, "akm-migration-state-");
512
- try {
513
- return readBoundStateGenerationMarkerAtPath(snapshot.databasePath, operationId);
514
- }
515
- finally {
516
- snapshot.cleanup();
517
- }
518
- }
519
- function cutoverMergeCommittedFromSnapshot(operationId) {
520
- const statePath = getStateDbPathInDataDir();
521
- if (!fs.existsSync(statePath))
522
- return false;
523
- const snapshot = createSqliteReadSnapshot(statePath, "akm-migration-state-");
524
- try {
525
- return cutoverMergeCommitted(snapshot.databasePath, operationId);
526
- }
527
- finally {
528
- snapshot.cleanup();
529
- }
530
- }
531
- function createMigrationInspectionSnapshots(applyPhase) {
532
- const missingRoot = fs.mkdtempSync(path.join(os.tmpdir(), "akm-migration-inspect-"));
533
- fs.chmodSync(missingRoot, 0o700);
534
- const snapshots = [];
535
- const capture = (sourcePath, prefix, name) => {
536
- if (!fs.existsSync(sourcePath))
537
- return path.join(missingRoot, name);
538
- const snapshot = createSqliteReadSnapshot(sourcePath, prefix, applyPhase);
539
- snapshots.push(snapshot);
540
- return snapshot.databasePath;
541
- };
542
- try {
543
- const paths = {
544
- stateDbPath: capture(getStateDbPathInDataDir(), "akm-migration-state-", "state.db"),
545
- workflowDbPath: capture(getLegacyWorkflowDbPath(), "akm-migration-workflow-", "workflow.db"),
546
- indexDbPath: capture(getDbPath(), "akm-migration-index-", "index.db"),
547
- };
548
- return {
549
- paths,
550
- cleanup: () => {
551
- for (const snapshot of snapshots)
552
- snapshot.cleanup();
553
- fs.rmSync(missingRoot, { recursive: true, force: true });
554
- },
555
- };
556
- }
557
- catch (error) {
558
- for (const snapshot of snapshots)
559
- snapshot.cleanup();
560
- fs.rmSync(missingRoot, { recursive: true, force: true });
561
- throw error;
562
- }
563
- }
564
- function captureMigrationInspection(applyPhase) {
565
- const before = fingerprintMigrationGeneration();
566
- const snapshots = createMigrationInspectionSnapshots(applyPhase);
567
- try {
568
- const after = fingerprintMigrationGeneration();
569
- if (!sameMigrationGeneration(before, after))
570
- throw migrationSnapshotChangedError();
571
- return {
572
- paths: snapshots.paths,
573
- artifacts: inspectMigrationState(snapshots.paths),
574
- generation: after,
575
- cleanup: snapshots.cleanup,
576
- };
577
- }
578
- catch (error) {
579
- snapshots.cleanup();
580
- throw error;
581
- }
582
- }
583
- function inspectMigrationStateFromSnapshots() {
584
- const capture = captureMigrationInspection();
585
- try {
586
- return capture.artifacts;
587
- }
588
- finally {
589
- capture.cleanup();
590
- }
591
- }
592
- class MigrationPreflightGenerationError extends ConfigError {
593
- }
594
- let applyPreflightHookForTests;
595
- export function _setApplyPreflightHookForTests(hook) {
596
- applyPreflightHookForTests = hook;
597
- }
598
- function inspectExactApplyJournalGeneration(journal) {
599
- if (journal.phase !== "state-applied" && journal.phase !== "workflow-applied") {
600
- throw new ConfigError(`Cannot preflight migration journal phase ${journal.phase}.`, "INVALID_CONFIG_FILE");
601
- }
602
- applyPreflightHookForTests?.(journal.phase);
603
- const capture = captureMigrationInspection(journal.phase);
604
- try {
605
- if (!sameMigrationGeneration(capture.generation, journal.generation)) {
606
- throw new MigrationPreflightGenerationError(`Migration apply journal phase ${journal.phase} changed before its next mutation; the external generation was preserved.`, "INVALID_CONFIG_FILE");
607
- }
608
- return capture.artifacts;
609
- }
610
- finally {
611
- capture.cleanup();
612
- }
613
- }
614
- function readSingleFileBoundStateMarker(journal, stateSnapshotPath) {
615
- const statePath = stateSnapshotPath ?? getStateDbPathInDataDir();
616
- if (!fs.existsSync(statePath) || stateDatabaseHeaderUsesWal(statePath))
617
- return undefined;
618
- return stateSnapshotPath
619
- ? readBoundStateGenerationMarkerAtPath(statePath, journal.operationId)
620
- : readBoundStateGenerationMarkerFromDisk(journal.operationId);
621
- }
622
- function advanceBoundStateMarker(db, operationId) {
623
- const marker = readBoundStateGenerationMarker(db, operationId);
624
- if (!marker) {
625
- throw new ConfigError("state.db lacks its exact marker-bound conversion generation.", "INVALID_CONFIG_FILE");
626
- }
627
- db.prepare("UPDATE akm_migration_generation SET phase='state-applied' WHERE singleton=1 AND operation_id=?").run(operationId);
628
- }
629
- function generationFromBackup(manifest) {
630
- const fingerprint = (name) => {
631
- const artifact = manifest.artifacts[name];
632
- return artifact.present ? { byteSize: artifact.byteSize, sha256: artifact.sha256 } : null;
633
- };
634
- return {
635
- config: { main: fingerprint("config.json"), wal: null, shm: null },
636
- state: { main: fingerprint("state.db"), wal: null, shm: null },
637
- workflow: { main: fingerprint("workflow.db"), wal: null, shm: null },
638
- };
639
- }
640
- function detectAdjacentGeneration(journal, manifest, live, current, workflowDbPath) {
641
- if (journal.phase === "rollback-prepared") {
642
- return {
643
- rollbackCompleted: sameMigrationGeneration(current, generationFromBackup(manifest)),
644
- };
645
- }
646
- const unchanged = (...names) => names.every((name) => sameArtifactFingerprint(journal.generation[name], current[name]));
647
- if (journal.phase === "state-applied" &&
648
- unchanged("config", "state") &&
649
- hasGenerationMarker(workflowDbPath, journal.operationId, "workflow-applied")) {
650
- return {
651
- adjacent: {
652
- phase: "workflow-applied",
653
- complete: live.workflow.status === "current" || live.workflow.status === "missing",
654
- generation: current,
655
- },
656
- };
657
- }
658
- const expectedTarget = `${JSON.stringify(journal.targetConfig, null, 2)}\n`;
659
- // Chunk 8, WI-8.2: config is written in the phase AFTER the cutover, so a crash
660
- // in the config mutation gap leaves the journal at `cutover-applied` with the
661
- // config already on disk. state (merged) + workflow (deleted) are unchanged
662
- // since the cutover-applied advance — detect the config-applied adjacent.
663
- if ((journal.phase === "workflow-applied" || journal.phase === "cutover-applied") &&
664
- unchanged("state", "workflow") &&
665
- fs.existsSync(getConfigPath()) &&
666
- readTextFileWithLimit(getConfigPath(), MAX_CONFIG_FILE_BYTES, "Config file") === expectedTarget) {
667
- return {
668
- adjacent: { phase: "config-applied", complete: true, generation: current },
669
- };
670
- }
671
- return {};
672
- }
673
- function assertRollbackTransitionAllowed(journal, current) {
674
- const unchanged = journal.phase === "prepared"
675
- ? ["config", "workflow"]
676
- : journal.phase === "state-converting"
677
- ? ["config", "workflow"]
678
- : journal.phase === "state-applied"
679
- ? ["config", "state"]
680
- : journal.phase === "workflow-applied" || journal.phase === "cutover-applied"
681
- ? // config is applied in the phase after the cutover, so a rollback from
682
- // either only needs state + workflow unchanged (workflow=deleted is
683
- // recorded in the journal's own generation and compares equal).
684
- ["state", "workflow"]
685
- : ["config", "state", "workflow"];
686
- for (const name of unchanged) {
687
- if (!sameArtifactFingerprint(journal.generation[name], current[name])) {
688
- if (name === "state" &&
689
- (journal.phase === "state-applied" || journal.phase === "workflow-applied") &&
690
- readSingleFileBoundStateMarker(journal)?.phase === "state-applied") {
691
- // SQLite rollback preserves the logical generation but may rewrite
692
- // physical pages. The operation-bound canonical digest covers the full
693
- // schema and every row, so it safely authenticates that rollback.
694
- continue;
695
- }
696
- throw new ConfigError(`Refusing migration rollback because ${name} changed outside the journaled ${journal.phase} transition.`, "INVALID_CONFIG_FILE");
697
- }
698
- }
699
- }
700
- function sameArtifactState(actual, expected) {
701
- return (actual.status === expected.status &&
702
- JSON.stringify(actual.migrationIds ?? []) === JSON.stringify(expected.migrationIds ?? []) &&
703
- JSON.stringify(actual.migrationChecksums ?? []) === JSON.stringify(expected.migrationChecksums ?? []));
704
- }
705
- function configMatchesBytes(expectedSize, expectedHash) {
706
- const configPath = getConfigPath();
707
- if (!fs.existsSync(configPath) || expectedHash === null)
708
- return false;
709
- const text = readTextFileWithLimit(configPath, MAX_CONFIG_FILE_BYTES, "Config file");
710
- return (fs.statSync(configPath).size === expectedSize &&
711
- createHash("sha256").update(Buffer.from(text)).digest("hex") === expectedHash);
712
- }
713
- function validateApplyPhase(journal, manifest, inspectionPaths, inspectedState) {
714
- const live = inspectedState ?? inspectMigrationState(inspectionPaths);
715
- const stateOriginal = sameArtifactState(live.state, manifest.artifacts["state.db"]);
716
- const workflowOriginal = sameArtifactState(live.workflow, manifest.artifacts["workflow.db"]);
717
- const configOriginal = manifest.artifacts["config.json"].present
718
- ? configMatchesBytes(manifest.artifacts["config.json"].byteSize, manifest.artifacts["config.json"].sha256)
719
- : live.config.status === "missing";
720
- const stateApplied = manifest.artifacts["state.db"].present
721
- ? live.state.status === "current"
722
- : live.state.status === "missing";
723
- const workflowApplied = manifest.artifacts["workflow.db"].present
724
- ? live.workflow.status === "current"
725
- : live.workflow.status === "missing";
726
- // Chunk 8, WI-8.2: at/after the cutover, workflow.db is DELETED (its rows are
727
- // merged into state.db). A backed-up-present workflow.db that is now missing is
728
- // the intended post-cutover terminal state, not a failure.
729
- const workflowDeleted = manifest.artifacts["workflow.db"].present && live.workflow.status === "missing";
730
- const workflowFinal = workflowApplied || workflowDeleted;
731
- const expectedTarget = `${JSON.stringify(journal.targetConfig, null, 2)}\n`;
732
- const configApplied = live.config.status === "current" &&
733
- fs.existsSync(getConfigPath()) &&
734
- readTextFileWithLimit(getConfigPath(), MAX_CONFIG_FILE_BYTES, "Config file") === expectedTarget;
735
- const reachable = journal.phase === "rollback-prepared"
736
- ? true
737
- : journal.phase === "prepared"
738
- ? configOriginal && workflowOriginal && (stateOriginal || stateApplied)
739
- : journal.phase === "state-converting" || journal.phase === "state-collapsing"
740
- ? configOriginal && workflowOriginal && (stateOriginal || stateApplied)
741
- : journal.phase === "state-applied"
742
- ? stateApplied && configOriginal && (workflowOriginal || workflowApplied)
743
- : journal.phase === "workflow-applied"
744
- ? stateApplied && workflowApplied && (configOriginal || configApplied)
745
- : journal.phase === "cutover-applied"
746
- ? stateApplied && workflowFinal && (configOriginal || configApplied)
747
- : // config-applied / tasks-* / pilot-* / committed
748
- stateApplied && workflowFinal && configApplied;
749
- if (!reachable) {
750
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match a reachable config/state/workflow artifact state.`, "INVALID_CONFIG_FILE");
751
- }
752
- return live;
753
- }
754
- function postCutoverArtifacts(journal, manifest, current, paths, artifacts) {
755
- assertPostCutoverWorkflowAuthenticated(journal, current);
756
- if (!cutoverMergeCommitted(paths.stateDbPath, journal.operationId)) {
757
- throw new ConfigError(`Migration apply journal phase ${journal.phase} lacks its operation-bound cutover marker.`, "INVALID_CONFIG_FILE");
758
- }
759
- return validateApplyPhase(journal, manifest, paths, artifacts);
760
- }
761
- function readApplyJournalMetadata() {
762
- const journalPath = getMigrationApplyJournalPath();
763
- if (!fs.existsSync(journalPath))
764
- return {};
765
- let journal;
766
- try {
767
- const value = JSON.parse(readTextFileWithLimit(journalPath, MAX_LOCAL_METADATA_BYTES, "Migration apply journal"));
768
- const phases = [
769
- "prepared",
770
- "state-converting",
771
- "state-collapsing",
772
- "state-applied",
773
- "workflow-applied",
774
- "cutover-applied",
775
- "config-applied",
776
- "tasks-prepared",
777
- "tasks-applied",
778
- "pilot-prepared",
779
- "pilot-applied",
780
- "rollback-prepared",
781
- "committed",
782
- ];
783
- if (typeof value !== "object" ||
784
- value === null ||
785
- Array.isArray(value) ||
786
- Object.keys(value).sort().join(",") !==
787
- [
788
- "backupPath",
789
- "backupRunId",
790
- "formatVersion",
791
- "generation",
792
- "installationId",
793
- "operationId",
794
- "phase",
795
- "targetConfig",
796
- "version",
797
- ]
798
- .sort()
799
- .join(",")) {
800
- return { error: `Invalid migration apply journal at ${journalPath}.` };
801
- }
802
- const candidate = value;
803
- if (candidate.formatVersion !== 2 ||
804
- candidate.version !== MIGRATION_BACKUP_VERSION ||
805
- typeof candidate.operationId !== "string" ||
806
- !/^[A-Za-z0-9._-]+$/.test(candidate.operationId) ||
807
- candidate.installationId !== path.basename(getMigrationBackupRoot()) ||
808
- !isGenerationFingerprint(candidate.generation) ||
809
- typeof candidate.backupRunId !== "string" ||
810
- !/^[A-Za-z0-9._-]+$/.test(candidate.backupRunId) ||
811
- candidate.backupPath !== getMigrationBackupDir(candidate.backupRunId) ||
812
- !candidate.targetConfig ||
813
- typeof candidate.targetConfig !== "object" ||
814
- Array.isArray(candidate.targetConfig) ||
815
- !phases.includes(candidate.phase)) {
816
- return { error: `Invalid or foreign migration apply journal at ${journalPath}.` };
817
- }
818
- journal = candidate;
819
- }
820
- catch (error) {
821
- return {
822
- error: `Unreadable migration apply journal at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`,
823
- };
824
- }
825
- try {
826
- const backupStat = fs.lstatSync(journal.backupPath);
827
- if (backupStat.isSymbolicLink() ||
828
- !backupStat.isDirectory() ||
829
- fs.realpathSync(path.dirname(journal.backupPath)) !== fs.realpathSync(getMigrationBackupRoot())) {
830
- throw new ConfigError(`Migration apply journal backup is not a canonical installation run directory.`, "INVALID_CONFIG_FILE");
831
- }
832
- const config = parseAndValidateConfigText(JSON.stringify(journal.targetConfig), journalPath);
833
- const manifest = verifyMigrationBackup(journal.backupPath);
834
- if (manifest.runId !== journal.backupRunId || manifest.installationId !== journal.installationId) {
835
- throw new ConfigError(`Migration apply journal backup provenance does not match its manifest.`, "INVALID_CONFIG_FILE");
836
- }
837
- return { journal, config, manifest };
838
- }
839
- catch (error) {
840
- return {
841
- journal,
842
- error: `Unreadable migration apply journal at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`,
843
- };
844
- }
845
- }
846
- function journalArtifactState(artifact, status = artifact.status) {
847
- return {
848
- status,
849
- ...(artifact.migrationIds ? { migrationIds: artifact.migrationIds } : {}),
850
- ...(artifact.migrationChecksums ? { migrationChecksums: artifact.migrationChecksums } : {}),
851
- };
852
- }
853
- function stateBeforeCompatibilityConversion(journal, manifest) {
854
- const workflowBackup = manifest.artifacts["workflow.db"];
855
- const indexBackup = manifest.artifacts["index.db"];
856
- return {
857
- config: journalArtifactState(manifest.artifacts["config.json"]),
858
- state: { status: manifest.artifacts["state.db"].present ? "current" : "missing" },
859
- workflow: journal.phase === "workflow-applied" && workflowBackup.present
860
- ? { status: "current" }
861
- : journalArtifactState(workflowBackup),
862
- index: indexBackup ? journalArtifactState(indexBackup) : { status: "missing" },
863
- };
864
- }
865
- function isPreConversionCompatiblePhase(phase) {
866
- return phase === "state-applied" || phase === "workflow-applied";
867
- }
868
- function isPostCutoverPhase(phase) {
869
- return [
870
- "cutover-applied",
871
- "config-applied",
872
- "tasks-prepared",
873
- "tasks-applied",
874
- "pilot-prepared",
875
- "pilot-applied",
876
- "committed",
877
- ].includes(phase);
878
- }
879
- function isAuthenticatedWorkflowAdjacent(journal, current, workflowSnapshotPath) {
880
- if (journal.phase !== "state-applied")
881
- return false;
882
- const markerPresent = workflowSnapshotPath
883
- ? hasGenerationMarker(workflowSnapshotPath, journal.operationId, "workflow-applied")
884
- : hasGenerationMarkerFromSnapshot(getLegacyWorkflowDbPath(), journal.operationId, "workflow-applied");
885
- return (sameArtifactFingerprint(journal.generation.config, current.config) &&
886
- sameArtifactFingerprint(journal.generation.state, current.state) &&
887
- markerPresent);
888
- }
889
- function isAuthenticatedCutoverAdjacent(journal, current, stateSnapshotPath) {
890
- return (journal.phase === "workflow-applied" &&
891
- sameArtifactFingerprint(journal.generation.config, current.config) &&
892
- workflowArtifactIsDeletionSubset(journal.generation, current) &&
893
- (stateSnapshotPath
894
- ? cutoverMergeCommitted(stateSnapshotPath, journal.operationId)
895
- : cutoverMergeCommittedFromSnapshot(journal.operationId)));
896
- }
897
- function workflowArtifactIsDeletionSubset(expected, current) {
898
- return ["main", "wal", "shm"].every((component) => {
899
- const actual = current.workflow[component];
900
- return actual === null || JSON.stringify(actual) === JSON.stringify(expected.workflow[component]);
901
- });
902
- }
903
- function assertPostCutoverWorkflowAuthenticated(journal, current) {
904
- if (workflowArtifactIsDeletionSubset(journal.generation, current))
905
- return;
906
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not authorize the live workflow.db generation.`, "INVALID_CONFIG_FILE");
907
- }
908
- function readApplyJournal() {
909
- const metadata = readApplyJournalMetadata();
910
- if (metadata.error || !metadata.journal || !metadata.config || !metadata.manifest)
911
- return metadata;
912
- const { journal, config, manifest } = metadata;
913
- let capture;
914
- try {
915
- capture = captureMigrationInspection();
916
- }
917
- catch (error) {
918
- return {
919
- journal,
920
- config,
921
- error: `Unreadable migration apply journal at ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`,
922
- };
923
- }
924
- const rawGeneration = capture.generation;
925
- const inspectedArtifacts = capture.artifacts;
926
- try {
927
- if (isPostCutoverPhase(journal.phase)) {
928
- if (isTaskOnlyRepair(manifest)) {
929
- const artifacts = validateApplyPhase(journal, manifest, capture.paths, inspectedArtifacts);
930
- if (!sameMigrationGeneration(rawGeneration, journal.generation)) {
931
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
932
- }
933
- return { journal, config, artifacts };
934
- }
935
- return {
936
- journal,
937
- config,
938
- artifacts: postCutoverArtifacts(journal, manifest, rawGeneration, capture.paths, inspectedArtifacts),
939
- };
940
- }
941
- if (!sameMigrationGeneration(rawGeneration, journal.generation) &&
942
- isAuthenticatedCutoverAdjacent(journal, rawGeneration, capture.paths.stateDbPath)) {
943
- const artifacts = postCutoverArtifacts({ ...journal, phase: "cutover-applied" }, manifest, rawGeneration, capture.paths, inspectedArtifacts);
944
- return {
945
- journal,
946
- config,
947
- artifacts,
948
- adjacent: {
949
- phase: "cutover-applied",
950
- complete: true,
951
- generation: rawGeneration,
952
- },
953
- };
954
- }
955
- if (isPreConversionCompatiblePhase(journal.phase)) {
956
- const stateMarker = readSingleFileBoundStateMarker(journal, capture.paths.stateDbPath);
957
- if (stateMarker?.phase === "state-applied") {
958
- if (sameMigrationGeneration(rawGeneration, journal.generation)) {
959
- return { journal, config, artifacts: inspectedArtifacts };
960
- }
961
- if (isAuthenticatedWorkflowAdjacent(journal, rawGeneration, capture.paths.workflowDbPath)) {
962
- return {
963
- journal,
964
- config,
965
- artifacts: inspectedArtifacts,
966
- adjacent: {
967
- phase: "workflow-applied",
968
- complete: true,
969
- generation: rawGeneration,
970
- },
971
- };
972
- }
973
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
974
- }
975
- }
976
- if (isPreConversionCompatiblePhase(journal.phase)) {
977
- if (sameMigrationGeneration(rawGeneration, journal.generation)) {
978
- return {
979
- journal,
980
- config,
981
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
982
- requiresStateConversion: true,
983
- };
984
- }
985
- if (isAuthenticatedWorkflowAdjacent(journal, rawGeneration, capture.paths.workflowDbPath)) {
986
- return {
987
- journal,
988
- config,
989
- artifacts: stateBeforeCompatibilityConversion({ ...journal, phase: "workflow-applied" }, manifest),
990
- requiresStateConversion: true,
991
- adjacent: {
992
- phase: "workflow-applied",
993
- complete: true,
994
- generation: rawGeneration,
995
- },
996
- };
997
- }
998
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
999
- }
1000
- if (journal.phase === "state-converting") {
1001
- if (sameMigrationGeneration(rawGeneration, journal.generation)) {
1002
- return {
1003
- journal,
1004
- config,
1005
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1006
- requiresStateConversion: true,
1007
- };
1008
- }
1009
- const marker = readBoundStateGenerationMarkerAtPath(capture.paths.stateDbPath, journal.operationId);
1010
- if (marker?.phase === "state-converting") {
1011
- return {
1012
- journal,
1013
- config,
1014
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1015
- requiresStateConversion: true,
1016
- adjacent: {
1017
- phase: "state-collapsing",
1018
- complete: true,
1019
- generation: rawGeneration,
1020
- },
1021
- };
1022
- }
1023
- throw new ConfigError("Migration apply journal phase state-converting does not match its exact marker-bound generation.", "INVALID_CONFIG_FILE");
1024
- }
1025
- if (journal.phase === "state-collapsing") {
1026
- if (sameMigrationGeneration(rawGeneration, journal.generation)) {
1027
- return {
1028
- journal,
1029
- config,
1030
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1031
- requiresStateConversion: true,
1032
- };
1033
- }
1034
- const marker = readBoundStateGenerationMarkerAtPath(capture.paths.stateDbPath, journal.operationId);
1035
- if (marker) {
1036
- return {
1037
- journal,
1038
- config,
1039
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1040
- requiresStateConversion: true,
1041
- adjacent: {
1042
- phase: marker.phase === "state-applied" ? "state-applied" : "state-collapsing",
1043
- complete: marker.phase === "state-applied",
1044
- generation: rawGeneration,
1045
- },
1046
- };
1047
- }
1048
- throw new ConfigError("Migration apply journal phase state-collapsing does not match its exact marker-bound generation.", "INVALID_CONFIG_FILE");
1049
- }
1050
- validateApplyPhase(journal, manifest, capture.paths, inspectedArtifacts);
1051
- if (!sameMigrationGeneration(rawGeneration, journal.generation)) {
1052
- const adjacent = detectAdjacentGeneration(journal, manifest, inspectedArtifacts, rawGeneration, capture.paths.workflowDbPath);
1053
- if (adjacent.adjacent || adjacent.rollbackCompleted) {
1054
- return { journal, config, artifacts: inspectedArtifacts, ...adjacent };
1055
- }
1056
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1057
- }
1058
- return { journal, config, artifacts: inspectedArtifacts };
1059
- }
1060
- catch (error) {
1061
- return {
1062
- journal,
1063
- config,
1064
- ...(isPreConversionCompatiblePhase(journal.phase) ||
1065
- journal.phase === "state-converting" ||
1066
- journal.phase === "state-collapsing"
1067
- ? {
1068
- artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1069
- requiresStateConversion: true,
1070
- }
1071
- : { artifacts: inspectedArtifacts }),
1072
- error: `Unreadable migration apply journal at ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`,
1073
- };
1074
- }
1075
- finally {
1076
- capture.cleanup();
1077
- }
1078
- }
1079
- function authenticatePreConversionJournalForApply() {
1080
- const metadata = readApplyJournalMetadata();
1081
- if (metadata.error)
1082
- throw new ConfigError(metadata.error, "INVALID_CONFIG_FILE");
1083
- const journal = metadata.journal;
1084
- if (!journal || !isPreConversionCompatiblePhase(journal.phase))
1085
- return;
1086
- const current = fingerprintMigrationGeneration();
1087
- if (isAuthenticatedCutoverAdjacent(journal, current))
1088
- return;
1089
- if (readSingleFileBoundStateMarker(journal)?.phase === "state-applied") {
1090
- if (sameMigrationGeneration(current, journal.generation) ||
1091
- isAuthenticatedWorkflowAdjacent(journal, current) ||
1092
- isAuthenticatedCutoverAdjacent(journal, current)) {
1093
- return;
1094
- }
1095
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1096
- }
1097
- if (!sameMigrationGeneration(current, journal.generation) &&
1098
- !isAuthenticatedWorkflowAdjacent(journal, current) &&
1099
- !isAuthenticatedCutoverAdjacent(journal, current)) {
1100
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1101
- }
1102
- }
1103
- function preparePreConversionJournalForApply() {
1104
- const metadata = readApplyJournalMetadata();
1105
- if (metadata.error)
1106
- throw new ConfigError(metadata.error, "INVALID_CONFIG_FILE");
1107
- const journal = metadata.journal;
1108
- if (!journal || !isPreConversionCompatiblePhase(journal.phase))
1109
- return;
1110
- let current = fingerprintMigrationGeneration();
1111
- if (isAuthenticatedCutoverAdjacent(journal, current))
1112
- return;
1113
- if (readSingleFileBoundStateMarker(journal)?.phase === "state-applied")
1114
- return;
1115
- if (!sameMigrationGeneration(current, journal.generation)) {
1116
- if (!isAuthenticatedWorkflowAdjacent(journal, current)) {
1117
- throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1118
- }
1119
- current = fingerprintMigrationGeneration();
1120
- }
1121
- journal.phase = "state-converting";
1122
- journal.generation = current;
1123
- writeApplyJournal(journal);
1124
- }
1125
- function writeApplyJournal(journal) {
1126
- fs.mkdirSync(path.dirname(getMigrationApplyJournalPath()), { recursive: true, mode: 0o700 });
1127
- const serialized = `${JSON.stringify(journal, null, 2)}\n`;
1128
- const byteLength = Buffer.byteLength(serialized, "utf8");
1129
- if (byteLength > MAX_LOCAL_METADATA_BYTES) {
1130
- throw new ConfigError(`Migration apply journal would exceed the ${MAX_LOCAL_METADATA_BYTES}-byte metadata limit (${byteLength} bytes).`, "INVALID_CONFIG_FILE");
1131
- }
1132
- writeFileAtomic(getMigrationApplyJournalPath(), serialized, 0o600);
1133
- }
1134
- function advanceApplyJournal(journal, phase) {
1135
- if (APPLY_PHASE_ORDER.indexOf(phase) > APPLY_PHASE_ORDER.indexOf(journal.phase))
1136
- journal.phase = phase;
1137
- journal.generation = fingerprintMigrationGeneration();
1138
- writeApplyJournal(journal);
1139
- }
1140
- function clearApplyJournal() {
1141
- fs.rmSync(getMigrationApplyJournalPath(), { force: true });
1142
- try {
1143
- const fd = fs.openSync(path.dirname(getMigrationApplyJournalPath()), "r");
1144
- try {
1145
- fs.fsyncSync(fd);
1146
- }
1147
- finally {
1148
- fs.closeSync(fd);
1149
- }
1150
- }
1151
- catch {
1152
- // Directory fsync is not available on every supported filesystem.
1153
- }
1154
- }
1155
- function crashAfterForTests(phase) {
1156
- if (process.env.AKM_TEST_MIGRATION_CRASH_AFTER === phase)
1157
- process.kill(process.pid, "SIGKILL");
1158
- }
1159
- function crashInMutationGapForTests(phase) {
1160
- if (process.env.AKM_TEST_MIGRATION_CRASH_GAP === phase)
1161
- process.kill(process.pid, "SIGKILL");
1162
- }
1163
- function isForwardRecoveryPhase(phase) {
1164
- return [
1165
- "state-collapsing",
1166
- "cutover-applied",
1167
- "config-applied",
1168
- "tasks-prepared",
1169
- "tasks-applied",
1170
- "pilot-prepared",
1171
- "pilot-applied",
1172
- "committed",
1173
- ].includes(phase);
1174
- }
1175
- /** Publish a preflighted task-target batch inside the journal's forward-only region. */
1176
- function runTaskTargetMigrationStep(journal, plan) {
1177
- applyTaskTargetRefMigration(plan);
1178
- crashInMutationGapForTests("tasks");
1179
- advanceApplyJournal(journal, "tasks-applied");
1180
- crashAfterForTests("tasks");
1181
- }
1182
- function isTaskOnlyRepair(manifest) {
1183
- return (manifest.artifacts["config.json"].status === "current" &&
1184
- ["current", "missing"].includes(manifest.artifacts["state.db"].status) &&
1185
- manifest.artifacts["workflow.db"].status === "missing");
1186
- }
1187
- function runTaskOnlyRepair(journal, plan) {
1188
- advanceApplyJournal(journal, "tasks-prepared");
1189
- if (journal.phase === "tasks-prepared")
1190
- runTaskTargetMigrationStep(journal, plan);
1191
- advanceApplyJournal(journal, "committed");
1192
- clearApplyJournal();
1193
- }
1194
- function runStateMigrationStep(journal) {
1195
- if (journal.phase === "prepared") {
1196
- advanceApplyJournal(journal, "state-converting");
1197
- crashAfterForTests("state-converting");
1198
- }
1199
- if (journal.phase === "state-converting") {
1200
- const db = openDatabaseFinalizing(getStateDbPathInDataDir());
1201
- try {
1202
- const marker = readBoundStateGenerationMarker(db, journal.operationId);
1203
- if (marker?.phase === "state-applied") {
1204
- throw new ConfigError("state.db is ahead of its state-converting journal.", "INVALID_CONFIG_FILE");
1205
- }
1206
- if (!marker) {
1207
- db.transaction(() => {
1208
- runStateMigrations(db);
1209
- bindStateConvertingMarker(db, journal.operationId);
1210
- })();
1211
- }
1212
- else {
1213
- runStateMigrations(db, { applyPending: false });
1214
- }
1215
- }
1216
- finally {
1217
- db.close();
1218
- }
1219
- crashInMutationGapForTests("state-marker");
1220
- advanceApplyJournal(journal, "state-collapsing");
1221
- crashAfterForTests("state-marker");
1222
- }
1223
- if (journal.phase === "state-collapsing") {
1224
- if (!sameMigrationGeneration(fingerprintMigrationGeneration(), journal.generation)) {
1225
- const marker = readBoundStateGenerationMarkerFromDisk(journal.operationId);
1226
- if (!marker) {
1227
- throw new ConfigError("state.db does not match the exact marker-bound generation recorded before conversion.", "INVALID_CONFIG_FILE");
1228
- }
1229
- journal.phase = marker.phase === "state-applied" ? "state-applied" : "state-collapsing";
1230
- journal.generation = fingerprintMigrationGeneration();
1231
- writeApplyJournal(journal);
1232
- if (journal.phase === "state-applied")
1233
- return;
1234
- }
1235
- const db = openDatabaseFinalizing(getStateDbPathInDataDir());
1236
- try {
1237
- if (!readBoundStateGenerationMarker(db, journal.operationId)) {
1238
- throw new ConfigError("state.db lacks its exact marker-bound conversion generation.", "INVALID_CONFIG_FILE");
1239
- }
1240
- collapseStateDbToSingleFile(db);
1241
- crashInMutationGapForTests("state-converting");
1242
- advanceBoundStateMarker(db, journal.operationId);
1243
- }
1244
- finally {
1245
- db.close();
1246
- }
1247
- crashInMutationGapForTests("state");
1248
- advanceApplyJournal(journal, "state-applied");
1249
- crashAfterForTests("state");
1250
- }
1251
- }
1252
- function stateDatabaseHeaderUsesWal(filePath) {
1253
- const fd = fs.openSync(filePath, "r");
1254
- try {
1255
- const header = Buffer.alloc(20);
1256
- if (fs.readSync(fd, header, 0, header.byteLength, 0) !== header.byteLength) {
1257
- throw new ConfigError(`Cannot verify the SQLite header for ${filePath}.`, "INVALID_CONFIG_FILE");
1258
- }
1259
- if (!header.subarray(0, 16).equals(Buffer.from("SQLite format 3\0"))) {
1260
- throw new ConfigError(`Cannot verify a non-SQLite state database at ${filePath}.`, "INVALID_CONFIG_FILE");
1261
- }
1262
- return header[18] === 2 || header[19] === 2;
1263
- }
1264
- finally {
1265
- fs.closeSync(fd);
1266
- }
1267
- }
1268
- function assertStateReadyForCutover(journal) {
1269
- const statePath = getStateDbPathInDataDir();
1270
- if (!fs.existsSync(statePath)) {
1271
- throw new ConfigError("Cannot run cutover without state.db.", "INVALID_CONFIG_FILE");
1272
- }
1273
- if (stateDatabaseHeaderUsesWal(statePath)) {
1274
- throw new ConfigError("Refusing cutover because state.db still uses WAL; resume through the state-converting phase first.", "INVALID_CONFIG_FILE");
1275
- }
1276
- const db = openDatabaseFinalizing(statePath, { readonly: true });
1277
- try {
1278
- const journalMode = String(db.prepare("PRAGMA journal_mode").get()?.journal_mode ?? "").toLowerCase();
1279
- const hasMarkerTable = db
1280
- .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'")
1281
- .get();
1282
- const marker = hasMarkerTable
1283
- ? db
1284
- .prepare("SELECT operation_id, phase FROM akm_migration_generation WHERE singleton=1 LIMIT 2")
1285
- .all()
1286
- : [];
1287
- if (journalMode !== "delete" ||
1288
- marker.length !== 1 ||
1289
- marker[0]?.operation_id !== journal.operationId ||
1290
- marker[0]?.phase !== "state-applied") {
1291
- throw new ConfigError("Refusing cutover because state.db lacks the operation-bound state-converting/single-file proof.", "INVALID_CONFIG_FILE");
1292
- }
1293
- }
1294
- finally {
1295
- db.close();
1296
- }
1297
- }
1298
- function assertMigrationArtifactsComplete() {
1299
- const completed = inspectMigrationState();
1300
- if (completed.config.status !== "current" ||
1301
- ![completed.state.status, completed.workflow.status].every((status) => status === "current" || status === "missing")) {
1302
- throw new ConfigError("Migration verification did not reach one current cross-artifact generation.", "INVALID_CONFIG_FILE");
1303
- }
1304
- }
1305
- /** Expand a leading `~` against the home directory (config stashDir/source paths may use it). */
1306
- function expandTilde(p) {
1307
- if (p === "~")
1308
- return os.homedir();
1309
- if (p.startsWith("~/") || p.startsWith("~\\"))
1310
- return path.join(os.homedir(), p.slice(2));
1311
- return p;
1312
- }
1313
- /**
1314
- * Stash roots for the cutover ref map's origin aliases, the
1315
- * source-(b) legacy walk, AND the WI-8.5d content migration's `.stash.json`
1316
- * fold + D-R6 rename walk. Derived from the TARGET config, which by this point in
1317
- * the apply has already been normalized to the 0.9 `bundles` shape by
1318
- * {@link parseMigrationTargetConfig}: each path-bearing bundle is a root, the
1319
- * `defaultBundle` is the primary, and a bundle's `registryId` (or its key)
1320
- * supplies the origin alias. The pre-cutover `stashDir`/`sources` shape is still
1321
- * honored as a fallback for a transitional config that reaches here un-migrated.
1322
- * Source (a) (the index `item_ref` join) is authoritative, so an unresolved root
1323
- * only costs a few origin aliases.
1324
- */
1325
- function cutoverStashRootsFromConfig(config) {
1326
- const roots = [];
1327
- const bundles = config.bundles;
1328
- if (bundles && typeof bundles === "object") {
1329
- for (const [id, entry] of Object.entries(bundles)) {
1330
- const bundlePath = entry.path;
1331
- if (typeof bundlePath !== "string" || bundlePath.length === 0)
1332
- continue; // only filesystem bundles
1333
- const registryId = entry.registryId ?? id;
1334
- roots.push({
1335
- path: path.resolve(expandTilde(bundlePath)),
1336
- registryId,
1337
- primary: config.defaultBundle === id,
1338
- });
1339
- }
1340
- }
1341
- return roots;
1342
- }
1343
- /**
1344
- * Roll a pre-cutover workflow.db forward to its final ledger (010) using the
1345
- * FROZEN migration bodies (`src/migrate/legacy/workflow-migrations-bodies.ts`)
1346
- * through the shared engine — never the live `WORKFLOW_MIGRATIONS` array
1347
- * (`src/workflows/db.ts` is deleted in WI-8.3). The roll materialises every
1348
- * migration-added column + DEFAULT so the subsequent state.db merge carries
1349
- * faithful data.
1350
- *
1351
- * Pre-versioning (0.7-era) workflow.dbs — rows present but NO `schema_migrations`
1352
- * ledger — are OUT of the migrator FROM-state (the rc-train fixtures pin a
1353
- * versioned ledger). We FAIL CLOSED with a clear message rather than
1354
- * bootstrapping (the old `bootstrapPreVersioningDb` back-fill is retired).
1355
- */
1356
- function runFrozenWorkflowRoll(operationId) {
1357
- const workflowPath = getLegacyWorkflowDbPath();
1358
- const db = openDatabaseFinalizing(workflowPath);
1359
- try {
1360
- const hasRuns = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get();
1361
- const hasLedger = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'").get();
1362
- if (hasRuns && !hasLedger) {
1363
- throw new ConfigError(`Refusing to migrate a pre-versioning workflow.db at ${workflowPath} (no schema_migrations ledger). ` +
1364
- "Pre-0.8 workflow databases are not a supported migrator source; upgrade through a 0.8.x release first.", "INVALID_CONFIG_FILE");
1365
- }
1366
- // Roll the pending frozen migrations ONLY — never the base-schema DDL. Any
1367
- // real pre-cutover workflow.db already carries the base schema (its runtime
1368
- // opener created it); running the baseline CREATE INDEX here would fail on a
1369
- // pre-existing-but-narrower table. Matches the WI-8.2 workflow-applied path
1370
- // (openDatabase + runSqliteMigrations, no pragmas/base-schema), preserving
1371
- // the crash-recovery generation-fingerprint invariants that flow pins.
1372
- runSqliteMigrations(db, FROZEN_WORKFLOW_MIGRATIONS, {
1373
- generationMarker: { operationId, phase: "workflow-applied" },
1374
- });
1375
- }
1376
- finally {
1377
- db.close();
1378
- }
1379
- }
1380
- /**
1381
- * The `cutover-applied` phase (Chunk 8, WI-8.2). Builds + persists the
1382
- * old-ref → item_ref map, runs the fail-closed three-DB merge/re-key
1383
- * transaction, then the idempotent index-quarantine / workflow.db-unlink
1384
- * boundary ops. A committed merge marker (from an interrupted-then-resumed
1385
- * apply) short-circuits the merge so it runs exactly once.
1386
- */
1387
- function runCutoverStep(journal, target) {
1388
- const statePath = getStateDbPathInDataDir();
1389
- const workflowPath = getLegacyWorkflowDbPath();
1390
- const indexPath = getDbPath();
1391
- const stashRoots = cutoverStashRootsFromConfig(target);
1392
- if (!cutoverMergeCommitted(statePath, journal.operationId)) {
1393
- const refMap = buildCutoverRefMap({
1394
- oldIndexDbPath: indexPath,
1395
- stashRoots,
1396
- mapOutputPath: cutoverRefMapPath(journal),
1397
- });
1398
- // Fail-closed: an integrity failure (unparseable ref / row-count mismatch)
1399
- // throws a CutoverIntegrityError, which the outer catch converts to a
1400
- // restore-from-backup. The state txn is atomic — a throw rolls it back, so
1401
- // state.db + workflow.db are unchanged going into the rollback.
1402
- runThreeDbCutover({ refMap, operationId: journal.operationId, statePath, workflowPath, oldIndexPath: indexPath });
1403
- crashInMutationGapForTests("cutover-commit");
1404
- }
1405
- else {
1406
- assertPostCutoverWorkflowAuthenticated(journal, fingerprintMigrationGeneration());
1407
- }
1408
- // Boundary ops run AFTER the committed state txn, OUTSIDE the fail-closed gate
1409
- // (cutover-design.md §2 step 5/6). Idempotent + best-effort — they log and
1410
- // return, never throw, so a rename/unlink hiccup never rolls back the merge.
1411
- quarantineIndexDb(journal.operationId, indexPath);
1412
- deleteWorkflowDb(workflowPath);
1413
- // WI-8.5d: the content migration (`.stash.json` fold + delete, D-R6 reserved-
1414
- // filename conformance) is an ADDITIVE filesystem step of the same phase. It
1415
- // also runs AFTER the committed state txn, is best-effort (a throw is swallowed
1416
- // + logged, never aborting a committed cutover), and idempotent (a resumed
1417
- // apply finds no sidecars and no mis-named concepts, so it re-runs to a no-op).
1418
- runContentMigrationStep(journal, target);
1419
- }
1420
- function cutoverRefMapPath(journal) {
1421
- return path.join(path.dirname(getMigrationApplyJournalPath()), `cutover-refmap-${journal.operationId}.json`);
1422
- }
1423
- /** Required forward-only filesystem step after the core config/database cutover verifies. */
1424
- function runPilotTreatmentStep(journal, target) {
1425
- const refMap = loadCutoverRefMap(cutoverRefMapPath(journal));
1426
- migratePilotTreatmentFiles(cutoverStashRootsFromConfig(target), refMap);
1427
- }
1428
- /**
1429
- * Persist location for the content-migration report — next to the ApplyJournal
1430
- * (alongside the cutover ref map). Survives `clearApplyJournal` (which removes
1431
- * only the journal file), so the operator + the WI-8.5d test can read the D-R6
1432
- * rename list after a committed apply.
1433
- */
1434
- function contentMigrationReportPath() {
1435
- return path.join(path.dirname(getMigrationApplyJournalPath()), "content-migration-report.json");
1436
- }
1437
- /** Best-effort content migration + report persistence (see {@link runCutoverStep}). */
1438
- function runContentMigrationStep(journal, target) {
1439
- try {
1440
- const roots = cutoverStashRootsFromConfig(target).map((r) => r.path);
1441
- const report = runContentMigration(roots);
1442
- // Fold the one-time pre-0.9 filesystem-proposal import into this same
1443
- // additive step (it used to run on every proposal operation via
1444
- // `withProposalsDb`). state.db has been merged + collapsed to single-file
1445
- // DELETE mode by this point, so we open it raw and INSERT OR IGNORE each
1446
- // legacy `proposal.json` on its UUID — idempotent, no ledger needed.
1447
- //
1448
- // rc-window edge: a user who already ran `akm migrate apply` on an EARLIER
1449
- // rc binary (before this fold existed) AND never ran a proposal command
1450
- // afterward would have their pre-0.9 fs proposals un-imported — the old
1451
- // live-path import that once covered that gap is gone. Re-running this
1452
- // idempotent `migrate apply` recovers them (the legacy files are left in
1453
- // place on import, so they are still there to re-walk). Acceptable for an rc.
1454
- report.legacyProposalsImported = importLegacyProposalsIntoState(getStateDbPathInDataDir(), roots);
1455
- persistContentMigrationReport(report);
1456
- if (report.sidecarsFolded > 0 || report.reservedRenames.length > 0 || report.legacyProposalsImported > 0) {
1457
- console.log(JSON.stringify({
1458
- event: "content-migration",
1459
- operationId: journal.operationId,
1460
- ...report,
1461
- }));
1462
- }
1463
- }
1464
- catch (error) {
1465
- console.error(`[akm] content migration skipped (${error instanceof Error ? error.message : String(error)}); the committed cutover is unaffected.`);
1466
- }
1467
- }
1468
- function persistContentMigrationReport(report) {
1469
- try {
1470
- const reportPath = contentMigrationReportPath();
1471
- fs.mkdirSync(path.dirname(reportPath), { recursive: true, mode: 0o700 });
1472
- writeFileAtomic(reportPath, `${JSON.stringify(report, null, 2)}\n`, 0o600);
1473
- }
1474
- catch {
1475
- // The report is auditing-only; a persistence failure never affects the cutover.
1476
- }
1477
- }
1478
- function unsafeArtifact(name, state) {
1479
- if (!["newer", "inconsistent", "corrupt"].includes(state.status))
1480
- return undefined;
1481
- return `${name} is ${state.status}${state.detail ? `: ${state.detail}` : ""}`;
1482
- }
1483
- /**
1484
- * Parse + validate a migration TARGET config, applying the Chunk-8 config-shape
1485
- * migration (`stashDir`/`sources[]`/`installed[]` → `bundles`/`defaultBundle`)
1486
- * as a pre-validation transform. This is why a target still carrying the
1487
- * pre-cutover source shape loads (and reports "current" in `migrate status`)
1488
- * even though the runtime loader now rejects that shape once `bundles` exists:
1489
- * the migrator normalizes it FIRST, then the strict schema gates the result.
1490
- *
1491
- * The transform is idempotent, so an already-migrated prepared config, or the
1492
- * new-shape config re-parsed from the apply journal, passes through untouched.
1493
- */
1494
- function parseMigrationTargetConfig(text, sourcePath) {
1495
- const raw = parseConfigText(text, sourcePath);
1496
- const migrated = migrateConfigSourcesToBundles(raw);
1497
- // Re-serialize the normalized object through the canonical validator so the
1498
- // version check, schema validation, and defaults merge stay in one place.
1499
- return parseAndValidateConfigText(JSON.stringify(migrated), sourcePath);
1500
- }
1501
- function loadTargetConfig(preparedConfigPath, artifacts) {
1502
- const targetPath = preparedConfigPath ?? (artifacts.config.status === "current" ? getConfigPath() : undefined);
1503
- if (!targetPath) {
1504
- return {
1505
- state: { status: "missing", source: "none", detail: MANUAL_GUIDANCE },
1506
- };
1507
- }
1508
- let text;
1509
- try {
1510
- text = readTextFileWithLimit(targetPath, MAX_CONFIG_FILE_BYTES, "Prepared migration config");
1511
- }
1512
- catch (error) {
1513
- return {
1514
- state: {
1515
- status: "corrupt",
1516
- source: preparedConfigPath ? "prepared" : "active",
1517
- path: targetPath,
1518
- detail: error instanceof Error ? error.message : String(error),
1519
- },
1520
- };
1521
- }
1522
- try {
1523
- return {
1524
- state: {
1525
- status: "current",
1526
- source: preparedConfigPath ? "prepared" : "active",
1527
- path: targetPath,
1528
- },
1529
- config: parseMigrationTargetConfig(text, targetPath),
1530
- };
1531
- }
1532
- catch (error) {
1533
- return {
1534
- state: {
1535
- status: "corrupt",
1536
- source: preparedConfigPath ? "prepared" : "active",
1537
- path: targetPath,
1538
- detail: error instanceof Error ? error.message : String(error),
1539
- },
1540
- };
1541
- }
1542
- }
1543
- function buildMigrationPlan(preparedConfigPath, activeApply) {
1544
- const artifacts = activeApply.artifacts ?? inspectMigrationStateFromSnapshots();
1545
- const restorePending = fs.existsSync(getMigrationRestoreJournalPath());
1546
- const target = activeApply.journal
1547
- ? {
1548
- state: {
1549
- status: activeApply.config ? "current" : "corrupt",
1550
- source: "prepared",
1551
- path: getMigrationApplyJournalPath(),
1552
- ...(!activeApply.config && activeApply.error ? { detail: activeApply.error } : {}),
1553
- },
1554
- config: activeApply.config,
1555
- }
1556
- : loadTargetConfig(preparedConfigPath, artifacts);
1557
- const blockers = [
1558
- unsafeArtifact("config.json", artifacts.config),
1559
- unsafeArtifact("state.db", artifacts.state),
1560
- unsafeArtifact("workflow.db", artifacts.workflow),
1561
- ].filter((blocker) => blocker !== undefined);
1562
- if (target.state.status !== "current")
1563
- blockers.push(target.state.detail ?? "A current target config is required.");
1564
- if (activeApply.error && (!activeApply.journal || target.state.status === "current"))
1565
- blockers.push(activeApply.error);
1566
- if (restorePending)
1567
- blockers.push(`Restore recovery is pending at ${getMigrationRestoreJournalPath()}.`);
1568
- let taskRewrites = 0;
1569
- if (blockers.length === 0 && target.config) {
1570
- try {
1571
- taskRewrites = planTaskTargetRefMigration(target.config).rewrites.length;
1572
- }
1573
- catch (error) {
1574
- blockers.push(error instanceof Error ? error.message : String(error));
1575
- }
1576
- }
1577
- const needsApply = !!activeApply.journal ||
1578
- artifacts.config.status !== "current" ||
1579
- artifacts.state.status === "old" ||
1580
- artifacts.workflow.status === "old" ||
1581
- artifacts.workflow.status === "current" ||
1582
- taskRewrites > 0;
1583
- return {
1584
- status: blockers.length > 0 ? "blocked" : needsApply ? "ready" : "current",
1585
- artifacts,
1586
- targetConfig: target.state,
1587
- blockers,
1588
- ...(restorePending
1589
- ? {
1590
- activeOperation: {
1591
- kind: "restore",
1592
- phase: "pending",
1593
- journalPath: getMigrationRestoreJournalPath(),
1594
- },
1595
- }
1596
- : activeApply.journal
1597
- ? {
1598
- activeOperation: {
1599
- kind: "apply",
1600
- phase: activeApply.journal.phase,
1601
- journalPath: getMigrationApplyJournalPath(),
1602
- },
1603
- }
1604
- : {}),
1605
- };
1606
- }
1607
- export function inspectMigrationPlan(preparedConfigPath) {
1608
- return buildMigrationPlan(preparedConfigPath, readApplyJournal());
1609
- }
1610
- function printPlan(plan) {
1611
- console.log(JSON.stringify(plan));
1612
- if (plan.status === "blocked")
1613
- process.exitCode = EXIT_CODES.GENERAL;
1614
- }
1615
- export async function runMigrationStatus(options = {}) {
1616
- printPlan(inspectMigrationPlan(options.preparedConfigPath));
1617
- }
1618
- function requireEligiblePlan(preparedConfigPath, active = readApplyJournal()) {
1619
- const plan = buildMigrationPlan(preparedConfigPath, active);
1620
- const loaded = active.journal ? { config: active.config } : loadTargetConfig(preparedConfigPath, plan.artifacts);
1621
- if (plan.status === "blocked" || !loaded.config) {
1622
- throw new ConfigError(`Migration is blocked: ${plan.blockers.join("; ")}`, "INVALID_CONFIG_FILE");
1623
- }
1624
- return { plan, target: loaded.config };
1625
- }
1626
- export async function runMigrationApply(options = {}) {
1627
- if (options.dryRun) {
1628
- printPlan(inspectMigrationPlan(options.preparedConfigPath));
1629
- return;
1630
- }
1631
- const result = withConfigLock(() => withMaintenanceStartBarrier(() => {
1632
- recoverInterruptedRestoreWithLocksHeld();
1633
- authenticatePreConversionJournalForApply();
1634
- assertNoArtifactReplacementBlockers(undefined, {
1635
- skipWorkflowClaims: fs.existsSync(getMigrationApplyJournalPath()),
1636
- });
1637
- preparePreConversionJournalForApply();
1638
- const active = readApplyJournal();
1639
- if (active.error)
1640
- throw new ConfigError(active.error, "INVALID_CONFIG_FILE");
1641
- if (active.rollbackCompleted && active.journal) {
1642
- clearApplyJournal();
1643
- resetConfigCache();
1644
- throw new ConfigError("Interrupted migration rollback was already committed; cleaned its apply journal. Rerun migrate apply with the prepared config.", "INVALID_CONFIG_FILE");
1645
- }
1646
- if (active.journal?.phase === "rollback-prepared") {
1647
- restoreMigrationBackupWithLocksHeld(active.journal.backupPath);
1648
- crashInMutationGapForTests("rollback");
1649
- clearApplyJournal();
1650
- resetConfigCache();
1651
- throw new ConfigError("Interrupted migration rollback completed from its exact journaled generation; rerun migrate apply with the prepared config.", "INVALID_CONFIG_FILE");
1652
- }
1653
- if (active.adjacent && active.journal) {
1654
- active.journal.generation = active.adjacent.generation;
1655
- if (active.adjacent.complete)
1656
- active.journal.phase = active.adjacent.phase;
1657
- writeApplyJournal(active.journal);
1658
- }
1659
- const { plan, target } = requireEligiblePlan(options.preparedConfigPath, active);
1660
- if (plan.status === "current")
1661
- return { plan };
1662
- const backup = active.journal
1663
- ? { path: active.journal.backupPath, manifest: verifyMigrationBackup(active.journal.backupPath) }
1664
- : ensureMigrationBackupWithConfigLockHeld();
1665
- const journal = active.journal ?? {
1666
- formatVersion: 2,
1667
- version: MIGRATION_BACKUP_VERSION,
1668
- operationId: `${process.pid}-${randomUUID()}`,
1669
- installationId: backup.manifest.installationId,
1670
- backupRunId: backup.manifest.runId,
1671
- phase: "prepared",
1672
- backupPath: backup.path,
1673
- targetConfig: sanitizeConfigForWrite(target),
1674
- generation: fingerprintMigrationGeneration(),
1675
- };
1676
- if (!active.journal)
1677
- writeApplyJournal(journal);
1678
- const taskOnlyRepair = isTaskOnlyRepair(backup.manifest);
1679
- let forwardRecoveryRequired = isForwardRecoveryPhase(journal.phase);
1680
- try {
1681
- const taskTargetPlan = planTaskTargetRefMigration(target);
1682
- if (taskOnlyRepair) {
1683
- forwardRecoveryRequired = true;
1684
- runTaskOnlyRepair(journal, taskTargetPlan);
1685
- return { plan: inspectMigrationPlan(), backup };
1686
- }
1687
- runStateMigrationStep(journal);
1688
- forwardRecoveryRequired = isForwardRecoveryPhase(journal.phase);
1689
- if (isPostCutoverPhase(journal.phase))
1690
- runCutoverStep(journal, target);
1691
- if (journal.phase === "state-applied") {
1692
- // Roll the pre-cutover workflow.db through the frozen migration bodies
1693
- // only while the journal is still before the committed cutover.
1694
- const beforeWorkflow = inspectExactApplyJournalGeneration(journal);
1695
- if (beforeWorkflow.workflow.status === "old") {
1696
- runFrozenWorkflowRoll(journal.operationId);
1697
- }
1698
- else if (beforeWorkflow.workflow.status !== "current" && beforeWorkflow.workflow.status !== "missing") {
1699
- throw new ConfigError(`Cannot resume workflow.db from ${beforeWorkflow.workflow.status} state.`, "INVALID_CONFIG_FILE");
1700
- }
1701
- crashInMutationGapForTests("workflow");
1702
- advanceApplyJournal(journal, "workflow-applied");
1703
- crashAfterForTests("workflow");
1704
- }
1705
- if (journal.phase === "workflow-applied") {
1706
- const migrated = inspectExactApplyJournalGeneration(journal);
1707
- for (const [name, state] of [
1708
- ["state.db", migrated.state],
1709
- ["workflow.db", migrated.workflow],
1710
- ]) {
1711
- if (state.status !== "current" && state.status !== "missing") {
1712
- throw new ConfigError(`Migration left ${name} in ${state.status} state.`, "INVALID_CONFIG_FILE");
1713
- }
1714
- }
1715
- assertStateReadyForCutover(journal);
1716
- runCutoverStep(journal, target);
1717
- forwardRecoveryRequired = true;
1718
- crashInMutationGapForTests("cutover");
1719
- advanceApplyJournal(journal, "cutover-applied");
1720
- crashAfterForTests("cutover");
1721
- }
1722
- if (journal.phase === "cutover-applied") {
1723
- // The cutover is committed, so all remaining work is forward-only.
1724
- try {
1725
- const preCutoverText = readConfigText(getConfigPath());
1726
- if (preCutoverText !== undefined) {
1727
- mergeLockEntriesSync(migratedLockEntries(parseConfigText(preCutoverText, getConfigPath())));
1728
- }
1729
- }
1730
- catch {
1731
- // Advisory lock re-key only; the committed cutover is unaffected.
1732
- }
1733
- backupExistingConfig(getConfigPath());
1734
- writeConfigAtomic(getConfigPath(), sanitizeConfigForWrite(target));
1735
- resetConfigCache();
1736
- crashInMutationGapForTests("config");
1737
- advanceApplyJournal(journal, "config-applied");
1738
- crashAfterForTests("config");
1739
- }
1740
- if (journal.phase === "config-applied") {
1741
- assertMigrationArtifactsComplete();
1742
- advanceApplyJournal(journal, "tasks-prepared");
1743
- }
1744
- if (journal.phase === "tasks-prepared") {
1745
- forwardRecoveryRequired = true;
1746
- runTaskTargetMigrationStep(journal, taskTargetPlan);
1747
- }
1748
- if (journal.phase === "tasks-applied")
1749
- advanceApplyJournal(journal, "pilot-prepared");
1750
- if (journal.phase === "pilot-prepared") {
1751
- forwardRecoveryRequired = true;
1752
- runPilotTreatmentStep(journal, target);
1753
- crashInMutationGapForTests("pilot");
1754
- advanceApplyJournal(journal, "pilot-applied");
1755
- crashAfterForTests("pilot");
1756
- }
1757
- if (journal.phase === "pilot-applied")
1758
- advanceApplyJournal(journal, "committed");
1759
- clearApplyJournal();
1760
- const completed = inspectMigrationPlan();
1761
- return { plan: completed, backup };
1762
- }
1763
- catch (error) {
1764
- if (error instanceof MigrationPreflightGenerationError || error instanceof MigrationSnapshotChangedError) {
1765
- forwardRecoveryRequired = true;
1766
- }
1767
- if (isForwardRecoveryPhase(journal.phase))
1768
- forwardRecoveryRequired = true;
1769
- if (!forwardRecoveryRequired && cutoverMergeCommitted(getStateDbPathInDataDir(), journal.operationId)) {
1770
- forwardRecoveryRequired = true;
1771
- }
1772
- if (forwardRecoveryRequired) {
1773
- throw new ConfigError(`Migration apply requires forward recovery from ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
1774
- }
1775
- try {
1776
- const rollbackGeneration = fingerprintMigrationGeneration();
1777
- assertRollbackTransitionAllowed(journal, rollbackGeneration);
1778
- journal.phase = "rollback-prepared";
1779
- journal.generation = rollbackGeneration;
1780
- writeApplyJournal(journal);
1781
- if (!sameMigrationGeneration(fingerprintMigrationGeneration(), journal.generation)) {
1782
- throw new ConfigError(`Refusing migration rollback because live artifacts no longer match journal phase ${journal.phase}.`, "INVALID_CONFIG_FILE");
1783
- }
1784
- restoreMigrationBackupWithLocksHeld(backup.path);
1785
- crashInMutationGapForTests("rollback");
1786
- clearApplyJournal();
1787
- resetConfigCache();
1788
- }
1789
- catch (rollbackError) {
1790
- throw new ConfigError(`Migration apply failed and rollback could not complete. Keep the current binary and recover from ${backup.path}. Apply error: ${error instanceof Error ? error.message : String(error)}. Rollback error: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, "INVALID_CONFIG_FILE");
1791
- }
1792
- throw new ConfigError(`Migration apply failed; config and databases were restored from ${backup.path}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
1793
- }
1794
- }));
1795
- console.log(JSON.stringify({
1796
- ...result.plan,
1797
- status: result.plan.status === "blocked" ? "blocked" : "current",
1798
- ...(result.backup ? { backupPath: result.backup.path, backupRunId: result.backup.manifest.runId } : {}),
1799
- }));
1800
- }
1801
- /** Backward-compatible config subcommand routed through the canonical coordinator. */
1802
- export async function runConfigMigrate(options = {}) {
1803
- if (options.dryRun || !options.preparedConfigPath)
1804
- return runMigrationStatus(options);
1805
- return runMigrationApply(options);
1806
- }