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,841 +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
- /**
5
- * @removeIn 0.10.0
6
- *
7
- * The one-time three-DB cutover DATA step (akm 0.9.0 Chunk 8, WI-8.2;
8
- * plan §3.2/§3.3/§8, normative §11.4, chunk-8 cutover design). Migration
9
- * `020-three-db-cutover` is the pure additive
10
- * DDL (`CREATE TABLE IF NOT EXISTS` the merge-target tables); THIS module is the
11
- * code that MOVES the durable rows into place, exactly once, under the
12
- * migrate-apply fail-closed gate (`src/cli/config-migrate.ts` `cutover-applied`
13
- * phase). It never runs as a sealed SQL migration body — the ATTACH path is
14
- * runtime-resolved and the old-ref → item_ref map is filesystem/index-derived.
15
- *
16
- * ## Frozen-resolver rule (plan §3.3 item 2)
17
- *
18
- * Old-ref resolution NEVER runs through new-layout code. This module therefore
19
- * imports ONLY the frozen legacy surface (`./legacy-layout`), the stored-ref
20
- * grammar (`../legacy-ref-grammar`), storage-engine helpers (`openDatabase`,
21
- * `applyStandardPragmas`), core path/warn leaves, and Node builtins. It imports
22
- * NOTHING from `src/indexer/` or `src/workflows/`: the last-good-index join
23
- * reads the ATTACHED old index.db by raw SQL, never through indexer code, and
24
- * the workflow merge reads the ATTACHED old workflow.db by raw SQL.
25
- *
26
- * ## Design choices where the design is silent (documented per the WI brief)
27
- *
28
- * - **ATTACH is read-write, safety enforced by construction.** `cutover-design`
29
- * calls for read-only ATTACH; to stay driver-portable (bun:sqlite has no
30
- * URI-mode guarantee) we ATTACH normally but (a) pre-check `fs.existsSync`
31
- * before every ATTACH so a missing file is never silently CREATED, and (b)
32
- * only ever `SELECT` from the attached schemas — the sole writes target
33
- * `main` (state.db). This is behaviourally equivalent to a read-only ATTACH.
34
- * - **Column-intersection copy.** The workflow / usage_events merge copies the
35
- * INTERSECTION of columns present in both the source and the target table, so
36
- * a source DB at any pre-cutover shape (or a partially-migrated test fixture)
37
- * copies verbatim what it holds without tripping "no such column".
38
- * - **Durable idempotency marker.** The merge writes a singleton row into
39
- * `akm_cutover_ledger` INSIDE the same transaction as the data move, so a
40
- * crash after COMMIT (but before the journal advances, or the workflow.db
41
- * unlink) never re-runs the INSERT…SELECT (which would duplicate rows). The
42
- * boundary ops (index quarantine, workflow.db unlink) key on that committed
43
- * marker and are idempotent.
44
- * - **Ref-map source (b) — the frozen legacy-layout walk — is best-effort.**
45
- * It only ADDS mappings for on-disk assets the index no longer holds, using
46
- * the source's `registryId` (or a local basename slug) as the bundle. The
47
- * primary correctness path is source (a): the last-good index join, which
48
- * reads the durable `item_ref` directly.
49
- */
50
- import crypto from "node:crypto";
51
- import fs from "node:fs";
52
- import path from "node:path";
53
- import { warn } from "../../core/warn.js";
54
- import { openDatabaseFinalizing } from "../../storage/database.js";
55
- import { applyStandardPragmas } from "../../storage/sqlite-pragmas.js";
56
- import { classifyRefGrammar, parseStoredRef } from "../legacy-ref-grammar.js";
57
- import { deriveCanonicalAssetName, TYPE_DIRS } from "./legacy-layout.js";
58
- // ═══════════════════════════════════════════════════════════════════════
59
- // Errors + report shapes
60
- // ═══════════════════════════════════════════════════════════════════════
61
- /**
62
- * A re-key INTEGRITY failure (unparseable stored ref, or a post-pass row-count
63
- * mismatch). Distinct from an EXPECTED orphan (old ref → no live item), which is
64
- * quarantined and never aborts the cutover. The apply flow converts this typed
65
- * error into a fail-closed restore.
66
- */
67
- export class CutoverIntegrityError extends Error {
68
- constructor(message) {
69
- super(message);
70
- this.name = "CutoverIntegrityError";
71
- }
72
- }
73
- const CUTOVER_REFMAP_FORMAT = 1;
74
- /**
75
- * Compute the old-ref → new item_ref map BEFORE any re-layout, and persist it as
76
- * JSON (fsynced) next to the ApplyJournal. Sources, in precedence order:
77
- *
78
- * (a) last-good index join — `entries.entry_key` / `item_ref`, generalizing
79
- * the F4c `classifyLegacyRefForRekey` origin rules to a full-table pass.
80
- * (b) frozen legacy-layout walk of the configured stash roots, for on-disk
81
- * refs the index no longer holds (best-effort — source (a) wins).
82
- */
83
- export function buildCutoverRefMap(opts) {
84
- const map = new Map();
85
- // ── Source (a): the last-good index join (authoritative). ──
86
- if (fs.existsSync(opts.oldIndexDbPath)) {
87
- const db = openDatabaseFinalizing(opts.oldIndexDbPath, { readonly: true });
88
- try {
89
- const entryColumns = tableExists(db, "main", "entries") ? new Set(columnNames(db, "main", "entries")) : undefined;
90
- if (["entry_key", "item_ref", "entry_type", "stash_dir"].every((column) => entryColumns?.has(column))) {
91
- const rows = db
92
- .prepare("SELECT entry_key AS entryKey, item_ref AS itemRef, entry_type AS entryType, stash_dir AS stashDir " +
93
- "FROM entries WHERE item_ref IS NOT NULL AND item_ref <> ''")
94
- .all();
95
- for (const row of rows)
96
- addIndexEntryMappings(map, row, opts.stashRoots);
97
- }
98
- }
99
- finally {
100
- db.close();
101
- }
102
- }
103
- // ── Source (b): the frozen legacy-layout walk (completeness for stale-index refs). ──
104
- for (const root of opts.stashRoots ?? [])
105
- walkLegacyLayoutInto(map, root);
106
- persistRefMapJson(opts.mapOutputPath, map);
107
- return map;
108
- }
109
- /** First-wins insertion: an old spelling that already maps to a different item_ref keeps its first target. */
110
- function setMapping(map, oldRef, itemRef) {
111
- if (!map.has(oldRef))
112
- map.set(oldRef, itemRef);
113
- }
114
- function addIndexEntryMappings(map, row, stashRoots) {
115
- const bareTail = row.entryKey.includes("//") ? row.entryKey.slice(row.entryKey.indexOf("//") + 2) : row.entryKey; // `type:name`
116
- const bundle = row.itemRef.includes("//") ? row.itemRef.slice(0, row.itemRef.indexOf("//")) : undefined;
117
- const matched = stashRoots?.find((r) => samePath(r.path, row.stashDir));
118
- // No stash-root info (or an unrecognized root) → treat as the primary source,
119
- // so single-source installs (and the test fixtures) always get bare keys.
120
- const isPrimary = matched ? matched.primary === true || stashRoots?.[0] === matched : true;
121
- if (isPrimary) {
122
- setMapping(map, bareTail, row.itemRef); // bare `type:name` resolves to the default/primary
123
- setMapping(map, `stash//${bareTail}`, row.itemRef);
124
- setMapping(map, `local//${bareTail}`, row.itemRef);
125
- }
126
- if (bundle)
127
- setMapping(map, `${bundle}//${bareTail}`, row.itemRef);
128
- if (matched?.registryId)
129
- setMapping(map, `${matched.registryId}//${bareTail}`, row.itemRef);
130
- setMapping(map, row.entryKey, row.itemRef); // the literal stored key
131
- }
132
- /**
133
- * Best-effort source (b): walk a configured stash root's `TYPE_DIRS` with the
134
- * frozen resolver and add a mapping for each on-disk asset the index map does
135
- * not already cover. The bundle is the source's `registryId`, or a basename slug
136
- * for the primary (matching how the index mints the primary bundle id).
137
- */
138
- function walkLegacyLayoutInto(map, root) {
139
- let bundle;
140
- if (root.registryId && root.registryId.length > 0)
141
- bundle = root.registryId;
142
- else if (root.primary)
143
- bundle = basenameSlug(root.path);
144
- else
145
- return; // non-primary source with no registryId — cannot form a stable bundle here
146
- for (const [type, dirName] of Object.entries(TYPE_DIRS)) {
147
- const typeRoot = path.join(root.path, dirName);
148
- let files;
149
- try {
150
- files = listFilesRecursive(typeRoot);
151
- }
152
- catch {
153
- continue; // dir absent / unreadable
154
- }
155
- for (const filePath of files) {
156
- const name = safeDerive(type, typeRoot, filePath);
157
- if (name === undefined)
158
- continue;
159
- const bareTail = `${type}:${name}`;
160
- if (map.has(bareTail))
161
- continue; // source (a) already covers it
162
- const conceptId = `${dirName}/${name}`;
163
- const itemRef = `${bundle}//${conceptId}`;
164
- setMapping(map, bareTail, itemRef);
165
- if (root.primary) {
166
- setMapping(map, `stash//${bareTail}`, itemRef);
167
- setMapping(map, `local//${bareTail}`, itemRef);
168
- }
169
- if (root.registryId)
170
- setMapping(map, `${root.registryId}//${bareTail}`, itemRef);
171
- }
172
- }
173
- }
174
- function safeDerive(type, typeRoot, filePath) {
175
- try {
176
- return deriveCanonicalAssetName(type, typeRoot, filePath);
177
- }
178
- catch {
179
- return undefined;
180
- }
181
- }
182
- /** Basename slug matching the index's `slugForPath` primary-bundle derivation (reimplemented, not imported). */
183
- function basenameSlug(sourcePath) {
184
- const base = path
185
- .basename(path.resolve(sourcePath))
186
- .toLowerCase()
187
- .replace(/[^a-z0-9]+/g, "-")
188
- .replace(/^-+|-+$/g, "");
189
- return base.length > 0 ? base : "bundle";
190
- }
191
- function listFilesRecursive(dir) {
192
- const out = [];
193
- const walk = (current) => {
194
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
195
- const abs = path.join(current, entry.name);
196
- if (entry.isDirectory())
197
- walk(abs);
198
- else if (entry.isFile())
199
- out.push(abs);
200
- }
201
- };
202
- walk(dir);
203
- return out;
204
- }
205
- function samePath(a, b) {
206
- if (!b)
207
- return false;
208
- return path.resolve(a) === path.resolve(b);
209
- }
210
- function persistRefMapJson(outputPath, map) {
211
- fs.mkdirSync(path.dirname(outputPath), { recursive: true, mode: 0o700 });
212
- const payload = {
213
- formatVersion: CUTOVER_REFMAP_FORMAT,
214
- entries: Object.fromEntries([...map.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
215
- };
216
- const tmp = `${outputPath}.tmp`;
217
- const fd = fs.openSync(tmp, "w", 0o600);
218
- try {
219
- fs.writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`);
220
- fs.fsyncSync(fd);
221
- }
222
- finally {
223
- fs.closeSync(fd);
224
- }
225
- fs.renameSync(tmp, outputPath);
226
- try {
227
- const dirFd = fs.openSync(path.dirname(outputPath), "r");
228
- try {
229
- fs.fsyncSync(dirFd);
230
- }
231
- finally {
232
- fs.closeSync(dirFd);
233
- }
234
- }
235
- catch {
236
- // Directory fsync is unavailable on some filesystems.
237
- }
238
- }
239
- /** Load the persisted cutover map when a committed migration resumes at a boundary step. */
240
- export function loadCutoverRefMap(inputPath) {
241
- if (!fs.existsSync(inputPath)) {
242
- throw new CutoverIntegrityError(`persisted cutover ref map is missing: ${inputPath}`);
243
- }
244
- const parsed = JSON.parse(fs.readFileSync(inputPath, "utf8"));
245
- if (parsed.formatVersion !== CUTOVER_REFMAP_FORMAT ||
246
- !parsed.entries ||
247
- typeof parsed.entries !== "object" ||
248
- Array.isArray(parsed.entries)) {
249
- throw new CutoverIntegrityError(`invalid persisted cutover ref map: ${inputPath}`);
250
- }
251
- const map = new Map();
252
- for (const [oldRef, itemRef] of Object.entries(parsed.entries)) {
253
- if (!oldRef || typeof itemRef !== "string" || !itemRef) {
254
- throw new CutoverIntegrityError(`invalid persisted cutover ref map entry for ${oldRef}`);
255
- }
256
- map.set(oldRef, itemRef);
257
- }
258
- return map;
259
- }
260
- const PILOT_TREATMENT_FILE = path.join(".akm", "measurement", "treatment-pilot-2026-06-14.txt");
261
- function fsyncPilotTreatmentDirectory(directory) {
262
- let fd;
263
- try {
264
- fd = fs.openSync(directory, "r");
265
- fs.fsyncSync(fd);
266
- }
267
- catch (error) {
268
- const code = error.code;
269
- if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR" && code !== "EPERM")
270
- throw error;
271
- }
272
- finally {
273
- if (fd !== undefined)
274
- fs.closeSync(fd);
275
- }
276
- }
277
- /** Re-key the one pre-0.9 pilot cohort file while the frozen old-ref map is available. */
278
- export function migratePilotTreatmentFiles(stashRoots, refMap) {
279
- let migrated = 0;
280
- const seen = new Set();
281
- for (const root of stashRoots) {
282
- const file = path.join(root.path, PILOT_TREATMENT_FILE);
283
- const resolved = path.resolve(file);
284
- if (seen.has(resolved) || !fs.existsSync(file))
285
- continue;
286
- seen.add(resolved);
287
- const original = fs.readFileSync(file, "utf8");
288
- const rewritten = original
289
- .split("\n")
290
- .map((line) => {
291
- const trimmed = line.trim();
292
- if (!trimmed || trimmed.startsWith("#"))
293
- return line;
294
- const target = refMap.get(trimmed);
295
- if (!target)
296
- return line;
297
- const prefix = line.slice(0, line.indexOf(trimmed));
298
- const suffix = line.slice(line.indexOf(trimmed) + trimmed.length);
299
- return `${prefix}${target}${suffix}`;
300
- })
301
- .join("\n");
302
- if (rewritten === original) {
303
- fsyncPilotTreatmentDirectory(path.dirname(file));
304
- continue;
305
- }
306
- const mode = fs.statSync(file).mode & 0o777;
307
- const tmp = `${file}.tmp-${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
308
- let ownsTmp = false;
309
- try {
310
- const fd = fs.openSync(tmp, "wx", mode);
311
- ownsTmp = true;
312
- try {
313
- fs.fchmodSync(fd, mode);
314
- fs.writeFileSync(fd, rewritten);
315
- fs.fsyncSync(fd);
316
- }
317
- finally {
318
- fs.closeSync(fd);
319
- }
320
- fs.renameSync(tmp, file);
321
- ownsTmp = false;
322
- fsyncPilotTreatmentDirectory(path.dirname(file));
323
- }
324
- catch (error) {
325
- if (ownsTmp)
326
- fs.rmSync(tmp, { force: true });
327
- throw error;
328
- }
329
- migrated += 1;
330
- }
331
- return migrated;
332
- }
333
- // ═══════════════════════════════════════════════════════════════════════
334
- // The re-key engine (per-table policy, cutover-design.md §3)
335
- // ═══════════════════════════════════════════════════════════════════════
336
- /** Scalar tables (PK = ref column) — most-recently-updated wins on collision. */
337
- const SCALAR_REKEY_TABLES = [
338
- { table: "asset_salience", keyColumn: "asset_ref", tsColumn: "updated_at" },
339
- { table: "asset_outcome", keyColumn: "asset_ref", tsColumn: "updated_at" },
340
- ];
341
- /** Row-carried tables — UPDATE the ref column in place, rows preserved as-is. */
342
- const EVENT_REKEY_TABLES = [
343
- { table: "events", keyColumn: "ref" },
344
- { table: "proposals", keyColumn: "ref" },
345
- { table: "task_history", keyColumn: "target_ref" },
346
- { table: "proposal_fingerprints", keyColumn: "ref" },
347
- { table: "canary_queries", keyColumn: "anchor_ref" },
348
- ];
349
- /**
350
- * Classify one stored ref against the map:
351
- * - in the map → re-key to its item_ref;
352
- * - already new-grammar → skip (idempotent; already canonical);
353
- * - legacy + parseable → EXPECTED orphan (no live item) → quarantine;
354
- * - legacy + unparseable → INTEGRITY failure → fail closed.
355
- */
356
- function classifyCutoverRef(ref, refMap) {
357
- const target = refMap.get(ref);
358
- if (target !== undefined)
359
- return { kind: "rekey", target };
360
- if (classifyRefGrammar(ref) === "bundle")
361
- return { kind: "skip" };
362
- try {
363
- parseStoredRef(ref);
364
- }
365
- catch {
366
- return { kind: "integrity", reason: `unparseable stored ref "${ref}"` };
367
- }
368
- return { kind: "orphan" };
369
- }
370
- function emptyReport() {
371
- return { rekeyed: {}, quarantined: {}, merged: {}, skipped: [] };
372
- }
373
- function ensureLegacyStateTable(db) {
374
- db.exec(`
375
- CREATE TABLE IF NOT EXISTS legacy_state (
376
- surface TEXT NOT NULL,
377
- old_ref TEXT NOT NULL,
378
- row_count INTEGER NOT NULL DEFAULT 0,
379
- reason TEXT NOT NULL,
380
- quarantined_at TEXT NOT NULL DEFAULT (datetime('now')),
381
- PRIMARY KEY (surface, old_ref)
382
- );
383
- `);
384
- }
385
- function quarantineRow(db, surface, oldRef, count, reason) {
386
- db.prepare(`INSERT INTO legacy_state (surface, old_ref, row_count, reason, quarantined_at)
387
- VALUES (?, ?, ?, ?, datetime('now'))
388
- ON CONFLICT(surface, old_ref) DO UPDATE SET row_count = excluded.row_count, reason = excluded.reason`).run(surface, oldRef, count, reason);
389
- }
390
- function isMissingTableOrColumn(err) {
391
- const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
392
- return msg.includes("no such table") || msg.includes("no such column");
393
- }
394
- /**
395
- * The re-key engine over the caller's OPEN handle, INSIDE the caller's
396
- * transaction. Exposed via {@link rekeyStateDb} (which opens + wraps a txn) so
397
- * the Chunk-0b property harness can drive it directly.
398
- */
399
- export function rekeyStateDbCore(db, refMap) {
400
- const report = emptyReport();
401
- ensureLegacyStateTable(db);
402
- for (const spec of SCALAR_REKEY_TABLES)
403
- rekeyScalarTable(db, spec, refMap, report);
404
- for (const spec of EVENT_REKEY_TABLES)
405
- rekeyEventTable(db, spec, refMap, report);
406
- return report;
407
- }
408
- /**
409
- * Open the state.db at `dbPath`, run the full re-key inside its own
410
- * transaction, and close. This is the shape the Chunk-0b `RekeyFn` harness
411
- * drives (`(dbPath, refMap)`); the cutover itself calls {@link rekeyStateDbCore}
412
- * directly inside the ATTACH transaction.
413
- */
414
- export function rekeyStateDb(dbPath, refMap) {
415
- const db = openDatabaseFinalizing(dbPath);
416
- try {
417
- applyStandardPragmas(db, { dataDir: path.dirname(dbPath) });
418
- let report = emptyReport();
419
- db.transaction(() => {
420
- report = rekeyStateDbCore(db, refMap);
421
- })();
422
- return report;
423
- }
424
- finally {
425
- db.close();
426
- }
427
- }
428
- function bump(bucket, key, by = 1) {
429
- bucket[key] = (bucket[key] ?? 0) + by;
430
- }
431
- function rekeyScalarTable(db, spec, refMap, report) {
432
- let rows;
433
- try {
434
- rows = db.prepare(`SELECT rowid AS __rowid, * FROM ${spec.table}`).all();
435
- }
436
- catch (err) {
437
- if (isMissingTableOrColumn(err)) {
438
- report.skipped.push(spec.table);
439
- return;
440
- }
441
- throw err;
442
- }
443
- const groups = new Map();
444
- const orphans = new Map(); // oldRef → rowids
445
- for (const row of rows) {
446
- const key = String(row[spec.keyColumn]);
447
- const resolution = classifyCutoverRef(key, refMap);
448
- if (resolution.kind === "integrity")
449
- throw new CutoverIntegrityError(`${spec.table}: ${resolution.reason}`);
450
- if (resolution.kind === "orphan") {
451
- const list = orphans.get(key) ?? [];
452
- list.push(row.__rowid);
453
- orphans.set(key, list);
454
- continue;
455
- }
456
- const target = resolution.kind === "rekey" ? resolution.target : key; // skip → itself
457
- const group = groups.get(target) ?? [];
458
- group.push(row);
459
- groups.set(target, group);
460
- }
461
- // Expected orphans: audit + delete.
462
- for (const [oldRef, rowids] of orphans) {
463
- quarantineRow(db, spec.table, oldRef, rowids.length, "orphan");
464
- for (const rowid of rowids)
465
- db.prepare(`DELETE FROM ${spec.table} WHERE rowid = ?`).run(rowid);
466
- bump(report.quarantined, spec.table);
467
- }
468
- // Groups: collapse each onto its canonical key (most-recently-updated wins).
469
- for (const [target, group] of groups) {
470
- if (group.length === 1 && String(group[0][spec.keyColumn]) === target)
471
- continue; // already canonical, nothing maps onto it
472
- const winner = group.reduce((best, candidate) => (mruWins(candidate, best, spec.tsColumn) ? candidate : best));
473
- for (const row of group)
474
- db.prepare(`DELETE FROM ${spec.table} WHERE rowid = ?`).run(row.__rowid);
475
- reinsertRow(db, spec.table, winner, spec.keyColumn, target);
476
- if (group.length > 1)
477
- bump(report.merged, spec.table);
478
- bump(report.rekeyed, spec.table);
479
- }
480
- }
481
- /** True when `candidate` should beat `best`: larger tsColumn, ties broken by larger rowid (deterministic). */
482
- function mruWins(candidate, best, tsColumn) {
483
- const ct = Number(candidate[tsColumn] ?? 0);
484
- const bt = Number(best[tsColumn] ?? 0);
485
- if (ct !== bt)
486
- return ct > bt;
487
- return candidate.__rowid > best.__rowid;
488
- }
489
- function reinsertRow(db, table, winner, keyColumn, target) {
490
- const row = {};
491
- for (const [col, value] of Object.entries(winner)) {
492
- if (col === "__rowid")
493
- continue;
494
- row[col] = col === keyColumn ? target : value;
495
- }
496
- const columns = Object.keys(row);
497
- const placeholders = columns.map(() => "?").join(", ");
498
- db.prepare(`INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`).run(...columns.map((c) => row[c]));
499
- }
500
- function rekeyEventTable(db, spec, refMap, report) {
501
- let beforeCount;
502
- let refs;
503
- try {
504
- beforeCount = countRows(db, spec.table);
505
- refs = db
506
- .prepare(`SELECT DISTINCT ${spec.keyColumn} AS ref FROM ${spec.table} WHERE ${spec.keyColumn} IS NOT NULL`)
507
- .all();
508
- }
509
- catch (err) {
510
- if (isMissingTableOrColumn(err)) {
511
- report.skipped.push(spec.table);
512
- return;
513
- }
514
- throw err;
515
- }
516
- let orphanRowsDeleted = 0;
517
- for (const { ref } of refs) {
518
- const resolution = classifyCutoverRef(ref, refMap);
519
- if (resolution.kind === "integrity")
520
- throw new CutoverIntegrityError(`${spec.table}: ${resolution.reason}`);
521
- if (resolution.kind === "skip")
522
- continue;
523
- if (resolution.kind === "orphan") {
524
- const n = db.prepare(`SELECT COUNT(*) AS n FROM ${spec.table} WHERE ${spec.keyColumn} = ?`).get(ref).n;
525
- quarantineRow(db, spec.table, ref, n, "orphan");
526
- db.prepare(`DELETE FROM ${spec.table} WHERE ${spec.keyColumn} = ?`).run(ref);
527
- orphanRowsDeleted += n;
528
- bump(report.quarantined, spec.table);
529
- continue;
530
- }
531
- db.prepare(`UPDATE ${spec.table} SET ${spec.keyColumn} = ? WHERE ${spec.keyColumn} = ?`).run(resolution.target, ref);
532
- bump(report.rekeyed, spec.table);
533
- }
534
- const afterCount = countRows(db, spec.table);
535
- if (afterCount !== beforeCount - orphanRowsDeleted) {
536
- throw new CutoverIntegrityError(`${spec.table}: row-count mismatch after re-key (before ${beforeCount}, deleted-orphans ${orphanRowsDeleted}, after ${afterCount})`);
537
- }
538
- }
539
- function countRows(db, table) {
540
- return db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
541
- }
542
- function ensureCutoverLedger(db) {
543
- db.exec(`
544
- CREATE TABLE IF NOT EXISTS akm_cutover_ledger (
545
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
546
- operation_id TEXT NOT NULL,
547
- merged_at TEXT NOT NULL
548
- );
549
- `);
550
- }
551
- /**
552
- * READ-ONLY check for the committed merge marker. Must NOT create the table:
553
- * this runs BEFORE the cutover transaction, and a `CREATE TABLE` here would be a
554
- * durable pre-transaction write that a later fail-closed rollback could not undo
555
- * (tripping the "state changed outside the journaled transition" guard). The
556
- * table is created inside the transaction alongside the marker INSERT.
557
- */
558
- function cutoverAlreadyMerged(db, operationId) {
559
- if (!tableExists(db, "main", "akm_cutover_ledger"))
560
- return false;
561
- const marker = db.prepare("SELECT operation_id FROM akm_cutover_ledger WHERE singleton = 1").get();
562
- if (!marker)
563
- return false;
564
- if (marker.operation_id !== operationId) {
565
- throw new CutoverIntegrityError(`state.db cutover marker belongs to operation ${marker.operation_id}, not ${operationId}`);
566
- }
567
- return true;
568
- }
569
- /**
570
- * Whether the state.db has already recorded the committed cutover merge marker —
571
- * the durable key the boundary ops (index quarantine, workflow.db unlink) and
572
- * the apply-flow idempotency check consult.
573
- */
574
- export function cutoverMergeCommitted(statePath, operationId) {
575
- if (!fs.existsSync(statePath))
576
- return false;
577
- const db = openDatabaseFinalizing(statePath, { readonly: true });
578
- try {
579
- if (!tableExists(db, "main", "akm_cutover_ledger"))
580
- return false;
581
- const marker = db.prepare("SELECT operation_id FROM akm_cutover_ledger WHERE singleton = 1").get();
582
- return !!marker && (operationId === undefined || marker.operation_id === operationId);
583
- }
584
- finally {
585
- db.close();
586
- }
587
- }
588
- /**
589
- * The full three-DB data step (cutover-design.md §2 step 3). Opens state.db,
590
- * ATTACHes workflow.db + the old index.db read-only OUTSIDE any transaction,
591
- * then in ONE `BEGIN IMMEDIATE`: INSERT…SELECTs the three workflow tables, the
592
- * usage_events rescue (residual legacy `entry_ref` re-keyed via the map), and
593
- * the old index.db `legacy_state` carry, then the FULL state re-key
594
- * ({@link rekeyStateDbCore}), then writes the idempotency marker, COMMITs, and
595
- * DETACHes. Idempotent: a committed marker short-circuits the whole run.
596
- *
597
- * Throws {@link CutoverIntegrityError} on an integrity failure (the apply flow
598
- * converts it into a fail-closed restore). A missing workflow.db skips the merge
599
- * arm (never ATTACHes it — ATTACH would CREATE the file).
600
- */
601
- export function runThreeDbCutover(opts) {
602
- const copied = {};
603
- const db = openDatabaseFinalizing(opts.statePath);
604
- try {
605
- db.exec("PRAGMA busy_timeout = 30000");
606
- if (cutoverAlreadyMerged(db, opts.operationId)) {
607
- return { merged: false, workflowMissing: !fs.existsSync(opts.workflowPath), copied };
608
- }
609
- const workflowExists = fs.existsSync(opts.workflowPath);
610
- const oldIndexExists = fs.existsSync(opts.oldIndexPath);
611
- assertNoStaleAttachments(db);
612
- if (workflowExists)
613
- db.exec(`ATTACH DATABASE '${sqliteQuote(opts.workflowPath)}' AS wf`);
614
- if (oldIndexExists)
615
- db.exec(`ATTACH DATABASE '${sqliteQuote(opts.oldIndexPath)}' AS oldidx`);
616
- let rekey = emptyReport();
617
- try {
618
- db.exec("BEGIN IMMEDIATE");
619
- ensureLegacyStateTable(db);
620
- ensureCutoverLedger(db);
621
- if (workflowExists) {
622
- // Parent-first for the ON DELETE CASCADE foreign keys.
623
- copied.workflow_runs = copyTable(db, "wf", "workflow_runs");
624
- copied.workflow_run_steps = copyTable(db, "wf", "workflow_run_steps");
625
- copied.workflow_run_units = copyTable(db, "wf", "workflow_run_units");
626
- // Normative §11.4: workflow target refs are a MUST-rekey durable
627
- // record. The run-key is a deterministic spelling transform
628
- // (`[origin//]workflow:<n>` → `[origin//]workflows/<n>`) — no index
629
- // join needed, and idempotent (already-new rows don't match).
630
- db.exec(`UPDATE workflow_runs SET workflow_ref =
631
- CASE
632
- WHEN workflow_ref LIKE 'workflow:%'
633
- THEN 'workflows/' || substr(workflow_ref, length('workflow:') + 1)
634
- WHEN instr(workflow_ref, '//workflow:') > 0
635
- THEN substr(workflow_ref, 1, instr(workflow_ref, '//workflow:') + 1)
636
- || 'workflows/'
637
- || substr(workflow_ref, instr(workflow_ref, '//workflow:') + length('//workflow:'))
638
- ELSE workflow_ref
639
- END
640
- WHERE workflow_ref LIKE 'workflow:%' OR instr(workflow_ref, '//workflow:') > 0`);
641
- }
642
- if (oldIndexExists) {
643
- copied.usage_events = rescueUsageEvents(db, opts.refMap);
644
- carryLegacyState(db, "oldidx");
645
- }
646
- rekey = rekeyStateDbCore(db, opts.refMap);
647
- db.prepare("INSERT INTO akm_cutover_ledger (singleton, operation_id, merged_at) VALUES (1, ?, datetime('now'))").run(opts.operationId);
648
- db.exec("COMMIT");
649
- }
650
- catch (error) {
651
- if (db.inTransaction) {
652
- try {
653
- db.exec("ROLLBACK");
654
- }
655
- catch {
656
- // Preserve the original error.
657
- }
658
- }
659
- throw error;
660
- }
661
- finally {
662
- // DETACH must happen OUTSIDE any transaction (an in-txn DETACH fails).
663
- if (oldIndexExists)
664
- safeDetach(db, "oldidx");
665
- if (workflowExists)
666
- safeDetach(db, "wf");
667
- }
668
- // Flush the WAL into the main file and truncate the sidecar to 0 bytes: the
669
- // migration generation fingerprint tracks state.db + its `-wal`/`-shm`
670
- // sidecars, and an uncheckpointed WAL is moved into the main file by the next
671
- // read-only inspect/resume open, desyncing the fingerprint and tripping the
672
- // "does not match the exact live artifact generation" guard. No-op when the
673
- // DB is not in WAL mode. The journal mode is left UNCHANGED (converting it
674
- // can lock a contended WAL db).
675
- try {
676
- db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
677
- }
678
- catch {
679
- // Not in WAL mode / nothing to checkpoint.
680
- }
681
- return { merged: true, workflowMissing: !workflowExists, copied, rekey };
682
- }
683
- finally {
684
- db.close();
685
- }
686
- }
687
- function assertNoStaleAttachments(db) {
688
- const attached = db.prepare("PRAGMA database_list").all().map((r) => r.name);
689
- const stale = attached.filter((name) => name === "wf" || name === "oldidx");
690
- if (stale.length > 0) {
691
- for (const name of stale)
692
- safeDetach(db, name);
693
- }
694
- }
695
- function safeDetach(db, schema) {
696
- try {
697
- db.exec(`DETACH DATABASE ${schema}`);
698
- }
699
- catch {
700
- // Already detached / never attached.
701
- }
702
- }
703
- /** Copy the INTERSECTION of columns from an attached-schema table into the matching `main` table. */
704
- function copyTable(db, srcSchema, table) {
705
- if (!tableExists(db, srcSchema, table))
706
- return 0;
707
- const srcCols = new Set(columnNames(db, srcSchema, table));
708
- const common = columnNames(db, "main", table).filter((c) => srcCols.has(c));
709
- if (common.length === 0)
710
- return 0;
711
- const colList = common.join(", ");
712
- db.exec(`INSERT INTO main.${table} (${colList}) SELECT ${colList} FROM ${srcSchema}.${table}`);
713
- return countRows(db, table);
714
- }
715
- /**
716
- * Rescue the durable index.db `usage_events` history into state.db. Copies the
717
- * column intersection (fresh AUTOINCREMENT ids are fine — `entry_id` is an
718
- * index-generation-scoped provenance column the relink pass re-derives), then
719
- * re-keys residual legacy `entry_ref`s via the map. Rows already in
720
- * `bundle//conceptId` grammar are carried as-is; unmapped legacy rows are KEPT
721
- * in place (append-only history) and recorded in `legacy_state`.
722
- */
723
- function rescueUsageEvents(db, refMap) {
724
- if (!tableExists(db, "oldidx", "usage_events"))
725
- return 0;
726
- const srcCols = new Set(columnNames(db, "oldidx", "usage_events"));
727
- // Never carry the source rowid/id — let state.db mint fresh AUTOINCREMENT ids.
728
- const common = columnNames(db, "main", "usage_events").filter((c) => c !== "id" && srcCols.has(c));
729
- if (common.length > 0) {
730
- const targetColumns = [...common];
731
- const selectColumns = [...common];
732
- // Rows from the pre-provenance schema are historical but not necessarily
733
- // interactive. Preserve them as unattributed rather than manufacturing
734
- // user demand through state.db's legacy column default.
735
- if (!srcCols.has("source")) {
736
- targetColumns.push("source");
737
- selectColumns.push("'unknown'");
738
- }
739
- db.exec(`INSERT INTO main.usage_events (${targetColumns.join(", ")}) ` +
740
- `SELECT ${selectColumns.join(", ")} FROM oldidx.usage_events`);
741
- }
742
- if (!columnNames(db, "main", "usage_events").includes("entry_ref"))
743
- return countRows(db, "usage_events");
744
- const legacyRefs = db.prepare("SELECT DISTINCT entry_ref AS ref FROM main.usage_events WHERE entry_ref IS NOT NULL").all()
745
- .map((r) => r.ref)
746
- .filter((ref) => classifyRefGrammar(ref) === "legacy");
747
- for (const oldRef of legacyRefs) {
748
- const target = refMap.get(oldRef);
749
- if (target !== undefined) {
750
- db.prepare("UPDATE main.usage_events SET entry_ref = ? WHERE entry_ref = ?").run(target, oldRef);
751
- }
752
- else {
753
- // Expected orphan — KEEP the append-only rows in place, archive for audit.
754
- const n = db.prepare("SELECT COUNT(*) AS n FROM main.usage_events WHERE entry_ref = ?").get(oldRef).n;
755
- quarantineRow(db, "usage_events", oldRef, n, "orphan");
756
- }
757
- }
758
- return countRows(db, "usage_events");
759
- }
760
- /** Carry the old index.db `legacy_state` quarantine rows into state.db (durable re-home). */
761
- function carryLegacyState(db, srcSchema) {
762
- if (!tableExists(db, srcSchema, "legacy_state"))
763
- return;
764
- const srcCols = new Set(columnNames(db, srcSchema, "legacy_state"));
765
- const common = ["surface", "old_ref", "row_count", "reason", "quarantined_at"].filter((c) => srcCols.has(c));
766
- if (!common.includes("surface") || !common.includes("old_ref"))
767
- return;
768
- const colList = common.join(", ");
769
- db.exec(`INSERT OR IGNORE INTO main.legacy_state (${colList}) SELECT ${colList} FROM ${srcSchema}.legacy_state`);
770
- }
771
- // ═══════════════════════════════════════════════════════════════════════
772
- // Index/workflow boundary steps (AFTER the committed state txn)
773
- // ═══════════════════════════════════════════════════════════════════════
774
- const DB_SIDECARS = ["-wal", "-shm"];
775
- /**
776
- * Journaled rename of the old index.db (+ `-wal`/`-shm`) to
777
- * `index.db.pre-cutover-<runId>`. Runs AFTER the state txn commits, OUTSIDE the
778
- * fail-closed gate — the next index run rebuilds from scratch (a rebuild failure
779
- * never rolls back the committed cutover). Idempotent + best-effort: a failure
780
- * is logged, never thrown.
781
- */
782
- export function quarantineIndexDb(runId, indexPath) {
783
- try {
784
- const target = `${indexPath}.pre-cutover-${runId}`;
785
- const sourceMainExists = fs.existsSync(indexPath);
786
- const targetMainExists = fs.existsSync(target);
787
- if (sourceMainExists && targetMainExists)
788
- return { quarantined: true, target };
789
- if (!sourceMainExists && !targetMainExists)
790
- return { quarantined: false };
791
- const remainingSidecars = DB_SIDECARS.filter((suffix) => fs.existsSync(`${indexPath}${suffix}`));
792
- if (remainingSidecars.some((suffix) => fs.existsSync(`${target}${suffix}`))) {
793
- warn("[akm] three-DB cutover: index.db quarantine sidecar collision; canonical sidecars were preserved.");
794
- return { quarantined: targetMainExists, ...(targetMainExists ? { target } : {}) };
795
- }
796
- if (sourceMainExists)
797
- fs.renameSync(indexPath, target);
798
- for (const suffix of remainingSidecars)
799
- fs.renameSync(`${indexPath}${suffix}`, `${target}${suffix}`);
800
- return { quarantined: true, target };
801
- }
802
- catch (error) {
803
- warn(`[akm] three-DB cutover: index.db quarantine rename failed (${error instanceof Error ? error.message : String(error)}); the next \`akm index\` rebuilds it — the committed state cutover is unaffected.`);
804
- return { quarantined: false };
805
- }
806
- }
807
- /**
808
- * Journaled, idempotent unlink of workflow.db + its `-wal`/`-shm` sidecars, keyed
809
- * on the committed cutover marker (the caller passes it only once the merge has
810
- * committed). Best-effort: a failure is logged, never thrown.
811
- */
812
- export function deleteWorkflowDb(workflowPath) {
813
- let deleted = false;
814
- try {
815
- for (const suffix of ["-shm", "-wal", ""]) {
816
- const target = `${workflowPath}${suffix}`;
817
- if (fs.existsSync(target)) {
818
- fs.rmSync(target, { force: true });
819
- if (suffix === "")
820
- deleted = true;
821
- }
822
- }
823
- }
824
- catch (error) {
825
- warn(`[akm] three-DB cutover: workflow.db unlink failed (${error instanceof Error ? error.message : String(error)}); it is retried on the next migrate apply.`);
826
- }
827
- return { deleted };
828
- }
829
- // ═══════════════════════════════════════════════════════════════════════
830
- // Small SQL helpers
831
- // ═══════════════════════════════════════════════════════════════════════
832
- function tableExists(db, schema, table) {
833
- return !!db.prepare(`SELECT 1 FROM ${schema}.sqlite_master WHERE type = 'table' AND name = ?`).get(table);
834
- }
835
- function columnNames(db, schema, table) {
836
- return db.prepare(`PRAGMA ${schema}.table_info(${table})`).all().map((r) => r.name);
837
- }
838
- /** Escape single quotes for an inline SQLite string literal (paths only — never user ref data). */
839
- function sqliteQuote(value) {
840
- return value.replace(/'/g, "''");
841
- }