akm-cli 0.9.1 → 0.9.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -1,77 +1,26 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * index.db schema, version stamps, and targeted migrations, kept in the
6
- * storage layer. This isolates the one genuinely risky area (schema
7
- * evolution) from the CRUD/FTS/vector queries.
8
- *
9
- * The meta accessors, embedding purge, and vec-availability probe that
10
- * `ensureSchema` leans on live in the sibling `index-meta-repository` /
11
- * `index-vec-repository` modules.
12
- */
13
- import { bestEffort } from "../../core/best-effort.js";
14
- import { warn } from "../../core/warn.js";
4
+ import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, isCanonicalIndexGeneration, } from "./index-entry-schema.js";
15
5
  import { getMeta, setMeta } from "./index-meta-repository.js";
16
6
  import { isVecAvailable, purgeEmbeddings } from "./index-vec-repository.js";
17
7
  // ── Constants ───────────────────────────────────────────────────────────────
18
- // NOTE: schema changes are additive. DB_VERSION is a forensic stamp only it
19
- // no longer gates any destructive path (the old nuclear drop-and-rebuild was
20
- // removed; index.db's idempotent CREATE IF NOT EXISTS schema converges any
21
- // older/partial DB forward without dropping data). Graph re-keying uses a
22
- // TARGETED, graph-only migration (migrateGraphFilesSchema) — the model for any
23
- // incompatible change: migrate in place, never wipe the whole index.
24
- //
25
- // v17→v18 (Chunk-5 Step 2, spec §14.4): the `entries` table gains the durable
26
- // bundle-adapter identity/provenance columns — `item_ref` (`<bundle>//<concept
27
- // -id>` canonical stored spelling), `bundle_id`/`component_id`/`concept_id`/
28
- // `adapter_id` provenance, `type` (open token), and `content_hash`/`document
29
- // _json`. They land ADDITIVELY ALONGSIDE the legacy `entry_key`/`dir_path`/
30
- // `stash_dir`/`entry_json`/`entry_type` columns (dev-time transitional shape):
31
- // the writer populates the identity/provenance columns while every reader still
32
- // keys on the legacy columns, so the battery stays green while the reader
33
- // repoint + ref-grammar flip land incrementally. The legacy columns + this
34
- // coexistence are removed once every reader is repointed onto `item_ref`
35
- // (spec §3.3 — single clean shape, no dual read-path). The index is a
36
- // regenerable derived cache, so an `akm index` rebuild repopulates the new
37
- // columns on any DB opened at an older version.
8
+ // index.db is a regenerable cache. Incompatible entry-schema changes advance
9
+ // this generation and discard only derived index tables; durable state remains
10
+ // in state.db. Current readers and writers therefore target exactly one schema
11
+ // and never carry live compatibility SQL for previous generations.
38
12
  //
39
- // v18v19 (Chunk-5 flip F4c, spec §11.4): `entries.item_ref` becomes THE durable
40
- // identity its lookup index is upgraded to UNIQUE (every indexed row now
41
- // carries item_ref; NULLs are distinct in a SQLite UNIQUE index, so write-back
42
- // stragglers coexist). The durable state keyed off refs (`usage_events.entry_ref`)
43
- // is re-keyed onto item_ref by the one-time §11.4 migration cutover
44
- // (020-three-db-cutover) with orphan quarantine in `legacy_state`. The index is
45
- // regenerable, so a rebuild is an acceptable fallback if the UNIQUE upgrade finds
46
- // a duplicate on a partially-migrated DB.
13
+ // v20v21: remove the transitional entry_key/dir_path/stash_dir/entry_json/
14
+ // entry_type columns. item_ref is the sole conflict key; document_json is the
15
+ // sole stored document projection; bundle provenance and file_path provide the
16
+ // current identity and materialized read path.
47
17
  //
48
- // v19v20 (Chunk-8 WI-8.3, three-DB merge): index.db STOPS owning `usage_events`
49
- // + `legacy_state`. Both are durable, non-regenerable state and now live in
50
- // state.db (folded by state migration 020; the cutover rescues the old index.db
51
- // rows across). index.db is a regenerable cache, so dropping their DDL from the
52
- // schema path needs no migration — a rebuild simply no longer re-creates them.
53
- // STANDARD REBUILD NOTE: after this bump, any index.db opened at v19 is rebuilt
54
- // from the stash on the next `akm index`; usage_events/legacy_state are read
55
- // exclusively from state.db thereafter.
56
- //
57
- // Reader/writer repoint progress (spec §3.3 "single clean shape"): the `entries`
58
- // upsert (index-entries-repository `getUpsertStmts`) uses the UNIQUE `item_ref`
59
- // as its PRIMARY conflict target, and the graph-boost related-ref reader
60
- // (`listRelatedPathsForFile`) resolves the user-facing ref from
61
- // `concept_id`/`item_ref` instead of stripping `entry_key`. The legacy
62
- // `entry_key`/`dir_path`/`stash_dir`/`entry_type`/`entry_json` columns are NOT
63
- // yet removable: they retain live consumers outside this module — `entry_key`
64
- // (mv-cli re-key, usage-event legacy resolution, index-entry-mapper, the LLM
65
- // cache), `entry_json` (the row payload every reader decodes), `entry_type`
66
- // (FTS + workflow loader), `stash_dir`/`dir_path` (scan/delete/utility scoping).
67
- // The upsert therefore keeps `entry_key` as a NULL-item_ref-safe SECOND conflict
68
- // target (the LLM metadata-enhance re-upsert still writes existing rows with a
69
- // NULL item_ref) and degrades to it entirely when the item_ref index is the
70
- // non-unique fallback. Both are deletable once every remaining reader repoints.
71
- export const DB_VERSION = 20;
18
+ // v21v22: entry mutations publish FTS synchronously and no dirty queue exists.
19
+ // Discard the old derived generation so stale FTS rows and caller-managed dirty
20
+ // state cannot cross the mutation-authority boundary.
21
+ export const DB_VERSION = CANONICAL_INDEX_DB_VERSION;
72
22
  export const EMBEDDING_DIM = 384;
73
- // #624-P1: graph_files re-keyed to (stash_root, file_path, body_hash). Bumped 3→4
74
- // as a marker; the actual migration is the targeted drop in migrateGraphFilesSchema.
23
+ // #624-P1: graph_files is keyed to (stash_root, file_path, body_hash).
75
24
  export const GRAPH_SCHEMA_VERSION = 4;
76
25
  // ── Schema ──────────────────────────────────────────────────────────────────
77
26
  /**
@@ -111,9 +60,7 @@ const REGISTRY_INDEX_CACHE_DDL = `
111
60
  `;
112
61
  /**
113
62
  * Create the graph-extraction tables (`graph_meta`/`graph_files`/`graph_file_entities`/
114
- * `graph_file_relations`/`graph_extraction_queue`). Extracted verbatim from
115
- * {@link ensureSchema} (called at the same point, between `migrateGraphFilesSchema`
116
- * and `migrateGraphDataFromLegacy`) — a pure, behavior-identical decomposition.
63
+ * `graph_file_relations`/`graph_extraction_queue`).
117
64
  */
118
65
  function ensureGraphTables(db) {
119
66
  db.exec(`
@@ -205,6 +152,48 @@ function ensureGraphTables(db) {
205
152
  ON graph_extraction_queue(stash_root, priority DESC, queued_at);
206
153
  `);
207
154
  }
155
+ /**
156
+ * Cross the incompatible entry-schema boundary by discarding the derived index
157
+ * generation. No row conversion or dual-schema compatibility is attempted:
158
+ * the next index run rebuilds entries, FTS, embeddings, utility aggregates,
159
+ * graph extraction, and enrichment caches from current sources/state.
160
+ */
161
+ function rebuildIncompatibleIndexGeneration(db) {
162
+ const version = getMeta(db, "version");
163
+ const hasEntries = tableExists(db, "entries");
164
+ if (!hasEntries && version === undefined)
165
+ return;
166
+ if (isCanonicalIndexGeneration(db))
167
+ return;
168
+ let vecResetPending = false;
169
+ try {
170
+ db.exec("DROP TABLE IF EXISTS entries_vec");
171
+ }
172
+ catch {
173
+ // A vec0 table cannot be dropped while sqlite-vec is unavailable. It does
174
+ // not reference entries, so leave a marker and drop it on the first later
175
+ // open where the extension is available.
176
+ vecResetPending = true;
177
+ }
178
+ db.transaction(() => {
179
+ db.exec("DROP TABLE IF EXISTS graph_file_relations");
180
+ db.exec("DROP TABLE IF EXISTS graph_file_entities");
181
+ db.exec("DROP TABLE IF EXISTS graph_files");
182
+ db.exec("DROP TABLE IF EXISTS graph_extraction_queue");
183
+ db.exec("DROP TABLE IF EXISTS graph_meta");
184
+ db.exec("DROP TABLE IF EXISTS entries_fts_dirty");
185
+ db.exec("DROP TABLE IF EXISTS entries_fts");
186
+ db.exec("DROP TABLE IF EXISTS embeddings");
187
+ db.exec("DROP TABLE IF EXISTS utility_scores_scoped");
188
+ db.exec("DROP TABLE IF EXISTS utility_scores");
189
+ db.exec("DROP TABLE IF EXISTS llm_enrichment_cache");
190
+ db.exec("DROP TABLE IF EXISTS index_dir_state");
191
+ db.exec("DROP TABLE IF EXISTS entries");
192
+ db.exec("DELETE FROM index_meta");
193
+ })();
194
+ if (vecResetPending)
195
+ setMeta(db, "vecResetPending", "1");
196
+ }
208
197
  export function ensureSchema(db, embeddingDim) {
209
198
  // Create meta table first so we can check version
210
199
  db.exec(`
@@ -213,87 +202,14 @@ export function ensureSchema(db, embeddingDim) {
213
202
  value TEXT NOT NULL
214
203
  );
215
204
  `);
216
- // index.db is a fully regenerable derived cache, so its schema is built
217
- // idempotently below: every table is CREATE … IF NOT EXISTS and column
218
- // additions go through guarded ALTERs (ensureDerivedFromColumn) and targeted
219
- // migrations (migrateGraphFilesSchema / migrateGraphDataFromLegacy). Opening a
220
- // database with an older or partial schema converges it forward WITHOUT ever
221
- // dropping data — there is intentionally no "nuclear drop the whole index on a
222
- // DB_VERSION mismatch" path (a destructive design the regenerable index never
223
- // needed, and whose pre-drop data-dir backup it required). A genuinely
224
- // incompatible change is handled by an additive/targeted migration; the few
225
- // derived tables that ever must be rebuilt are regenerated by `akm index`.
226
- db.exec(`
227
- CREATE TABLE IF NOT EXISTS entries (
228
- id INTEGER PRIMARY KEY AUTOINCREMENT,
229
- entry_key TEXT NOT NULL UNIQUE,
230
- dir_path TEXT NOT NULL,
231
- file_path TEXT NOT NULL,
232
- stash_dir TEXT NOT NULL,
233
- entry_json TEXT NOT NULL,
234
- search_text TEXT NOT NULL,
235
- entry_type TEXT NOT NULL,
236
- derived_from TEXT,
237
- -- Chunk-5 Step 2 / DB v18 (spec 14.4): bundle-adapter identity + provenance,
238
- -- ADDITIVE alongside the legacy columns above. item_ref is the durable
239
- -- <bundle>//<concept-id> spelling; nullable during the transition so a
240
- -- pre-repoint reader path never trips a NOT NULL on a partially-migrated row.
241
- item_ref TEXT,
242
- bundle_id TEXT,
243
- component_id TEXT,
244
- concept_id TEXT,
245
- adapter_id TEXT,
246
- type TEXT,
247
- content_hash TEXT,
248
- document_json TEXT
249
- );
250
-
251
- CREATE INDEX IF NOT EXISTS idx_entries_dir ON entries(dir_path);
252
- CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
253
- CREATE INDEX IF NOT EXISTS idx_entries_file_path ON entries(file_path);
254
- `);
255
- // v18: backfill the bundle-adapter identity/provenance columns on databases
256
- // created against a pre-v18 binary (partial schema) — same PRAGMA-then-ALTER
257
- // guard pattern as `ensureDerivedFromColumn`. Runs BEFORE the item_ref index
258
- // so the CREATE INDEX below never references a not-yet-added column.
259
- ensureBundleRefColumns(db);
260
- // v19 (F4c, spec §11.4): item_ref is THE durable identity — its index is
261
- // UNIQUE. Every indexed row now carries item_ref; SQLite treats NULLs as
262
- // distinct in a UNIQUE index, so NULL-item_ref write-back stragglers coexist.
263
- // A pre-v19 DB carries a NON-unique `idx_entries_item_ref`, so drop-then-create.
264
- ensureUniqueItemRefIndex(db);
265
- // Phase 5A / DB v17: backfill `derived_from` column + index on databases
266
- // that were created at v17 fresh OR carry a partial v17 schema (a DB whose
267
- // `index_meta.version` was bumped to 17 but whose `entries` table still
268
- // lacks the column — this happens when a previous v17 binary opened a
269
- // pre-v17 DB without taking the upgrade path because no version mismatch
270
- // was seen at boot). The PRAGMA-then-ALTER guard runs unconditionally so
271
- // both fresh and partial schemas converge. The CREATE INDEX for
272
- // `derived_from` MUST run after this helper so we never reference a
273
- // column that has not yet been added on partial schemas.
274
- ensureDerivedFromColumn(db);
275
- // Validated WorkflowDocument JSON, one row per indexed workflow entry.
276
- // Pure index data — fully rebuilt on each `akm index`. ON DELETE CASCADE
277
- // means clearing entries (full rebuild or per-dir delete) drops these too.
278
- db.exec(`
279
- CREATE TABLE IF NOT EXISTS workflow_documents (
280
- entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE,
281
- schema_version INTEGER NOT NULL,
282
- document_json TEXT NOT NULL,
283
- source_path TEXT NOT NULL,
284
- source_hash TEXT NOT NULL,
285
- updated_at TEXT NOT NULL
286
- );
287
-
288
- CREATE INDEX IF NOT EXISTS idx_workflow_documents_source_path
289
- ON workflow_documents(source_path);
290
- `);
291
- // Set version immediately after table creation so a crash before the end of
292
- // ensureSchema() does not leave the database in a versionless state on next open.
293
- const versionAfterCreate = getMeta(db, "version");
294
- if (!versionAfterCreate) {
295
- setMeta(db, "version", String(DB_VERSION));
296
- }
205
+ rebuildIncompatibleIndexGeneration(db);
206
+ db.exec(CANONICAL_ENTRY_SCHEMA_SQL);
207
+ // Workflow source is compiled directly into source IR at each command
208
+ // boundary. The former workflow_documents cache duplicated that IR in a
209
+ // second persisted representation and was never used by current execution.
210
+ // index.db is derived state, so remove the obsolete table on every open.
211
+ db.exec("DROP TABLE IF EXISTS workflow_documents");
212
+ setMeta(db, "version", String(DB_VERSION));
297
213
  // BLOB-based embedding storage (always available, no sqlite-vec needed)
298
214
  db.exec(`
299
215
  CREATE TABLE IF NOT EXISTS embeddings (
@@ -317,9 +233,8 @@ export function ensureSchema(db, embeddingDim) {
317
233
  );
318
234
  `);
319
235
  }
320
- // usage_events + legacy_state moved to state.db (Chunk-8 WI-8.3, DB_VERSION
321
- // v20). index.db no longer creates them; usage_events writers/readers open
322
- // state.db. utility_scores (a regenerable index.db cache) stays here.
236
+ // usage_events lives in state.db. utility_scores remains a regenerable
237
+ // index.db cache.
323
238
  // Utility scores table (aggregated per-entry utility metrics)
324
239
  db.exec(`
325
240
  CREATE TABLE IF NOT EXISTS utility_scores (
@@ -360,7 +275,7 @@ export function ensureSchema(db, embeddingDim) {
360
275
  // result for each asset so that subsequent `akm index --enrich` runs can
361
276
  // skip the LLM call when the body hasn't changed. The cache is keyed by
362
277
  // a stable asset_ref string (e.g. the absolute file path for graph/memory
363
- // passes, or `entryKey:passId` for the metadata-enhance pass).
278
+ // passes, or `itemRef:passId` for the metadata-enhance pass).
364
279
  // Entries are cleaned up when assets are removed or --re-enrich is used.
365
280
  db.exec(`
366
281
  CREATE TABLE IF NOT EXISTS llm_enrichment_cache (
@@ -389,27 +304,13 @@ export function ensureSchema(db, embeddingDim) {
389
304
  // body_hash) and declare a composite FK -> graph_files ON DELETE CASCADE so
390
305
  // child rows are removed when a graph_files row is replaced.
391
306
  //
392
- // #624-P1 targeted migration: an existing DB may still hold the OLD graph_files
393
- // (entry_id PK). SQLite can't ALTER a primary key, so we RENAME the 3 graph
394
- // tables aside (→ *_legacy) here — ONLY the graph tables, never the index/
395
- // embeddings — then the CREATE block below builds the new shape, then
396
- // migrateGraphDataFromLegacy() copies the data across so the graph is PRESERVED
397
- // (not re-extracted).
398
- migrateGraphFilesSchema(db);
399
307
  ensureGraphTables(db);
400
- // #624-P1 migration step 2: copy any renamed-aside legacy graph data into the
401
- // new-shape tables (just created above), then drop the legacy tables. No-op
402
- // unless migrateGraphFilesSchema renamed a legacy graph_files this open.
403
- migrateGraphDataFromLegacy(db);
404
- // FTS-dirty queue. Created here (not lazily on first upsert) so the
405
- // per-entry write path doesn't issue a CREATE TABLE IF NOT EXISTS on
406
- // every call — that DDL would fire thousands of times during a full
407
- // index. See `markFtsDirty` and `rebuildFts({ incremental: true })`.
408
- db.exec(`
409
- CREATE TABLE IF NOT EXISTS entries_fts_dirty (
410
- entry_id INTEGER PRIMARY KEY
411
- );
412
- `);
308
+ // If a generation rebuild could not drop a vec0 table while the extension
309
+ // was unavailable, finish that reset as soon as vec0 can be loaded again.
310
+ if (isVecAvailable(db) && getMeta(db, "vecResetPending") === "1") {
311
+ db.exec("DROP TABLE IF EXISTS entries_vec");
312
+ setMeta(db, "vecResetPending", "0");
313
+ }
413
314
  // sqlite-vec table
414
315
  //
415
316
  // Dimension contract:
@@ -471,86 +372,6 @@ export function ensureSchema(db, embeddingDim) {
471
372
  // `akm search` does not hit the network on every invocation.
472
373
  db.exec(REGISTRY_INDEX_CACHE_DDL);
473
374
  }
474
- /**
475
- * Phase 5A / DB v17 schema guard.
476
- *
477
- * Ensures the `entries.derived_from` column + index exist on the open
478
- * connection. Called from `ensureSchema()` after the entries CREATE so that
479
- * legacy databases (created against a pre-v17 binary) still gain the new column
480
- * without data loss. Idempotent: a `PRAGMA table_info` lookup gates the ALTER.
481
- */
482
- function ensureDerivedFromColumn(db) {
483
- bestEffort(() => {
484
- const cols = db.prepare("PRAGMA table_info(entries)").all();
485
- const hasColumn = cols.some((c) => c.name === "derived_from");
486
- if (!hasColumn) {
487
- db.exec("ALTER TABLE entries ADD COLUMN derived_from TEXT");
488
- }
489
- // Index creation is idempotent on its own; safe to call unconditionally.
490
- db.exec("CREATE INDEX IF NOT EXISTS idx_entries_derived_from ON entries(derived_from)");
491
- }, "entries table may not exist on a brand-new DB before CREATE — caller is responsible");
492
- }
493
- /**
494
- * Chunk-5 Step 2 / DB v18 schema guard.
495
- *
496
- * Ensures the bundle-adapter identity/provenance columns exist on the open
497
- * `entries` table. Called from `ensureSchema()` after the entries CREATE so a
498
- * legacy database (created against a pre-v18 binary) gains the new columns
499
- * without a rebuild. All columns are nullable and added ADDITIVELY — the
500
- * writer populates `item_ref`/`bundle_id`/`component_id`/`concept_id`/
501
- * `adapter_id`/`type` while readers still key on the legacy columns. Idempotent:
502
- * a `PRAGMA table_info` lookup gates each ALTER.
503
- */
504
- function ensureBundleRefColumns(db) {
505
- bestEffort(() => {
506
- const cols = db.prepare("PRAGMA table_info(entries)").all();
507
- const have = new Set(cols.map((c) => c.name));
508
- const additive = [
509
- ["item_ref", "TEXT"],
510
- ["bundle_id", "TEXT"],
511
- ["component_id", "TEXT"],
512
- ["concept_id", "TEXT"],
513
- ["adapter_id", "TEXT"],
514
- ["type", "TEXT"],
515
- ["content_hash", "TEXT"],
516
- ["document_json", "TEXT"],
517
- ];
518
- for (const [name, sqlType] of additive) {
519
- if (!have.has(name))
520
- db.exec(`ALTER TABLE entries ADD COLUMN ${name} ${sqlType}`);
521
- }
522
- }, "entries table may not exist on a brand-new DB before CREATE — caller is responsible");
523
- }
524
- /**
525
- * Chunk-5 flip F4c / DB v19 schema guard.
526
- *
527
- * Upgrade `entries.item_ref`'s lookup index to UNIQUE (item_ref is now THE
528
- * durable identity — spec §11.4). A pre-v19 DB has a NON-unique
529
- * `idx_entries_item_ref`, so we DROP-then-CREATE (a `CREATE UNIQUE INDEX IF NOT
530
- * EXISTS` under the same name would no-op against the existing non-unique index).
531
- * SQLite treats NULLs as distinct in a UNIQUE index. Duplicate durable identities
532
- * are an invalid index and fail the schema open rather than enabling a dual-key
533
- * fallback.
534
- */
535
- function ensureUniqueItemRefIndex(db) {
536
- // Probe before mutating. This ran unconditionally on EVERY open as two
537
- // separate autocommit statements, so there was always a window in which the
538
- // index did not exist — a concurrent open (registry-cache search, indexer,
539
- // improve) could DROP between the other's DROP and CREATE and then fail with
540
- // "index idx_entries_item_ref already exists", or serve a query with no index
541
- // at all. A DB whose index is already UNIQUE needs no work.
542
- const existing = db
543
- .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'idx_entries_item_ref'")
544
- .get();
545
- if (existing?.sql && /\bUNIQUE\b/i.test(existing.sql))
546
- return;
547
- // Pre-v19 (non-unique index) or absent: convert atomically so a racing open
548
- // sees either the old index or the new one, never neither.
549
- db.transaction(() => {
550
- db.exec("DROP INDEX IF EXISTS idx_entries_item_ref");
551
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_entries_item_ref ON entries(item_ref)");
552
- })();
553
- }
554
375
  /**
555
376
  * Returns true when a table exists in the current database.
556
377
  */
@@ -558,100 +379,3 @@ function tableExists(db, name) {
558
379
  const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1").get(name);
559
380
  return row !== undefined && row !== null;
560
381
  }
561
- /**
562
- * #624-P1 targeted graph-schema migration — STEP 1 of 2 (rename).
563
- *
564
- * graph_files was re-keyed from `entry_id INTEGER PRIMARY KEY REFERENCES
565
- * entries(id)` to a self-contained `(stash_root, file_path, body_hash)` PK.
566
- * SQLite cannot ALTER a primary key, so an existing DB carrying the OLD shape
567
- * has its 3 graph tables RENAMED to `*_legacy` here; ensureSchema's CREATE block
568
- * then builds the new-shape tables, and {@link migrateGraphDataFromLegacy} COPIES
569
- * the data across before dropping the legacy tables. The graph is preserved —
570
- * NOT re-extracted (re-extraction is ~19s/file of LLM work).
571
- *
572
- * Crucially this is GRAPH-SCOPED: it touches ONLY the graph tables, never the
573
- * index / embeddings / enrichment cache. So users keep their (expensive)
574
- * embeddings instead of being forced into a full re-embed by a DB_VERSION bump.
575
- *
576
- * Detection: the old schema has an `entry_id` column on graph_files. Fresh DBs
577
- * (no graph_files yet) and already-migrated DBs (no entry_id column) are no-ops.
578
- * Idempotent.
579
- */
580
- function migrateGraphFilesSchema(db) {
581
- bestEffort(() => {
582
- const cols = db.prepare("PRAGMA table_info(graph_files)").all();
583
- const isLegacyShape = cols.some((c) => c.name === "entry_id");
584
- if (!isLegacyShape)
585
- return;
586
- // A previous interrupted migration may have left *_legacy behind — drop those
587
- // husks first so the rename below doesn't collide.
588
- db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
589
- db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
590
- db.exec("DROP TABLE IF EXISTS graph_files_legacy");
591
- // Rename the 3 entry_id-keyed tables aside. graph_meta is unchanged (stash_root
592
- // key) so it is left in place. ALTER … RENAME auto-updates child FK refs in
593
- // SQLite ≥3.25, which is fine — the legacy children are dropped after the copy.
594
- db.exec("ALTER TABLE graph_files RENAME TO graph_files_legacy");
595
- if (tableExists(db, "graph_file_entities")) {
596
- db.exec("ALTER TABLE graph_file_entities RENAME TO graph_file_entities_legacy");
597
- }
598
- if (tableExists(db, "graph_file_relations")) {
599
- db.exec("ALTER TABLE graph_file_relations RENAME TO graph_file_relations_legacy");
600
- }
601
- }, "graph_files may not exist on a brand-new DB before CREATE — caller is responsible");
602
- }
603
- /**
604
- * #624-P1 targeted graph-schema migration — STEP 2 of 2 (copy + drop legacy).
605
- *
606
- * Runs AFTER the graph CREATE TABLE block, so the new-shape tables exist. Copies
607
- * every legacy row into the re-keyed tables — the old tables already carry
608
- * (stash_root, file_path, body_hash) next to entry_id, so the projection is a
609
- * straight column copy (children JOIN back to graph_files_legacy to resolve the
610
- * composite key from their entry_id). Then drops the `*_legacy` tables.
611
- *
612
- * Best-effort: a copy failure (e.g. a pre-body_hash legacy schema) is tolerated,
613
- * and the legacy tables are dropped regardless so they never linger. Rows whose
614
- * body_hash is null/empty can't form the new PK and are skipped (they re-extract).
615
- */
616
- function migrateGraphDataFromLegacy(db) {
617
- if (!tableExists(db, "graph_files_legacy"))
618
- return;
619
- let migratedFiles = 0;
620
- bestEffort(() => {
621
- db.transaction(() => {
622
- const res = db
623
- .prepare(`INSERT OR IGNORE INTO graph_files
624
- (stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id)
625
- SELECT stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id
626
- FROM graph_files_legacy
627
- WHERE body_hash IS NOT NULL AND body_hash != ''`)
628
- .run();
629
- migratedFiles = Number(res.changes);
630
- if (tableExists(db, "graph_file_entities_legacy")) {
631
- db.exec(`INSERT OR IGNORE INTO graph_file_entities
632
- (stash_root, file_path, body_hash, entity_order, entity_norm, entity)
633
- SELECT gf.stash_root, gf.file_path, gf.body_hash, e.entity_order, e.entity_norm, e.entity
634
- FROM graph_file_entities_legacy e
635
- JOIN graph_files_legacy gf ON gf.entry_id = e.entry_id
636
- WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
637
- }
638
- if (tableExists(db, "graph_file_relations_legacy")) {
639
- db.exec(`INSERT OR IGNORE INTO graph_file_relations
640
- (stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence)
641
- SELECT gf.stash_root, gf.file_path, gf.body_hash, r.relation_order, r.from_entity_norm, r.from_entity, r.to_entity_norm, r.to_entity, r.relation_type, r.confidence
642
- FROM graph_file_relations_legacy r
643
- JOIN graph_files_legacy gf ON gf.entry_id = r.entry_id
644
- WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
645
- }
646
- })();
647
- }, "graph data migration is best-effort; legacy tables are dropped regardless below");
648
- // Always drop the legacy tables (children first), migrated or not.
649
- bestEffort(() => {
650
- db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
651
- db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
652
- db.exec("DROP TABLE IF EXISTS graph_files_legacy");
653
- }, "drop legacy graph tables after migration");
654
- if (migratedFiles > 0) {
655
- warn(`[akm] graph index re-keyed (#624): migrated ${migratedFiles} extracted file(s) to the new schema — no re-extraction needed. Index + embeddings untouched.`);
656
- }
657
- }
@@ -8,7 +8,6 @@
8
8
  * retrieval-frequency counting over `usage_events`. The bounded-step EMA policy
9
9
  * itself lives in `indexer/feedback/utility-policy`; this repo only reads/writes.
10
10
  */
11
- import path from "node:path";
12
11
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
13
12
  import { computeNextUtility } from "../../indexer/feedback/utility-policy.js";
14
13
  import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
@@ -145,7 +144,7 @@ function bareRefCandidates(ref) {
145
144
  export function getRetrievalCounts(indexDb, stateDb, refs, options = {}) {
146
145
  if (refs.length === 0)
147
146
  return new Map();
148
- if (options.sourceName || options.stashDir) {
147
+ if (options.sourceName) {
149
148
  return getSourceScopedRetrievalCounts(indexDb, stateDb, refs, options);
150
149
  }
151
150
  // Map each conceptId back to the input refs that produced it so DB results
@@ -218,20 +217,18 @@ function getSourceScopedRetrievalCounts(indexDb, stateDb, refs, options) {
218
217
  bareToInputs.set(bare, [ref]);
219
218
  }
220
219
  }
221
- // Cross-DB (Chunk-8 WI-8.3): usage_events rows come from state.db; the
222
- // per-row `stash_dir` (formerly a LEFT JOIN on entries) is resolved from
223
- // index.db by entry_id. Read the usage rows first, then batch-look-up the
224
- // stash_dir for their entry_ids and join in JS.
225
- const entryStashDir = indexDb.prepare("SELECT stash_dir FROM entries WHERE id = ?");
226
- const stashDirFor = (entryId) => {
220
+ // Cross-DB: usage rows come from state.db; resolve the linked entry's
221
+ // canonical bundle identity from index.db by entry_id.
222
+ const entryBundle = indexDb.prepare("SELECT bundle_id FROM entries WHERE id = ?");
223
+ const bundleFor = (entryId) => {
227
224
  if (entryId === null)
228
225
  return null;
229
- const row = entryStashDir.get(entryId);
230
- return row?.stash_dir ?? null;
226
+ const row = entryBundle.get(entryId);
227
+ return row?.bundle_id ?? null;
231
228
  };
232
229
  const countsByBare = new Map();
233
230
  const bareForms = [...bareToInputs.keys()];
234
- const selectedRoot = options.stashDir ? path.resolve(options.stashDir) : undefined;
231
+ const selectedBundle = options.sourceName;
235
232
  for (let i = 0; i < bareForms.length; i += SQLITE_CHUNK_SIZE) {
236
233
  const chunk = bareForms.slice(i, i + SQLITE_CHUNK_SIZE);
237
234
  const placeholders = chunk.map(() => "?").join(", ");
@@ -248,15 +245,13 @@ function getSourceScopedRetrievalCounts(indexDb, stateDb, refs, options) {
248
245
  ELSE ue.entry_ref
249
246
  END IN (${placeholders})`)
250
247
  .all(...chunk);
251
- const rows = rawRows.map((r) => ({ ...r, stash_dir: stashDirFor(r.entry_id) }));
248
+ const rows = rawRows.map((r) => ({ ...r, bundle_id: bundleFor(r.entry_id) }));
252
249
  for (const row of rows) {
253
250
  const bare = bareRef(row.entry_ref);
254
- const linkedToSelectedRoot = row.entry_id !== null && selectedRoot !== undefined && row.stash_dir !== null
255
- ? path.resolve(row.stash_dir) === selectedRoot
256
- : false;
257
- const detached = row.entry_id === null || selectedRoot === undefined;
251
+ const linkedToSelectedBundle = row.entry_id !== null && row.bundle_id === selectedBundle;
252
+ const detached = row.entry_id === null;
258
253
  const qualifiedForSource = detached && options.sourceName !== undefined && row.entry_ref === `${options.sourceName}//${bare}`;
259
- if (!linkedToSelectedRoot && !qualifiedForSource)
254
+ if (!linkedToSelectedBundle && !qualifiedForSource)
260
255
  continue;
261
256
  countsByBare.set(bare, (countsByBare.get(bare) ?? 0) + 1);
262
257
  }
@@ -12,6 +12,7 @@ import { bestEffort } from "../../core/best-effort.js";
12
12
  import { warn } from "../../core/warn.js";
13
13
  import { cosineSimilarity } from "../../llm/embedders/types.js";
14
14
  import { getMeta, setMeta } from "./index-meta-repository.js";
15
+ import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
15
16
  // ── sqlite-vec extension ────────────────────────────────────────────────────
16
17
  const vecStatus = new WeakMap();
17
18
  /**
@@ -68,6 +69,43 @@ export function isVecFastPathReady(db) {
68
69
  // flag, so the read path has to verify the table really exists.
69
70
  return hasVecTable(db);
70
71
  }
72
+ /**
73
+ * Verify that the vec fast-path table mirrors the complete durable BLOB set.
74
+ *
75
+ * A targeted embedding write preserves the prior readiness decision because
76
+ * its subset cannot prove an older degraded generation is healed. Global
77
+ * materialization uses this aggregate check before promoting the persisted
78
+ * flag; search itself still reads the cheap flag and does not repeat the check
79
+ * per query.
80
+ */
81
+ export function isVecFastPathComplete(db) {
82
+ if (!isVecAvailable(db) || !hasVecTable(db))
83
+ return false;
84
+ try {
85
+ const missingVecRows = db
86
+ .prepare(`
87
+ SELECT id FROM embeddings
88
+ EXCEPT
89
+ SELECT id FROM entries_vec
90
+ LIMIT 1
91
+ `)
92
+ .all();
93
+ if (missingVecRows.length > 0)
94
+ return false;
95
+ const orphanVecRows = db
96
+ .prepare(`
97
+ SELECT id FROM entries_vec
98
+ EXCEPT
99
+ SELECT id FROM embeddings
100
+ LIMIT 1
101
+ `)
102
+ .all();
103
+ return orphanVecRows.length === 0;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
71
109
  const vecTablePresent = new WeakMap();
72
110
  /**
73
111
  * Whether `entries_vec` exists on this connection, memoized per handle.
@@ -274,13 +312,24 @@ function searchBlobVec(db, queryEmbedding, k) {
274
312
  * Return all entries that do not yet have an embedding row.
275
313
  * Used by the embedding phase to determine which entries need vectors generated.
276
314
  */
277
- export function getAllEntriesForEmbedding(db) {
278
- return db
279
- .prepare(`
280
- SELECT e.id, e.search_text AS searchText, e.entry_key AS entryKey, e.file_path AS filePath FROM entries e
281
- WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
282
- `)
283
- .all();
315
+ export function getAllEntriesForEmbedding(db, entryIds) {
316
+ const select = `
317
+ SELECT e.id, e.search_text AS searchText, e.item_ref AS itemRef, e.file_path AS filePath FROM entries e
318
+ `;
319
+ const missing = "NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)";
320
+ if (entryIds === undefined) {
321
+ return db.prepare(`${select} WHERE ${missing} ORDER BY e.id`).all();
322
+ }
323
+ const targets = [...new Set(entryIds)].sort((left, right) => left - right);
324
+ const rows = [];
325
+ for (let offset = 0; offset < targets.length; offset += SQLITE_CHUNK_SIZE) {
326
+ const chunk = targets.slice(offset, offset + SQLITE_CHUNK_SIZE);
327
+ if (chunk.length === 0)
328
+ continue;
329
+ const placeholders = chunk.map(() => "?").join(",");
330
+ rows.push(...db.prepare(`${select} WHERE e.id IN (${placeholders}) AND ${missing} ORDER BY e.id`).all(...chunk));
331
+ }
332
+ return rows;
284
333
  }
285
334
  export function getEmbeddingCount(db) {
286
335
  const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();