akm-cli 0.9.0-rc.1 → 0.9.0-rc.13

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 (587) hide show
  1. package/CHANGELOG.md +1190 -52
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/stash-skeleton/README.md +38 -10
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  38. package/dist/assets/tasks/core/extract.yml +3 -2
  39. package/dist/assets/tasks/core/improve.yml +2 -1
  40. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  41. package/dist/assets/tasks/core/sync.yml +1 -0
  42. package/dist/assets/tasks/core/version-check.yml +2 -1
  43. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  44. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  45. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  46. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  48. package/dist/assets/templates/html/health.html +5 -4
  49. package/dist/assets/workflows/workflow-template.md +31 -15
  50. package/dist/cli/invocation.js +279 -0
  51. package/dist/cli/parse-args.js +5 -90
  52. package/dist/cli/retired-commands.js +78 -0
  53. package/dist/cli/shared.js +158 -48
  54. package/dist/cli-node.mjs +2 -1
  55. package/dist/cli.js +747 -293
  56. package/dist/commands/agent/agent-dispatch.js +19 -18
  57. package/dist/commands/agent/agent-support.js +0 -24
  58. package/dist/commands/agent/contribute-cli.js +43 -97
  59. package/dist/commands/completions.js +80 -23
  60. package/dist/commands/config-cli.js +44 -281
  61. package/dist/commands/env/env-binding.js +13 -9
  62. package/dist/commands/env/env-cli.js +76 -159
  63. package/dist/commands/env/env.js +12 -163
  64. package/dist/commands/env/marker-path.js +6 -0
  65. package/dist/commands/env/secret-cli.js +45 -61
  66. package/dist/commands/env/secret.js +32 -62
  67. package/dist/commands/feedback-cli.js +179 -85
  68. package/dist/commands/health/accept-rate.js +58 -0
  69. package/dist/commands/health/advisories.js +7 -8
  70. package/dist/commands/health/checks.js +279 -94
  71. package/dist/commands/health/html-report.js +197 -578
  72. package/dist/commands/health/improve-metrics.js +277 -246
  73. package/dist/commands/health/llm-usage.js +19 -19
  74. package/dist/commands/health/md-report.js +16 -7
  75. package/dist/commands/health/metrics.js +67 -32
  76. package/dist/commands/health/renderers.js +47 -0
  77. package/dist/commands/health/report-view-model.js +508 -0
  78. package/dist/commands/health/stash-exposure.js +1 -1
  79. package/dist/commands/health/surfaces.js +16 -56
  80. package/dist/commands/health/task-runs.js +3 -67
  81. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  82. package/dist/commands/health/types-improve.js +29 -0
  83. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  84. package/dist/commands/health/types-result.js +7 -0
  85. package/dist/commands/health/types-runs.js +4 -0
  86. package/dist/commands/health/types-session-log.js +4 -0
  87. package/dist/commands/health/types-windows.js +4 -0
  88. package/dist/commands/health/types.js +26 -21
  89. package/dist/commands/health/windows.js +2 -3
  90. package/dist/commands/health.js +296 -167
  91. package/dist/commands/improve/anti-collapse.js +5 -5
  92. package/dist/commands/improve/autonomy-gate.js +68 -0
  93. package/dist/commands/improve/collapse-detector.js +65 -52
  94. package/dist/commands/improve/consolidate/chunking.js +9 -7
  95. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  96. package/dist/commands/improve/consolidate/merge.js +4 -0
  97. package/dist/commands/improve/consolidate.js +454 -1354
  98. package/dist/commands/improve/content-hash.js +39 -0
  99. package/dist/commands/improve/distill/content-repair.js +4 -10
  100. package/dist/commands/improve/distill/promote-memory.js +89 -64
  101. package/dist/commands/improve/distill/quality-gate.js +118 -42
  102. package/dist/commands/improve/distill-guards.js +1 -1
  103. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  104. package/dist/commands/improve/distill.js +607 -363
  105. package/dist/commands/improve/eligibility.js +165 -79
  106. package/dist/commands/improve/extract-cli.js +35 -126
  107. package/dist/commands/improve/extract-prompt.js +6 -35
  108. package/dist/commands/improve/extract.js +640 -391
  109. package/dist/commands/improve/feedback-valence.js +2 -12
  110. package/dist/commands/improve/improve-cli.js +134 -135
  111. package/dist/commands/improve/improve-result-file.js +30 -50
  112. package/dist/commands/improve/improve-run-types.js +4 -0
  113. package/dist/commands/improve/improve-strategies.js +135 -0
  114. package/dist/commands/improve/improve.js +904 -701
  115. package/dist/commands/improve/locks.js +64 -111
  116. package/dist/commands/improve/loop-stages.js +1110 -923
  117. package/dist/commands/improve/memory/derived-ref.js +124 -0
  118. package/dist/commands/improve/memory/memory-belief.js +79 -7
  119. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  120. package/dist/commands/improve/memory/memory-improve.js +25 -37
  121. package/dist/commands/improve/outcome-loop.js +25 -88
  122. package/dist/commands/improve/preparation.js +1034 -813
  123. package/dist/commands/improve/proactive-maintenance.js +34 -9
  124. package/dist/commands/improve/proposal-envelope.js +31 -0
  125. package/dist/commands/improve/reflect.js +983 -794
  126. package/dist/commands/improve/run-context.js +119 -0
  127. package/dist/commands/improve/salience.js +24 -127
  128. package/dist/commands/improve/session-asset.js +7 -3
  129. package/dist/commands/improve/shared.js +14 -34
  130. package/dist/commands/improve/source-identity.js +28 -0
  131. package/dist/commands/improve/triage.js +20 -17
  132. package/dist/commands/lint/base-linter.js +340 -313
  133. package/dist/commands/lint/env-key-rules.js +31 -47
  134. package/dist/commands/lint/index.js +185 -30
  135. package/dist/commands/{events.js → log.js} +28 -38
  136. package/dist/commands/migrate-cli.js +54 -0
  137. package/dist/commands/migration-tool.js +55 -0
  138. package/dist/commands/observability-cli.js +70 -208
  139. package/dist/commands/proposal/diff-format.js +50 -0
  140. package/dist/commands/proposal/drain-policies.js +0 -6
  141. package/dist/commands/proposal/drain.js +91 -40
  142. package/dist/commands/proposal/proposal-cli.js +134 -132
  143. package/dist/commands/proposal/proposal-types.js +56 -0
  144. package/dist/commands/proposal/proposal.js +83 -65
  145. package/dist/commands/proposal/propose-cli.js +88 -0
  146. package/dist/commands/proposal/propose.js +105 -88
  147. package/dist/commands/proposal/repository.js +1303 -278
  148. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  149. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  150. package/dist/commands/proposal/validators/proposals.js +6 -8
  151. package/dist/commands/read/curate.js +78 -73
  152. package/dist/commands/read/knowledge.js +510 -13
  153. package/dist/commands/read/registry-search.js +2 -2
  154. package/dist/commands/read/remember-cli.js +84 -15
  155. package/dist/commands/read/search-cli.js +203 -96
  156. package/dist/commands/read/search.js +126 -94
  157. package/dist/commands/read/show.js +226 -250
  158. package/dist/commands/registry-cli.js +34 -60
  159. package/dist/commands/remember.js +18 -57
  160. package/dist/commands/sources/add-cli.js +104 -49
  161. package/dist/commands/sources/bundle-cli.js +166 -0
  162. package/dist/commands/sources/bundle-config-ops.js +63 -0
  163. package/dist/commands/sources/info.js +27 -15
  164. package/dist/commands/sources/init.js +30 -40
  165. package/dist/commands/sources/installed-stashes.js +469 -172
  166. package/dist/commands/sources/schema-repair.js +10 -9
  167. package/dist/commands/sources/self-update.js +182 -121
  168. package/dist/commands/sources/source-add.js +169 -178
  169. package/dist/commands/sources/source-clone.js +144 -41
  170. package/dist/commands/sources/source-manage.js +94 -59
  171. package/dist/commands/sources/sources-cli.js +64 -205
  172. package/dist/commands/sources/stash-cli.js +91 -54
  173. package/dist/commands/sources/stash-skeleton.js +1 -1
  174. package/dist/commands/tasks/tasks-cli.js +106 -104
  175. package/dist/commands/tasks/tasks.js +445 -262
  176. package/dist/commands/workflow-cli.js +75 -228
  177. package/dist/core/action-contributors.js +1 -1
  178. package/dist/core/activation-policy.js +49 -0
  179. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  180. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  181. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  182. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  183. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  184. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  185. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  186. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  187. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  188. package/dist/core/adapter/adapters/index.js +80 -0
  189. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  190. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  191. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  192. package/dist/core/adapter/adapters/shared.js +286 -0
  193. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  194. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  195. package/dist/core/adapter/bundle-adapter.js +4 -0
  196. package/dist/core/adapter/detect-adapter.js +17 -0
  197. package/dist/core/adapter/recognize-match.js +44 -0
  198. package/dist/core/adapter/registry.js +56 -0
  199. package/dist/core/adapter/types.js +4 -0
  200. package/dist/core/asset/akm-markdown.js +30 -0
  201. package/dist/core/asset/asset-placement.js +243 -0
  202. package/dist/core/asset/asset-ref.js +110 -79
  203. package/dist/core/asset/asset-serialize.js +20 -0
  204. package/dist/core/asset/frontmatter.js +28 -12
  205. package/dist/core/asset/markdown.js +40 -51
  206. package/dist/core/asset/resolve-ref.js +274 -0
  207. package/dist/core/asset/stash-meta.js +2 -2
  208. package/dist/core/bundle-id.js +51 -0
  209. package/dist/core/common.js +281 -86
  210. package/dist/core/config/config-io.js +42 -128
  211. package/dist/core/config/config-schema.js +233 -855
  212. package/dist/core/config/config-sources.js +162 -39
  213. package/dist/core/config/config-types.js +16 -11
  214. package/dist/core/config/config-version.js +29 -0
  215. package/dist/core/config/config-walker.js +126 -37
  216. package/dist/core/config/config.js +154 -331
  217. package/dist/core/config/deep-merge.js +41 -0
  218. package/dist/core/config/engine-semantics.js +28 -0
  219. package/dist/core/config/experimental.js +21 -0
  220. package/dist/core/config/schema/embedding.js +38 -0
  221. package/dist/core/config/schema/engines.js +116 -0
  222. package/dist/core/config/schema/experimental.js +47 -0
  223. package/dist/core/config/schema/feedback.js +31 -0
  224. package/dist/core/config/schema/improve-processes.js +389 -0
  225. package/dist/core/config/schema/improve.js +94 -0
  226. package/dist/core/config/schema/index-config.js +176 -0
  227. package/dist/core/config/schema/output.js +18 -0
  228. package/dist/core/config/schema/primitives.js +94 -0
  229. package/dist/core/config/schema/search.js +30 -0
  230. package/dist/core/config/schema/setup.js +18 -0
  231. package/dist/core/config/schema/sources-bundles.js +169 -0
  232. package/dist/core/config/schema/workflow.js +29 -0
  233. package/dist/core/env-secret-ref.js +155 -20
  234. package/dist/core/errors.js +17 -15
  235. package/dist/core/events-types.js +4 -0
  236. package/dist/core/events.js +46 -128
  237. package/dist/core/extra-params.js +62 -0
  238. package/dist/core/file-change.js +17 -0
  239. package/dist/core/file-lock.js +202 -57
  240. package/dist/core/fs-txn.js +392 -0
  241. package/dist/core/git-message.js +59 -0
  242. package/dist/core/improve-result.js +167 -0
  243. package/dist/core/lesson-lint.js +1 -17
  244. package/dist/core/logs-db.js +1 -1
  245. package/dist/core/maintenance-barrier.js +135 -0
  246. package/dist/core/migration-operation.js +44 -0
  247. package/dist/core/mutation-target.js +78 -0
  248. package/dist/core/paths.js +22 -25
  249. package/dist/core/platform.js +10 -0
  250. package/dist/core/recognition-util.js +128 -0
  251. package/dist/core/redaction.js +392 -0
  252. package/dist/core/standards/resolve-standards-context.js +36 -65
  253. package/dist/core/standards/resolve-stash-standards.js +2 -2
  254. package/dist/core/standards/resolve-type-conventions.js +5 -5
  255. package/dist/core/state/migrations.js +242 -11
  256. package/dist/core/state-db.js +98 -10
  257. package/dist/core/structured.js +1 -1
  258. package/dist/core/subprocess.js +303 -0
  259. package/dist/core/text-truncation.js +9 -5
  260. package/dist/core/time.js +20 -0
  261. package/dist/core/type-presentation.js +130 -0
  262. package/dist/core/warn.js +0 -3
  263. package/dist/core/write-source.js +834 -118
  264. package/dist/indexer/bundle-identity-guard.js +92 -0
  265. package/dist/indexer/db/graph-db.js +1 -25
  266. package/dist/indexer/db/llm-cache.js +1 -1
  267. package/dist/indexer/ensure-index.js +30 -9
  268. package/dist/indexer/graph/graph-boost.js +9 -30
  269. package/dist/indexer/graph/graph-extraction.js +41 -27
  270. package/dist/indexer/graph/graph-types.js +4 -0
  271. package/dist/indexer/index-writer-lock.js +93 -49
  272. package/dist/indexer/index-written-assets.js +100 -53
  273. package/dist/indexer/indexer.js +746 -329
  274. package/dist/indexer/init.js +18 -25
  275. package/dist/indexer/installations.js +142 -0
  276. package/dist/indexer/passes/dir-staleness.js +18 -10
  277. package/dist/indexer/passes/memory-inference.js +25 -15
  278. package/dist/indexer/passes/metadata.js +412 -243
  279. package/dist/indexer/scan/doc-to-entry.js +160 -0
  280. package/dist/indexer/scan/drain-dir.js +134 -0
  281. package/dist/indexer/search/db-search.js +292 -108
  282. package/dist/indexer/search/fts-query.js +64 -0
  283. package/dist/indexer/search/ranking-contributors.js +145 -25
  284. package/dist/indexer/search/ranking-types.js +4 -0
  285. package/dist/indexer/search/ranking.js +28 -71
  286. package/dist/indexer/search/search-attribution.js +67 -0
  287. package/dist/indexer/search/search-fields.js +18 -3
  288. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  289. package/dist/indexer/search/search-source.js +157 -111
  290. package/dist/indexer/search/semantic-status.js +4 -1
  291. package/dist/indexer/usage/usage-events.js +10 -30
  292. package/dist/indexer/walk/file-context.js +3 -45
  293. package/dist/indexer/walk/matchers.js +42 -73
  294. package/dist/indexer/walk/path-resolver.js +11 -5
  295. package/dist/indexer/walk/walker.js +42 -14
  296. package/dist/integrations/agent/builder-shared.js +7 -0
  297. package/dist/integrations/agent/builders.js +5 -58
  298. package/dist/integrations/agent/config.js +3 -143
  299. package/dist/integrations/agent/detect.js +17 -2
  300. package/dist/integrations/agent/engine-resolution.js +231 -0
  301. package/dist/integrations/agent/index.js +1 -2
  302. package/dist/integrations/agent/model-aliases.js +8 -3
  303. package/dist/integrations/agent/profiles.js +6 -99
  304. package/dist/integrations/agent/prompts.js +46 -18
  305. package/dist/integrations/agent/runner-dispatch.js +78 -13
  306. package/dist/integrations/agent/runner.js +76 -208
  307. package/dist/integrations/agent/spawn.js +48 -279
  308. package/dist/integrations/harnesses/aider/agent-builder.js +9 -8
  309. package/dist/integrations/harnesses/aider/index.js +2 -12
  310. package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -16
  311. package/dist/integrations/harnesses/amazonq/index.js +3 -17
  312. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  313. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  314. package/dist/integrations/harnesses/claude/index.js +1 -14
  315. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  316. package/dist/integrations/harnesses/codex/agent-builder.js +8 -7
  317. package/dist/integrations/harnesses/codex/index.js +2 -13
  318. package/dist/integrations/harnesses/copilot/agent-builder.js +9 -9
  319. package/dist/integrations/harnesses/copilot/index.js +1 -13
  320. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -9
  321. package/dist/integrations/harnesses/gemini/index.js +1 -13
  322. package/dist/integrations/harnesses/ids.js +24 -0
  323. package/dist/integrations/harnesses/index.js +31 -33
  324. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  325. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  326. package/dist/integrations/harnesses/opencode/index.js +1 -18
  327. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  328. package/dist/integrations/harnesses/opencode-sdk/harness.js +3 -17
  329. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  330. package/dist/integrations/harnesses/openhands/agent-builder.js +12 -10
  331. package/dist/integrations/harnesses/openhands/index.js +2 -12
  332. package/dist/integrations/harnesses/pi/agent-builder.js +11 -18
  333. package/dist/integrations/harnesses/pi/index.js +3 -16
  334. package/dist/integrations/harnesses/shared.js +17 -0
  335. package/dist/integrations/harnesses/types.js +38 -34
  336. package/dist/integrations/lockfile.js +211 -24
  337. package/dist/integrations/session-logs/index.js +24 -40
  338. package/dist/integrations/session-logs/provider-base.js +113 -0
  339. package/dist/llm/client.js +182 -110
  340. package/dist/llm/embedders/deterministic.js +2 -2
  341. package/dist/llm/embedders/remote.js +21 -9
  342. package/dist/llm/feature-gate.js +17 -57
  343. package/dist/llm/graph-extract.js +12 -13
  344. package/dist/llm/index-passes.js +8 -42
  345. package/dist/llm/memory-infer.js +144 -1
  346. package/dist/llm/metadata-enhance.js +45 -30
  347. package/dist/llm/structured-call.js +16 -8
  348. package/dist/llm/usage-persist.js +30 -5
  349. package/dist/llm/usage-telemetry.js +59 -6
  350. package/dist/output/cli-hints.js +1 -2
  351. package/dist/output/command-registry.js +27 -0
  352. package/dist/output/context.js +22 -7
  353. package/dist/output/format-exempt.js +80 -0
  354. package/dist/output/generic-render.js +251 -0
  355. package/dist/output/html-render.js +11 -16
  356. package/dist/output/render-registry.js +57 -0
  357. package/dist/output/renderers.js +15 -281
  358. package/dist/output/shapes/curate.js +10 -1
  359. package/dist/output/shapes/events.js +12 -7
  360. package/dist/output/shapes/helpers.js +58 -84
  361. package/dist/output/shapes/passthrough.js +8 -40
  362. package/dist/output/shapes/proposal/producer.js +15 -7
  363. package/dist/output/shapes/registry.js +12 -6
  364. package/dist/output/shapes.js +0 -9
  365. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  366. package/dist/output/text/bundle-show.js +7 -0
  367. package/dist/output/text/command-format.js +562 -0
  368. package/dist/output/text/env.js +1 -3
  369. package/dist/output/text/events.js +8 -7
  370. package/dist/output/text/helpers.js +15 -1375
  371. package/dist/output/text/proposal/producer.js +4 -2
  372. package/dist/output/text/proposal-format.js +202 -0
  373. package/dist/output/text/registry-commands.js +1 -2
  374. package/dist/output/text/registry.js +12 -6
  375. package/dist/output/text/show-directives.js +117 -0
  376. package/dist/output/text/show-format.js +103 -0
  377. package/dist/output/text/sync.js +5 -0
  378. package/dist/output/text/workflow-format.js +332 -0
  379. package/dist/output/text/workflow.js +1 -2
  380. package/dist/output/text.js +10 -19
  381. package/dist/registry/factory.js +4 -6
  382. package/dist/registry/origin-resolve.js +16 -27
  383. package/dist/registry/providers/skills-sh.js +3 -3
  384. package/dist/registry/providers/static-index.js +15 -25
  385. package/dist/registry/resolve.js +43 -94
  386. package/dist/registry/semver.js +43 -0
  387. package/dist/runtime.js +81 -12
  388. package/dist/scripts/akm-migrate.js +35529 -0
  389. package/dist/setup/detect.js +5 -7
  390. package/dist/setup/detected-engines.js +136 -0
  391. package/dist/setup/engine-config.js +100 -0
  392. package/dist/setup/registry-stash-loader.js +3 -3
  393. package/dist/setup/semantic-assets.js +12 -9
  394. package/dist/setup/setup.js +444 -208
  395. package/dist/setup/steps/connection-shared.js +120 -0
  396. package/dist/setup/steps/connection.js +108 -305
  397. package/dist/setup/steps/platforms.js +13 -12
  398. package/dist/setup/steps/semantic.js +15 -3
  399. package/dist/setup/steps/sources.js +21 -15
  400. package/dist/setup/steps/stashdir.js +6 -4
  401. package/dist/setup/steps/tasks.js +236 -119
  402. package/dist/setup/steps.js +3 -2
  403. package/dist/sources/freshness.js +39 -0
  404. package/dist/sources/provider-factory.js +11 -17
  405. package/dist/sources/providers/filesystem.js +2 -3
  406. package/dist/sources/providers/git-install.js +278 -34
  407. package/dist/sources/providers/git-provider.js +54 -56
  408. package/dist/sources/providers/git-stash.js +420 -91
  409. package/dist/sources/providers/git.js +2 -2
  410. package/dist/sources/providers/npm.js +16 -19
  411. package/dist/sources/providers/provider-utils.js +47 -22
  412. package/dist/sources/providers/sync-from-ref.js +3 -9
  413. package/dist/sources/providers/website.js +2 -2
  414. package/dist/sources/resolve.js +11 -10
  415. package/dist/sources/snapshot-fetchers/types.js +4 -0
  416. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  417. package/dist/storage/database.js +60 -4
  418. package/dist/storage/engines/sqlite-migrations.js +156 -5
  419. package/dist/storage/locations.js +1 -2
  420. package/dist/storage/repositories/canaries-repository.js +1 -1
  421. package/dist/storage/repositories/events-repository.js +51 -11
  422. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  423. package/dist/storage/repositories/index-connection.js +79 -0
  424. package/dist/storage/repositories/index-db.js +4 -3
  425. package/dist/storage/repositories/index-entries-repository.js +863 -0
  426. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  427. package/dist/storage/repositories/index-entry-types.js +4 -0
  428. package/dist/storage/repositories/index-fts-repository.js +167 -0
  429. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  430. package/dist/storage/repositories/index-meta-repository.js +49 -0
  431. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  432. package/dist/storage/repositories/index-sql.js +12 -0
  433. package/dist/storage/repositories/index-utility-repository.js +356 -0
  434. package/dist/storage/repositories/index-vec-repository.js +250 -0
  435. package/dist/storage/repositories/outcome-repository.js +119 -0
  436. package/dist/storage/repositories/proposals-repository.js +317 -75
  437. package/dist/storage/repositories/registry-cache.js +1 -1
  438. package/dist/storage/repositories/salience-repository.js +172 -0
  439. package/dist/storage/repositories/task-history-repository.js +110 -3
  440. package/dist/storage/repositories/workflow-runs-repository.js +68 -35
  441. package/dist/tasks/backends/cron.js +169 -46
  442. package/dist/tasks/backends/exec-utils.js +76 -3
  443. package/dist/tasks/backends/index.js +6 -9
  444. package/dist/tasks/backends/launchd.js +292 -55
  445. package/dist/tasks/backends/schtasks.js +557 -70
  446. package/dist/tasks/backends/types.js +4 -0
  447. package/dist/tasks/command-executable.js +93 -0
  448. package/dist/tasks/embedded.js +56 -38
  449. package/dist/tasks/parser.js +156 -64
  450. package/dist/tasks/resolve-akm-bin.js +144 -51
  451. package/dist/tasks/runner.js +377 -209
  452. package/dist/tasks/schedule.js +108 -19
  453. package/dist/tasks/scheduler-invocation.js +296 -0
  454. package/dist/tasks/schema.js +1 -1
  455. package/dist/tasks/task-id.js +35 -0
  456. package/dist/tasks/validator.js +30 -16
  457. package/dist/workflows/authoring/authoring.js +96 -148
  458. package/dist/workflows/authoring/scope-key.js +1 -1
  459. package/dist/workflows/cli.js +0 -20
  460. package/dist/workflows/concurrency-policy.js +15 -0
  461. package/dist/workflows/exec/brief.js +25 -59
  462. package/dist/workflows/exec/frozen-judge.js +47 -0
  463. package/dist/workflows/exec/native-executor.js +157 -94
  464. package/dist/workflows/exec/report.js +365 -200
  465. package/dist/workflows/exec/run-workflow.js +46 -40
  466. package/dist/workflows/exec/scheduler.js +12 -41
  467. package/dist/workflows/exec/step-work.js +239 -205
  468. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  469. package/dist/workflows/ir/compile.js +141 -283
  470. package/dist/workflows/ir/freeze.js +233 -0
  471. package/dist/workflows/ir/plan-hash.js +40 -5
  472. package/dist/workflows/ir/schema.js +537 -1
  473. package/dist/workflows/parser.js +878 -306
  474. package/dist/workflows/program/expressions.js +20 -208
  475. package/dist/workflows/program/schema.js +7 -10
  476. package/dist/workflows/renderer.js +99 -121
  477. package/dist/workflows/resource-limits.js +22 -0
  478. package/dist/workflows/runtime/checkin.js +1 -1
  479. package/dist/workflows/runtime/plan-classifier.js +131 -0
  480. package/dist/workflows/runtime/runs.js +200 -113
  481. package/dist/workflows/runtime/unit-checkin.js +1 -1
  482. package/dist/workflows/runtime/unit-phases.js +20 -0
  483. package/dist/workflows/runtime/workflow-asset-loader.js +235 -97
  484. package/dist/workflows/schema.js +1 -11
  485. package/dist/workflows/validate-summary.js +4 -26
  486. package/dist/workflows/validator.js +52 -30
  487. package/docs/README.md +42 -78
  488. package/docs/migration/README.md +8 -0
  489. package/docs/migration/release-notes/0.6.0.md +1 -1
  490. package/docs/migration/release-notes/0.7.0.md +9 -8
  491. package/docs/migration/release-notes/0.9.0.md +158 -14
  492. package/docs/migration/v0.7-to-v0.8.md +46 -47
  493. package/docs/migration/v0.8-to-v0.9.md +844 -0
  494. package/docs/reference/README.md +12 -0
  495. package/docs/reference/data-and-telemetry.md +333 -0
  496. package/package.json +21 -17
  497. package/schemas/akm-asset-envelope.json +93 -0
  498. package/schemas/akm-config.json +4636 -0
  499. package/schemas/akm-task.json +87 -0
  500. package/{dist/schemas → schemas}/akm-workflow.json +127 -82
  501. package/dist/akm-migrate-storage +0 -38
  502. package/dist/assets/help/help-accept.md +0 -12
  503. package/dist/assets/help/help-improve.md +0 -84
  504. package/dist/assets/help/help-proposals.md +0 -17
  505. package/dist/assets/help/help-propose.md +0 -17
  506. package/dist/assets/help/help-reject.md +0 -11
  507. package/dist/assets/profiles/frequent.json +0 -13
  508. package/dist/assets/profiles/recombine-only.json +0 -21
  509. package/dist/assets/profiles/reflect-distill.json +0 -30
  510. package/dist/assets/profiles/synthesize.json +0 -15
  511. package/dist/assets/prompts/procedural-system.md +0 -44
  512. package/dist/assets/prompts/recombine-system.md +0 -40
  513. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  514. package/dist/assets/tasks/core/backup.yml +0 -4
  515. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  516. package/dist/assets/templates/html/default.html +0 -78
  517. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  518. package/dist/assets/wiki/index-template.md +0 -12
  519. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  520. package/dist/assets/wiki/log-template.md +0 -8
  521. package/dist/assets/wiki/schema-template.md +0 -61
  522. package/dist/cli/config-migrate.js +0 -150
  523. package/dist/cli/config-validate.js +0 -39
  524. package/dist/commands/graph/graph-cli.js +0 -124
  525. package/dist/commands/graph/graph.js +0 -487
  526. package/dist/commands/improve/calibration.js +0 -161
  527. package/dist/commands/improve/dedup.js +0 -482
  528. package/dist/commands/improve/extract-watch.js +0 -140
  529. package/dist/commands/improve/hot-probation.js +0 -45
  530. package/dist/commands/improve/improve-auto-accept.js +0 -276
  531. package/dist/commands/improve/improve-profiles.js +0 -168
  532. package/dist/commands/improve/procedural.js +0 -398
  533. package/dist/commands/improve/recombine.js +0 -818
  534. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  535. package/dist/commands/lint/agent-linter.js +0 -44
  536. package/dist/commands/lint/command-linter.js +0 -44
  537. package/dist/commands/lint/default-linter.js +0 -16
  538. package/dist/commands/lint/fact-linter.js +0 -39
  539. package/dist/commands/lint/knowledge-linter.js +0 -16
  540. package/dist/commands/lint/memory-linter.js +0 -61
  541. package/dist/commands/lint/registry.js +0 -41
  542. package/dist/commands/lint/skill-linter.js +0 -45
  543. package/dist/commands/lint/task-linter.js +0 -50
  544. package/dist/commands/lint/workflow-linter.js +0 -81
  545. package/dist/commands/proposal/legacy-import.js +0 -115
  546. package/dist/commands/sources/history.js +0 -196
  547. package/dist/commands/tasks/default-tasks.js +0 -186
  548. package/dist/commands/wiki-cli.js +0 -292
  549. package/dist/core/asset/asset-registry.js +0 -76
  550. package/dist/core/asset/asset-spec.js +0 -316
  551. package/dist/core/config/config-migration.js +0 -602
  552. package/dist/core/deep-merge.js +0 -38
  553. package/dist/core/eval/rank-metrics.js +0 -113
  554. package/dist/core/ripgrep/install.js +0 -163
  555. package/dist/core/ripgrep/resolve.js +0 -81
  556. package/dist/indexer/db/db.js +0 -1414
  557. package/dist/indexer/manifest.js +0 -170
  558. package/dist/indexer/passes/metadata-contributors.js +0 -31
  559. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  560. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
  561. package/dist/llm/call-ai.js +0 -62
  562. package/dist/llm/memory-infer-impl.js +0 -138
  563. package/dist/output/shapes/distill.js +0 -14
  564. package/dist/output/shapes/history.js +0 -11
  565. package/dist/output/text/distill.js +0 -6
  566. package/dist/output/text/enable-disable.js +0 -8
  567. package/dist/output/text/history.js +0 -6
  568. package/dist/output/text/wiki.js +0 -16
  569. package/dist/registry/build-index.js +0 -386
  570. package/dist/schemas/akm-config.json +0 -14225
  571. package/dist/scripts/migrate-storage.js +0 -13169
  572. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -10169
  573. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  574. package/dist/setup/legacy-config.js +0 -106
  575. package/dist/storage/repositories/consolidation-repository.js +0 -38
  576. package/dist/storage/repositories/recombine-repository.js +0 -213
  577. package/dist/wiki/wiki-templates.js +0 -15
  578. package/dist/wiki/wiki.js +0 -1012
  579. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  580. package/dist/workflows/db.js +0 -350
  581. package/dist/workflows/exec/watch.js +0 -116
  582. package/dist/workflows/program/parser.js +0 -760
  583. package/dist/workflows/program/project.js +0 -105
  584. package/docs/data-and-telemetry.md +0 -227
  585. package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
  586. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  587. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -1,1414 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { createRequire } from "node:module";
5
- import path from "node:path";
6
- import { parseAssetRef } from "../../core/asset/asset-ref.js";
7
- import { bestEffort } from "../../core/best-effort.js";
8
- import { getDbPath } from "../../core/paths.js";
9
- import { warn } from "../../core/warn.js";
10
- import { cosineSimilarity } from "../../llm/embedders/types.js";
11
- import { sha256Hex } from "../../runtime.js";
12
- import { openManagedDatabase } from "../../storage/managed-db.js";
13
- import { computeNextUtility, HIGH_UTILITY_THRESHOLD, UTILITY_REVIEW_THRESHOLD, } from "../feedback/utility-policy.js";
14
- import { buildPrefixQuery, sanitizeFtsQuery } from "../search/fts-query.js";
15
- import { buildSearchFields } from "../search/search-fields.js";
16
- import { ENTRY_COLUMNS, rowToIndexedEntry } from "./entry-mapper.js";
17
- import { ensureSchema } from "./schema.js";
18
- export { HIGH_UTILITY_THRESHOLD, sanitizeFtsQuery, UTILITY_REVIEW_THRESHOLD };
19
- // ── Database lifecycle ──────────────────────────────────────────────────────
20
- export function openIndexDatabase(dbPath, options) {
21
- return openManagedDatabase({
22
- path: dbPath ?? getDbPath(),
23
- init: (db) => {
24
- // Try to load sqlite-vec extension
25
- loadVecExtension(db);
26
- // Dim resolution: explicit option wins; otherwise consult the on-disk
27
- // config so unparameterised opens (registry providers, graph helpers,
28
- // ad-hoc CLI subcommands) honour the operator-declared dimension. Only if
29
- // both are absent do we fall through to the no-clobber path, which keeps
30
- // ensureSchema from touching `index_meta.embeddingDim` at all.
31
- const resolvedDim = options?.embeddingDim ?? resolveConfiguredEmbeddingDim();
32
- ensureSchema(db, resolvedDim);
33
- // Warn once at init if using JS fallback with many entries
34
- warnIfVecMissing(db, { once: true });
35
- },
36
- });
37
- }
38
- /**
39
- * Read the operator-configured embedding dimension from the on-disk config.
40
- * Returns `undefined` when no config file is present, when the config has
41
- * no `embedding.dimension` set, or when reading the config throws (e.g.
42
- * inside isolated test fixtures with no XDG home). Failure is silent on
43
- * purpose — every openDatabase() call would otherwise have to handle a
44
- * config-not-found error path, and the fallback (no-clobber semantics) is
45
- * already correct.
46
- */
47
- function resolveConfiguredEmbeddingDim() {
48
- try {
49
- const esmRequire = createRequire(import.meta.url);
50
- const { loadConfig } = esmRequire("../../core/config/config");
51
- const dim = loadConfig().embedding?.dimension;
52
- if (typeof dim === "number" && Number.isInteger(dim) && dim > 0 && dim <= 4096) {
53
- return dim;
54
- }
55
- return undefined;
56
- }
57
- catch {
58
- return undefined;
59
- }
60
- }
61
- export function openExistingDatabase(dbPath) {
62
- // Existing-DB callers must not mutate schema or embedding metadata on open,
63
- // but some paths still need write access to usage_events and other tables —
64
- // so init only loads the vec extension, it does not run ensureSchema.
65
- return openManagedDatabase({ path: dbPath ?? getDbPath(), init: loadVecExtension });
66
- }
67
- export function closeDatabase(db) {
68
- db.close();
69
- }
70
- // ── sqlite-vec extension ────────────────────────────────────────────────────
71
- const vecStatus = new WeakMap();
72
- function loadVecExtension(db) {
73
- try {
74
- const esmRequire = createRequire(import.meta.url);
75
- const sqliteVec = esmRequire("sqlite-vec");
76
- // `db` here is the genuine underlying driver handle returned by the storage
77
- // boundary (bun:sqlite on Bun, better-sqlite3 on Node) — only structurally
78
- // narrowed for callers. sqlite-vec's `load()` accepts either real handle,
79
- // so no raw-handle escape hatch is required.
80
- sqliteVec.load(db);
81
- vecStatus.set(db, true);
82
- }
83
- catch {
84
- vecStatus.set(db, false);
85
- }
86
- }
87
- export function isVecAvailable(db) {
88
- return vecStatus.get(db) ?? false;
89
- }
90
- const VEC_DOCS_URL = "https://github.com/itlackey/akm/blob/main/docs/configuration.md#sqlite-vec-extension";
91
- const VEC_FALLBACK_THRESHOLD = 10_000;
92
- // Per-database warning state: tracks which databases have already emitted the
93
- // vec-missing warning so we don't spam on every openDatabase() call.
94
- const vecInitWarnedDbs = new WeakSet();
95
- /**
96
- * Warn if sqlite-vec is unavailable and embedding count exceeds threshold.
97
- * Called from openDatabase (once at init) and from indexer (each run).
98
- */
99
- export function warnIfVecMissing(db, { once } = { once: false }) {
100
- if (isVecAvailable(db))
101
- return;
102
- if (once && vecInitWarnedDbs.has(db))
103
- return;
104
- bestEffort(() => {
105
- const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
106
- const count = row?.cnt ?? 0;
107
- if (count >= VEC_FALLBACK_THRESHOLD) {
108
- warn("Semantic search is using JS fallback for %d entries. Install sqlite-vec for faster performance.\n See: %s", count, VEC_DOCS_URL);
109
- if (once)
110
- vecInitWarnedDbs.add(db);
111
- }
112
- }, "embeddings table may not exist yet during init");
113
- }
114
- /**
115
- * Purge stored embeddings (BLOB rows in `embeddings`, plus the `entries_vec`
116
- * virtual table) and mark the index as embedding-free. The single place that
117
- * invalidates embeddings — used on a dimension change, a model/provider change,
118
- * and a full rebuild.
119
- *
120
- * No backup: embeddings are a derived cache, fully regenerable from the markdown
121
- * by the next `akm index`. (Recovery model decided 2026-06-25.)
122
- *
123
- * `dropVecTable: true` DROPs `entries_vec` — used on a DIMENSION change, where
124
- * the vec0 table must be recreated at the new width by the caller. The default
125
- * clears its rows in place (same dimension, stale vectors).
126
- */
127
- export function purgeEmbeddings(db, opts) {
128
- bestEffort(() => db.exec("DELETE FROM embeddings"), "purge embeddings");
129
- if (isVecAvailable(db)) {
130
- bestEffort(() => db.exec(opts?.dropVecTable ? "DROP TABLE IF EXISTS entries_vec" : "DELETE FROM entries_vec"), "purge entries_vec");
131
- }
132
- setMeta(db, "hasEmbeddings", "0");
133
- }
134
- // ── Meta helpers ────────────────────────────────────────────────────────────
135
- export function getMeta(db, key) {
136
- const row = db.prepare("SELECT value FROM index_meta WHERE key = ?").get(key);
137
- return row?.value;
138
- }
139
- export function setMeta(db, key, value) {
140
- db.prepare("INSERT OR REPLACE INTO index_meta (key, value) VALUES (?, ?)").run(key, value);
141
- }
142
- export function getIndexDirState(db, dirPath) {
143
- const row = db
144
- .prepare("SELECT dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at FROM index_dir_state WHERE dir_path = ?")
145
- .get(dirPath);
146
- if (!row)
147
- return undefined;
148
- return {
149
- dirPath: row.dir_path,
150
- fileSetHash: row.file_set_hash,
151
- fileMtimeMaxMs: row.file_mtime_max_ms,
152
- reason: row.reason,
153
- updatedAt: row.updated_at,
154
- };
155
- }
156
- export function upsertIndexDirState(db, state) {
157
- db.prepare(`INSERT INTO index_dir_state (dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at)
158
- VALUES (?, ?, ?, ?, ?)
159
- ON CONFLICT(dir_path) DO UPDATE SET
160
- file_set_hash = excluded.file_set_hash,
161
- file_mtime_max_ms = excluded.file_mtime_max_ms,
162
- reason = excluded.reason,
163
- updated_at = excluded.updated_at`).run(state.dirPath, state.fileSetHash, state.fileMtimeMaxMs, state.reason, new Date().toISOString());
164
- }
165
- export function deleteIndexDirState(db, dirPath) {
166
- db.prepare("DELETE FROM index_dir_state WHERE dir_path = ?").run(dirPath);
167
- }
168
- export function deleteIndexDirStatesByStashDir(db, stashDir) {
169
- db.prepare("DELETE FROM index_dir_state WHERE dir_path = ? OR dir_path LIKE ?").run(stashDir, `${stashDir}${path.sep}%`);
170
- }
171
- // ── Entry operations ────────────────────────────────────────────────────────
172
- /**
173
- * SQLite parameter chunk size — chosen well below SQLITE_MAX_VARIABLE_NUMBER
174
- * (default 999 on most builds) so multi-row `IN (?, ?, ...)` queries stay
175
- * within bounds. Shared by helpers below.
176
- */
177
- const SQLITE_CHUNK_SIZE = 500;
178
- /**
179
- * Insert or update an entry in the `entries` table. Returns the row id.
180
- *
181
- * **Important:** This does not update the FTS index. Callers must call
182
- * `rebuildFts()` after all upserts are complete for full-text search to
183
- * reflect the changes.
184
- */
185
- export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, searchText) {
186
- // Hot path during indexing — cache the two prepared statements per
187
- // database connection so we don't pay the SQL parse/compile cost on
188
- // every call. The dirty-mark INSERT and the upsert-with-RETURNING
189
- // share the same WeakMap so they live and die with the connection.
190
- const stmts = getUpsertStmts(db);
191
- // Phase 5A / Advantage D5: surface derived memory parent ref into the
192
- // dedicated `derived_from` column so retrieval-time lookup (parent→child)
193
- // does not have to scan + JSON-decode every memory row.
194
- const derivedFrom = typeof entry.derivedFrom === "string" && entry.derivedFrom.trim() ? entry.derivedFrom.trim() : null;
195
- const result = stmts.upsert.get(entryKey, dirPath, filePath, stashDir, JSON.stringify(entry), searchText, entry.type, derivedFrom);
196
- if (!result)
197
- throw new Error("upsertEntry: entry_key not found after upsert");
198
- // Mark this entry as FTS-dirty so `rebuildFts({ incremental: true })`
199
- // only revisits entries that actually changed. INSERT OR IGNORE is
200
- // idempotent across multiple upserts of the same row.
201
- stmts.markDirty.run(result.id);
202
- return result.id;
203
- }
204
- const upsertStmtsByDb = new WeakMap();
205
- function getUpsertStmts(db) {
206
- const existing = upsertStmtsByDb.get(db);
207
- if (existing)
208
- return existing;
209
- const stmts = {
210
- // RETURNING id handles ON CONFLICT DO UPDATE correctly — no second
211
- // SELECT round-trip needed (last_insert_rowid() is unreliable for
212
- // ON CONFLICT). Use `.get()` so a single row comes back.
213
- upsert: db.prepare(`
214
- INSERT INTO entries (entry_key, dir_path, file_path, stash_dir, entry_json, search_text, entry_type, derived_from)
215
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
216
- ON CONFLICT(entry_key) DO UPDATE SET
217
- dir_path = excluded.dir_path,
218
- file_path = excluded.file_path,
219
- stash_dir = excluded.stash_dir,
220
- entry_json = excluded.entry_json,
221
- search_text = excluded.search_text,
222
- entry_type = excluded.entry_type,
223
- derived_from = excluded.derived_from
224
- RETURNING id
225
- `),
226
- markDirty: db.prepare("INSERT OR IGNORE INTO entries_fts_dirty (entry_id) VALUES (?)"),
227
- };
228
- upsertStmtsByDb.set(db, stmts);
229
- return stmts;
230
- }
231
- /**
232
- * Phase 5A / Advantage D5: look up the derived-memory child row whose
233
- * `derived_from` column matches `parentRef` (e.g. `"memory:claude-prefs"`).
234
- *
235
- * Returns the most-recently-updated derived child when multiple exist (one
236
- * parent should yield exactly one `.derived` child in practice, but the
237
- * ordering keeps results deterministic). Returns `null` when no derived
238
- * child has been indexed for this parent.
239
- */
240
- export function getDerivedForParent(db, parentRef) {
241
- if (!parentRef)
242
- return null;
243
- try {
244
- const row = db
245
- .prepare(`SELECT ${ENTRY_COLUMNS}
246
- FROM entries
247
- WHERE derived_from = ?
248
- ORDER BY id DESC
249
- LIMIT 1`)
250
- .get(parentRef);
251
- if (!row)
252
- return null;
253
- return rowToIndexedEntry(row, "getDerivedForParent");
254
- }
255
- catch {
256
- /* `derived_from` column may not exist on legacy DBs that haven't been
257
- rebuilt; treat as "no derived child". */
258
- return null;
259
- }
260
- }
261
- /**
262
- * 03-R3: for the given derived-twin row ids, fetch each twin's BASE memory
263
- * `beliefState`, keyed by twin id.
264
- *
265
- * Used by the derived-twin belief inheritance in search ranking: a `.derived`
266
- * twin has no belief state of its own, so it inherits its base memory's
267
- * demoting state (contradicted/superseded/…) at search time. A twin's
268
- * `entry_key` is exactly its base's `entry_key` plus the `.derived` suffix
269
- * (same stash + type prefix, `<name>` vs `<name>.derived`), so the base is
270
- * found by stripping that suffix — no ref/prefix reconstruction. Returns a map
271
- * of twin id → base beliefState for bases that carry a non-empty state.
272
- * Best-effort: any query error (e.g. legacy DB) yields no inheritance rather
273
- * than failing the search.
274
- */
275
- export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
276
- const out = new Map();
277
- if (twinIds.length === 0)
278
- return out;
279
- // Chunk at SQLITE_CHUNK_SIZE like the sibling bulk-by-id helpers, so a large
280
- // `--limit` candidate set never trips SQLITE_MAX_VARIABLE_NUMBER (which would
281
- // otherwise fall into the best-effort catch and silently disable the feature).
282
- for (let i = 0; i < twinIds.length; i += SQLITE_CHUNK_SIZE) {
283
- const chunk = twinIds.slice(i, i + SQLITE_CHUNK_SIZE);
284
- const placeholders = chunk.map(() => "?").join(",");
285
- bestEffort(() => {
286
- const rows = db
287
- .prepare(`SELECT twin.id AS twin_id, json_extract(base.entry_json, '$.beliefState') AS belief
288
- FROM entries twin
289
- JOIN entries base
290
- ON base.entry_type = 'memory'
291
- AND base.entry_key = substr(twin.entry_key, 1, length(twin.entry_key) - length('.derived'))
292
- WHERE twin.id IN (${placeholders})
293
- AND twin.entry_key LIKE '%.derived'
294
- AND json_extract(base.entry_json, '$.beliefState') IS NOT NULL`)
295
- .all(...chunk);
296
- for (const r of rows) {
297
- if (typeof r.belief === "string" && r.belief.trim().length > 0)
298
- out.set(r.twin_id, r.belief.trim());
299
- }
300
- }, "legacy DB / entry_json without beliefState — treat as no twin inheritance");
301
- }
302
- return out;
303
- }
304
- /**
305
- * Phase 2A / Rec 5: bulk-load positive feedback event counts for the given
306
- * entry ids. Used by the utility-decay forgetting curve to stabilize
307
- * (extend the half-life of) memories that have repeatedly proven useful.
308
- *
309
- * Returns a `Map<entryId, count>` containing only entries with at least one
310
- * positive feedback event — missing ids implicitly map to `0`. Chunks at
311
- * `SQLITE_CHUNK_SIZE` (500) to respect `SQLITE_MAX_VARIABLE_NUMBER`.
312
- *
313
- * Cheap when called with zero ids, and silently empty when the
314
- * `usage_events` table is missing.
315
- */
316
- export function getPositiveFeedbackCountsByIds(db, ids) {
317
- const result = new Map();
318
- if (ids.length === 0)
319
- return result;
320
- for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
321
- const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
322
- const placeholders = chunk.map(() => "?").join(",");
323
- bestEffort(() => {
324
- const rows = db
325
- .prepare(`SELECT entry_id, COUNT(*) AS cnt
326
- FROM usage_events
327
- WHERE event_type = 'feedback'
328
- AND signal = 'positive'
329
- AND entry_id IN (${placeholders})
330
- GROUP BY entry_id`)
331
- .all(...chunk);
332
- for (const row of rows) {
333
- if (row.entry_id !== null && row.cnt > 0) {
334
- result.set(row.entry_id, row.cnt);
335
- }
336
- }
337
- }, "usage_events table may be missing on legacy DBs — treat as zero counts");
338
- }
339
- return result;
340
- }
341
- function deleteEntriesWhere(db, column, value) {
342
- db.transaction(() => {
343
- const ids = db.prepare(`SELECT id FROM entries WHERE ${column} = ?`).all(value);
344
- deleteRelatedRows(db, ids);
345
- db.prepare(`DELETE FROM entries WHERE ${column} = ?`).run(value);
346
- })();
347
- }
348
- export function deleteEntriesByDir(db, dirPath) {
349
- deleteEntriesWhere(db, "dir_path", dirPath);
350
- }
351
- export function deleteEntriesByStashDir(db, stashDir) {
352
- deleteEntriesWhere(db, "stash_dir", stashDir);
353
- }
354
- function deleteRelatedRows(db, ids) {
355
- if (ids.length === 0)
356
- return;
357
- const numericIds = ids.map((r) => r.id);
358
- const vecAvail = isVecAvailable(db);
359
- // Drop matching FTS rows + dirty markers immediately so an incremental
360
- // rebuild after a deletion doesn't try to re-index entries that no longer
361
- // exist (and so a full scan after deletion sees a consistent FTS).
362
- for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
363
- const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
364
- const placeholders = chunk.map(() => "?").join(",");
365
- bestEffort(() => db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk), "fts table may not exist on a brand-new db");
366
- bestEffort(() => db.prepare(`DELETE FROM entries_fts_dirty WHERE entry_id IN (${placeholders})`).run(...chunk), "fts dirty table is created lazily by upsertEntry");
367
- }
368
- // Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
369
- for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
370
- const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
371
- const placeholders = chunk.map(() => "?").join(",");
372
- bestEffort(() => db.prepare(`DELETE FROM embeddings WHERE id IN (${placeholders})`).run(...chunk), "delete embeddings for entries");
373
- if (vecAvail) {
374
- bestEffort(() => db.prepare(`DELETE FROM entries_vec WHERE id IN (${placeholders})`).run(...chunk), "delete entries_vec for entries");
375
- }
376
- // Clean up utility scores before deleting entries
377
- bestEffort(() => db.prepare(`DELETE FROM utility_scores WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores for entries");
378
- bestEffort(() => db.prepare(`DELETE FROM utility_scores_scoped WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores_scoped for entries");
379
- // Clean up usage events before deleting entries
380
- bestEffort(() => db.prepare(`DELETE FROM usage_events WHERE entry_id IN (${placeholders})`).run(...chunk), "delete usage_events for entries");
381
- }
382
- // #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
383
- // entries row must NOT wipe the extracted graph (that is the whole point —
384
- // the graph survives a reindex when body_hash is unchanged). We therefore do
385
- // NOT delete graph_files here. We DO, however, recompute graph_meta counts
386
- // for the stash roots touched by the deleted entries so the summary numbers
387
- // stay consistent with the live child rows (the counts are derived, and the
388
- // entries delete may have changed which files are considered/indexed).
389
- //
390
- // Resolve the affected stash roots from the entries rows BEFORE deletion.
391
- const affectedStashRoots = new Set();
392
- for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
393
- const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
394
- const placeholders = chunk.map(() => "?").join(",");
395
- bestEffort(() => {
396
- const rows = db
397
- .prepare(`SELECT DISTINCT stash_dir FROM entries WHERE id IN (${placeholders})`)
398
- .all(...chunk);
399
- for (const row of rows) {
400
- if (row.stash_dir)
401
- affectedStashRoots.add(row.stash_dir);
402
- }
403
- }, "resolve stash roots for graph_meta recompute");
404
- }
405
- for (const stashRoot of affectedStashRoots) {
406
- bestEffort(() => db
407
- .prepare(`UPDATE graph_meta
408
- SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
409
- entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
410
- relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
411
- WHERE stash_root = ?`)
412
- .run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
413
- }
414
- }
415
- /**
416
- * Delete entries by their primary key IDs, along with all related rows
417
- * (embeddings, entries_vec, entries_fts, utility_scores, usage_events).
418
- *
419
- * Used by the `--clean` post-pass to remove stale entries whose source files
420
- * no longer exist on disk.
421
- */
422
- export function deleteEntriesByIds(db, ids) {
423
- if (ids.length === 0)
424
- return;
425
- db.transaction(() => {
426
- const idObjs = ids.map((id) => ({ id }));
427
- deleteRelatedRows(db, idObjs);
428
- for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
429
- const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
430
- const placeholders = chunk.map(() => "?").join(",");
431
- db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk);
432
- }
433
- })();
434
- }
435
- /**
436
- * Rebuild the FTS5 search index.
437
- *
438
- * `incremental` (default `false`): when true, only rebuild rows that
439
- * `upsertEntry` marked dirty since the last `rebuildFts` call. The full path
440
- * (default) wipes `entries_fts` and re-inserts every row from `entries` —
441
- * appropriate for `akm index --full` and version-upgrade rebuilds.
442
- *
443
- * Both paths are wrapped in a single transaction so the FTS table is never
444
- * left in a half-rebuilt state.
445
- *
446
- * Skipped corrupt-JSON rows are aggregated into one warning instead of
447
- * spamming stderr per-entry.
448
- */
449
- export function rebuildFts(db, options) {
450
- const incremental = options?.incremental === true;
451
- db.transaction(() => {
452
- let rows;
453
- if (incremental) {
454
- // Read the dirty queue and join against entries to get the JSON.
455
- // Then drop the matching rows from entries_fts so the INSERT below
456
- // doesn't double-up. The dirty list is drained at the end.
457
- rows = db
458
- .prepare(`SELECT e.id AS id, e.entry_json AS entry_json
459
- FROM entries_fts_dirty d
460
- JOIN entries e ON e.id = d.entry_id`)
461
- .all();
462
- if (rows.length === 0)
463
- return;
464
- const ids = rows.map((r) => r.id);
465
- // Delete only the dirty FTS rows — chunk to stay under
466
- // SQLITE_MAX_VARIABLE_NUMBER on large dirty queues.
467
- for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
468
- const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
469
- const placeholders = chunk.map(() => "?").join(",");
470
- db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
471
- }
472
- }
473
- else {
474
- // Full path: wipe and re-read every row.
475
- db.exec("DELETE FROM entries_fts");
476
- rows = db.prepare("SELECT id, entry_json FROM entries").all();
477
- }
478
- const insertStmt = db.prepare("INSERT INTO entries_fts (entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?)");
479
- let skipped = 0;
480
- for (const row of rows) {
481
- let entry;
482
- let fields;
483
- try {
484
- entry = JSON.parse(row.entry_json);
485
- fields = buildSearchFields(entry);
486
- }
487
- catch {
488
- skipped++;
489
- continue;
490
- }
491
- insertStmt.run(row.id, fields.name, fields.description, fields.tags, fields.hints, fields.content);
492
- }
493
- if (skipped > 0) {
494
- warn(`[db] rebuildFts: skipped ${skipped} entr${skipped === 1 ? "y" : "ies"} with invalid entry_json`);
495
- }
496
- // Always drain the dirty queue — both paths converge here. The
497
- // incremental path drains it because we just consumed every dirty row;
498
- // the full path drains it because a full rebuild covers everything the
499
- // dirty list tracks. The table is guaranteed to exist (created by
500
- // ensureSchema()).
501
- //
502
- // BUG-L1: previously the if/else arms ran identical statements — the
503
- // duplication has been collapsed.
504
- db.exec("DELETE FROM entries_fts_dirty");
505
- })();
506
- }
507
- // ── Vector operations ───────────────────────────────────────────────────────
508
- export function upsertEmbedding(db, entryId, embedding) {
509
- // Pre-flight FK guard: when an entry is deleted between when its id is queued
510
- // for embedding and when this INSERT runs (e.g. consolidation deletes during
511
- // a concurrent improve cycle), the INSERT throws "FOREIGN KEY constraint failed"
512
- // and rolls back the entire batch transaction in the caller, losing every
513
- // embedding for that run. A cheap SELECT here turns the race into a clean skip.
514
- const exists = db.prepare("SELECT 1 FROM entries WHERE id = ?").get(entryId);
515
- if (!exists)
516
- return false;
517
- const buf = float32Buffer(embedding);
518
- // Always write to BLOB table (works without sqlite-vec)
519
- db.prepare("INSERT OR REPLACE INTO embeddings (id, embedding) VALUES (?, ?)").run(entryId, buf);
520
- // Also write to sqlite-vec table when available (fast path).
521
- // Wrapped in a transaction so a crash between DELETE and INSERT does not
522
- // leave the entry missing from the vec table.
523
- if (isVecAvailable(db)) {
524
- bestEffort(() => {
525
- db.transaction(() => {
526
- db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
527
- db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
528
- })();
529
- }, "vec table unavailable or constraint failure");
530
- }
531
- return true;
532
- }
533
- export function searchVec(db, queryEmbedding, k) {
534
- // Fast path: use sqlite-vec when available
535
- if (isVecAvailable(db)) {
536
- const buf = float32Buffer(queryEmbedding);
537
- try {
538
- return db
539
- .prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
540
- .all(buf, k);
541
- }
542
- catch (err) {
543
- // Log the failure so it's visible in diagnostics
544
- warn("[db] searchVec (sqlite-vec path) failed:", err instanceof Error ? err.message : String(err));
545
- return [];
546
- }
547
- }
548
- // Fallback: JS-based cosine similarity over BLOB table
549
- return searchBlobVec(db, queryEmbedding, k);
550
- }
551
- /**
552
- * Return the k nearest neighbours of an already-indexed entry using its
553
- * persisted embedding — no re-embedding, no network. Decodes the stored BLOB by
554
- * byte length (dim = bytes / 4) and reuses searchVec (sqlite-vec fast path or
555
- * JS-cosine fallback). Returns [] when the entry has no stored embedding or the
556
- * BLOB is corrupt. The query entry itself is typically returned with distance
557
- * ~0 — callers should filter it out by id.
558
- */
559
- export function getNeighborsByEntryId(db, id, k) {
560
- const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
561
- if (!row)
562
- return [];
563
- const queryEmbedding = bufferToFloat32(row.embedding, Math.floor(row.embedding.byteLength / 4));
564
- if (!queryEmbedding)
565
- return [];
566
- return searchVec(db, queryEmbedding, k);
567
- }
568
- function float32Buffer(vec) {
569
- const f32 = new Float32Array(vec);
570
- return Buffer.from(f32.buffer);
571
- }
572
- /**
573
- * Decode a stored embedding BLOB into a Float32 array of `expectedDim`
574
- * dimensions. Returns `null` (and emits a warning) when the byte length does
575
- * not exactly match `expectedDim * 4`, including the legacy partial-trailing
576
- * float case the previous truncating-divide silently swallowed.
577
- *
578
- * BUG-M2: the previous `buf.byteLength / 4` divide would truncate any
579
- * trailing partial float and a misaligned `byteOffset` would throw — both
580
- * surfaced as opaque generic errors caught upstream.
581
- */
582
- function bufferToFloat32(buf, expectedDim) {
583
- if (buf.byteLength !== expectedDim * 4) {
584
- warn("[db] bufferToFloat32: skipping embedding row — expected %d bytes (%d dim x 4), got %d", expectedDim * 4, expectedDim, buf.byteLength);
585
- return null;
586
- }
587
- // Copy into a fresh ArrayBuffer to sidestep any byteOffset alignment
588
- // requirements imposed by Float32Array's typed-array view contract.
589
- const aligned = new ArrayBuffer(buf.byteLength);
590
- new Uint8Array(aligned).set(buf);
591
- const f32 = new Float32Array(aligned);
592
- return Array.from(f32);
593
- }
594
- function searchBlobVec(db, queryEmbedding, k) {
595
- try {
596
- const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
597
- if (rows.length === 0)
598
- return [];
599
- const expectedDim = queryEmbedding.length;
600
- const scored = [];
601
- for (const row of rows) {
602
- const embedding = bufferToFloat32(row.embedding, expectedDim);
603
- if (embedding === null)
604
- continue;
605
- const similarity = cosineSimilarity(queryEmbedding, embedding);
606
- scored.push({ id: row.id, similarity });
607
- }
608
- scored.sort((a, b) => b.similarity - a.similarity);
609
- // Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
610
- // For normalized vectors: L2² = 2(1 - cos_sim)
611
- return scored.slice(0, k).map(({ id, similarity }) => ({
612
- id,
613
- distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
614
- }));
615
- }
616
- catch (err) {
617
- // MD-5: Log the failure so it's visible in diagnostics
618
- warn("[db] searchBlobVec (JS fallback) failed:", err instanceof Error ? err.message : String(err));
619
- return [];
620
- }
621
- }
622
- // ── FTS5 search ─────────────────────────────────────────────────────────────
623
- export function searchFts(db, query, limit, entryType, excludeTypes) {
624
- const ftsQuery = sanitizeFtsQuery(query);
625
- if (!ftsQuery)
626
- return [];
627
- // Try the exact AND query first
628
- const exactResults = runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes);
629
- if (exactResults.length > 0)
630
- return exactResults;
631
- // Exact match returned zero results — try prefix fallback.
632
- // Append FTS5 `*` suffix to each token that is >= 3 characters long.
633
- // Short tokens (1-2 chars) are excluded from prefix expansion because
634
- // they produce too many false positives.
635
- const prefixQuery = buildPrefixQuery(ftsQuery);
636
- if (!prefixQuery)
637
- return [];
638
- return runFtsQuery(db, prefixQuery, limit, entryType, excludeTypes);
639
- }
640
- function runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes) {
641
- // #627 — exclude-type clause. Only applies on the untyped ('any') path; an
642
- // explicit include filter (entryType) already narrows to a single type, so
643
- // exclusion is redundant there. An empty list skips the clause entirely
644
- // (never emit `NOT IN ()`, which is a SQL error / always-false).
645
- const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
646
- // The typed and untyped paths differ ONLY by one WHERE clause (an entry_type
647
- // equality vs. an optional NOT IN exclusion) and their param order — the
648
- // SELECT/JOIN/ORDER/LIMIT is shared, so build it once. Join on integer
649
- // entry_id directly (no CAST; we store integer). bm25() per-column weights:
650
- // entry_id(0), name(10), description(5), tags(3), hints(2), content(1).
651
- let filterClause;
652
- let params;
653
- if (entryType && entryType !== "any") {
654
- filterClause = "AND e.entry_type = ?";
655
- params = [ftsQuery, entryType, limit];
656
- }
657
- else {
658
- filterClause = excludes.length > 0 ? `AND e.entry_type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
659
- // Param order: MATCH, then the NOT IN values, then LIMIT.
660
- params = [ftsQuery, ...excludes, limit];
661
- }
662
- const sql = `
663
- SELECT e.id, e.file_path AS filePath, e.entry_json, e.search_text AS searchText,
664
- bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
665
- FROM entries_fts f
666
- JOIN entries e ON e.id = f.entry_id
667
- WHERE entries_fts MATCH ?
668
- ${filterClause}
669
- ORDER BY bm25Score, e.id ASC
670
- LIMIT ?
671
- `;
672
- try {
673
- const rows = db.prepare(sql).all(...params);
674
- // Guard against corrupt JSON — skip the row rather than crashing
675
- const results = [];
676
- for (const row of rows) {
677
- let entry;
678
- try {
679
- entry = JSON.parse(row.entry_json);
680
- }
681
- catch {
682
- warn(`[db] searchFts: skipping entry id=${row.id} — corrupt entry_json`);
683
- continue;
684
- }
685
- results.push({
686
- id: row.id,
687
- filePath: row.filePath,
688
- entry,
689
- searchText: row.searchText,
690
- bm25Score: row.bm25Score,
691
- });
692
- }
693
- return results;
694
- }
695
- catch (err) {
696
- warn("[db] runFtsQuery failed:", err instanceof Error ? err.message : String(err));
697
- return [];
698
- }
699
- }
700
- // ── All entries ─────────────────────────────────────────────────────────────
701
- function parseEntryRows(rows, context) {
702
- const entries = [];
703
- for (const row of rows) {
704
- const mapped = rowToIndexedEntry(row, context);
705
- if (mapped)
706
- entries.push(mapped);
707
- }
708
- return entries;
709
- }
710
- export function getAllEntries(db, entryType, excludeTypes) {
711
- let sql;
712
- let params;
713
- // #627 — exclude-type clause applies only on the untyped ('any') path. Empty
714
- // list skips the clause (never `NOT IN ()`).
715
- const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
716
- if (entryType && entryType !== "any") {
717
- sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type = ?`;
718
- params = [entryType];
719
- }
720
- else if (excludes.length > 0) {
721
- sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type NOT IN (${excludes.map(() => "?").join(", ")})`;
722
- params = [...excludes];
723
- }
724
- else {
725
- sql = `SELECT ${ENTRY_COLUMNS} FROM entries`;
726
- params = [];
727
- }
728
- const rows = db.prepare(sql).all(...params);
729
- return parseEntryRows(rows, "getAllEntries");
730
- }
731
- /**
732
- * #609 — read graph entities (normalized) for a set of entry ids. Used by the
733
- * recombine pass to cluster memories by shared graph entity ("graph"
734
- * relatedness source). Returns a map of `entry_id -> entity_norm[]`. Entries
735
- * with no graph entities (graph extraction has not run, or the file produced
736
- * no entities) are simply absent from the map — callers must fail open
737
- * (fall back to tag relatedness) when the map is empty.
738
- */
739
- export function getEntitiesByEntryIds(db, entryIds) {
740
- const result = new Map();
741
- if (entryIds.length === 0)
742
- return result;
743
- // #624-P1: graph_file_entities no longer carries entry_id. Re-derive the
744
- // entry_id -> entity_norm[] contract by JOINing through entries on
745
- // (stash_dir, file_path) -> graph_files. Chunk the IN(?) list because the
746
- // recombine pass can pass 10k+ entry ids (well over the SQLite param limit).
747
- for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
748
- const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
749
- const placeholders = chunk.map(() => "?").join(", ");
750
- const rows = db
751
- .prepare(`SELECT e.id AS entry_id, gfe.entity_norm AS entity_norm
752
- FROM entries e
753
- JOIN graph_files gf
754
- ON gf.stash_root = e.stash_dir AND gf.file_path = e.file_path
755
- JOIN graph_file_entities gfe
756
- ON gfe.stash_root = gf.stash_root
757
- AND gfe.file_path = gf.file_path
758
- AND gfe.body_hash = gf.body_hash
759
- WHERE e.id IN (${placeholders})
760
- ORDER BY e.id, gfe.entity_order`)
761
- .all(...chunk);
762
- for (const row of rows) {
763
- const list = result.get(row.entry_id);
764
- if (list)
765
- list.push(row.entity_norm);
766
- else
767
- result.set(row.entry_id, [row.entity_norm]);
768
- }
769
- }
770
- return result;
771
- }
772
- export function findEntryIdByRef(db, ref) {
773
- const parsed = parseAssetRef(ref);
774
- const nameVariants = [parsed.name];
775
- if (parsed.name.endsWith(".md")) {
776
- nameVariants.push(parsed.name.slice(0, -3));
777
- }
778
- else {
779
- nameVariants.push(`${parsed.name}.md`);
780
- }
781
- const stmt = db.prepare("SELECT id FROM entries WHERE entry_type = ? AND substr(entry_key, length(entry_key) - length(?) + 1) = ? LIMIT 1");
782
- for (const name of nameVariants) {
783
- const suffix = `${parsed.type}:${name}`;
784
- const row = stmt.get(parsed.type, suffix, suffix);
785
- if (row)
786
- return row.id;
787
- }
788
- return undefined;
789
- }
790
- export function getEntryCount(db) {
791
- const row = db.prepare("SELECT COUNT(*) AS cnt FROM entries").get();
792
- return row.cnt;
793
- }
794
- export function getEmbeddableEntryCount(db) {
795
- return getEntryCount(db);
796
- }
797
- export function getEmbeddingCount(db) {
798
- const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
799
- return row.cnt;
800
- }
801
- export function getEntryById(db, id) {
802
- const row = db.prepare("SELECT file_path, entry_json FROM entries WHERE id = ?").get(id);
803
- if (!row)
804
- return undefined;
805
- // Guard against corrupt JSON
806
- let entry;
807
- try {
808
- entry = JSON.parse(row.entry_json);
809
- }
810
- catch {
811
- warn(`[db] getEntryById: skipping entry id=${id} — corrupt entry_json`);
812
- return undefined;
813
- }
814
- return { filePath: row.file_path, entry };
815
- }
816
- export function getEntriesByDir(db, dirPath) {
817
- const rows = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries WHERE dir_path = ?`).all(dirPath);
818
- return parseEntryRows(rows, "getEntriesByDir");
819
- }
820
- /**
821
- * Resolve a single `entries.id` by exact `file_path` (the canonical on-disk
822
- * path), or `undefined` if no row matches.
823
- *
824
- * Lifted verbatim (WS5) from the inline `SELECT id FROM entries WHERE
825
- * file_path = ? LIMIT 1` in commands/search.ts so all `entries` SQL lives in
826
- * this module. The result is a plain number materialised before return —
827
- * nothing lazy crosses a connection boundary.
828
- */
829
- export function getEntryIdByFilePath(db, filePath) {
830
- const row = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
831
- return row?.id;
832
- }
833
- /**
834
- * Set of every non-empty `entries.file_path` currently indexed (across all
835
- * stashes/sources). Used by staleness detection to spot files that exist on
836
- * disk but were never indexed — a clock-independent signal for newly-added
837
- * assets that an mtime-vs-builtAt comparison can miss when the two clocks
838
- * (filesystem vs wall-clock) are skewed within the same millisecond.
839
- */
840
- export function getIndexedFilePaths(db) {
841
- const rows = db
842
- .prepare("SELECT DISTINCT file_path FROM entries WHERE file_path IS NOT NULL AND file_path <> ''")
843
- .all();
844
- return new Set(rows.map((r) => r.file_path));
845
- }
846
- /**
847
- * Resolve a single `entries.file_path` by primary key, or `undefined` if no
848
- * row matches.
849
- *
850
- * Lifted verbatim (WS5) from the inline `SELECT file_path FROM entries WHERE
851
- * id = ?` in commands/feedback-cli.ts. Unlike {@link getEntryById}, this does
852
- * NOT parse `entry_json`, so a row with corrupt JSON still yields its path —
853
- * preserving feedback-cli's pre-extraction behaviour byte-for-byte.
854
- */
855
- export function getEntryFilePathById(db, id) {
856
- const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(id);
857
- return row?.file_path;
858
- }
859
- /**
860
- * Fetch every `(file_path, entry_json)` row whose entry belongs to a given
861
- * stash root — matched either by exact `stash_dir` OR by `file_path` prefix.
862
- *
863
- * Lifted verbatim (WS5) from the inline query in commands/graph.ts'
864
- * `buildRefByPath`. The full result set is materialised with `.all()` before
865
- * return so callers can iterate it after the connection closes (WS5
866
- * connection-lifetime rule). JSON parsing stays with the caller, unchanged.
867
- */
868
- export function getEntryRefRowsForStashRoot(db, stashRoot) {
869
- return db
870
- .prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
871
- .all(stashRoot, `${stashRoot}%`);
872
- }
873
- /**
874
- * Get the utility score for an entry, or undefined if none exists.
875
- */
876
- export function getUtilityScore(db, entryId) {
877
- const row = db
878
- .prepare("SELECT entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at FROM utility_scores WHERE entry_id = ?")
879
- .get(entryId);
880
- if (!row)
881
- return undefined;
882
- return {
883
- entryId: row.entry_id,
884
- utility: row.utility,
885
- showCount: row.show_count,
886
- searchCount: row.search_count,
887
- selectRate: row.select_rate,
888
- lastUsedAt: row.last_used_at ?? undefined,
889
- updatedAt: row.updated_at,
890
- };
891
- }
892
- /**
893
- * Batch-load utility scores for multiple entry IDs in a single query.
894
- * Returns a `{ global, scoped }` pair, both Maps keyed by entry_id.
895
- *
896
- * When `scopeKey` is provided a second query runs against
897
- * `utility_scores_scoped` and the result is returned as `scoped`.
898
- * Both maps are always present; `scoped` is empty when `scopeKey` is absent.
899
- */
900
- export function getUtilityScoresByIds(db, ids, scopeKey) {
901
- const global = new Map();
902
- const scoped = new Map();
903
- if (ids.length === 0)
904
- return { global, scoped };
905
- // Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
906
- for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
907
- const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
908
- const placeholders = chunk.map(() => "?").join(",");
909
- const rows = db
910
- .prepare(`SELECT entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at FROM utility_scores WHERE entry_id IN (${placeholders})`)
911
- .all(...chunk);
912
- for (const row of rows) {
913
- global.set(row.entry_id, {
914
- entryId: row.entry_id,
915
- utility: row.utility,
916
- showCount: row.show_count,
917
- searchCount: row.search_count,
918
- selectRate: row.select_rate,
919
- lastUsedAt: row.last_used_at ?? undefined,
920
- updatedAt: row.updated_at,
921
- });
922
- }
923
- if (scopeKey) {
924
- const scopedRows = db
925
- .prepare(`SELECT entry_id, scope_key, utility, last_used_at FROM utility_scores_scoped WHERE scope_key = ? AND entry_id IN (${placeholders})`)
926
- .all(scopeKey, ...chunk);
927
- for (const row of scopedRows) {
928
- scoped.set(row.entry_id, {
929
- entryId: row.entry_id,
930
- scopeKey: row.scope_key,
931
- utility: row.utility,
932
- lastUsedAt: row.last_used_at,
933
- });
934
- }
935
- }
936
- }
937
- return { global, scoped };
938
- }
939
- /**
940
- * Insert or update a utility score for an entry.
941
- */
942
- export function upsertUtilityScore(db, entryId, data) {
943
- // Pre-flight FK guard (mirrors `upsertEmbedding`): when an entry is
944
- // deleted between when its id is aggregated from usage_events and when
945
- // this INSERT runs, the FK constraint fails and rolls back the entire
946
- // finalize transaction. A cheap SELECT here turns the race into a
947
- // clean skip. Returns false when the entry no longer exists.
948
- const exists = db.prepare("SELECT 1 FROM entries WHERE id = ?").get(entryId);
949
- if (!exists)
950
- return false;
951
- db.prepare(`
952
- INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
953
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
954
- ON CONFLICT(entry_id) DO UPDATE SET
955
- utility = excluded.utility,
956
- show_count = excluded.show_count,
957
- search_count = excluded.search_count,
958
- select_rate = excluded.select_rate,
959
- last_used_at = excluded.last_used_at,
960
- updated_at = datetime('now')
961
- `).run(entryId, data.utility, data.showCount, data.searchCount, data.selectRate, data.lastUsedAt ?? null);
962
- return true;
963
- }
964
- /**
965
- * Look up a cached LLM result for the given asset_ref.
966
- *
967
- * Returns `undefined` when no entry exists OR when the stored body_hash
968
- * doesn't match `currentBodyHash` (body has changed since the result was
969
- * cached). In both cases the caller should invoke the LLM and write a new
970
- * cache entry.
971
- */
972
- export function getLlmCacheEntry(db, assetRef, currentBodyHash, cacheVariant = "") {
973
- const row = db
974
- .prepare("SELECT asset_ref, cache_variant, body_hash, result_json, updated_at FROM llm_enrichment_cache WHERE asset_ref = ? AND cache_variant = ?")
975
- .get(assetRef, cacheVariant);
976
- if (!row)
977
- return undefined;
978
- // Hash mismatch → body changed, treat as cache miss.
979
- if (row.body_hash !== currentBodyHash)
980
- return undefined;
981
- return {
982
- assetRef: row.asset_ref,
983
- cacheVariant: row.cache_variant,
984
- bodyHash: row.body_hash,
985
- resultJson: row.result_json,
986
- updatedAt: row.updated_at,
987
- };
988
- }
989
- /**
990
- * Batched variant of {@link getLlmCacheEntry}. Fetches every cache row whose
991
- * `asset_ref` is in `refs` with a single `IN (...)` query (chunked to respect
992
- * SQLITE_MAX_VARIABLE_NUMBER), returning a `Map<assetRef, LlmCacheEntry>`.
993
- *
994
- * Unlike `getLlmCacheEntry`, this does NOT filter by body hash — callers must
995
- * compare `entry.bodyHash` against the current body hash themselves. This lets
996
- * the batch path issue one DB query per chunk instead of one per file.
997
- */
998
- export function getLlmCacheEntriesByRefs(db, refs, cacheVariant = "") {
999
- const result = new Map();
1000
- if (refs.length === 0)
1001
- return result;
1002
- for (let i = 0; i < refs.length; i += SQLITE_CHUNK_SIZE) {
1003
- const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
1004
- const placeholders = chunk.map(() => "?").join(", ");
1005
- const rows = db
1006
- .prepare(`SELECT asset_ref, cache_variant, body_hash, result_json, updated_at FROM llm_enrichment_cache
1007
- WHERE cache_variant = ? AND asset_ref IN (${placeholders})`)
1008
- .all(cacheVariant, ...chunk);
1009
- for (const row of rows) {
1010
- result.set(row.asset_ref, {
1011
- assetRef: row.asset_ref,
1012
- cacheVariant: row.cache_variant,
1013
- bodyHash: row.body_hash,
1014
- resultJson: row.result_json,
1015
- updatedAt: row.updated_at,
1016
- });
1017
- }
1018
- }
1019
- return result;
1020
- }
1021
- /**
1022
- * Insert or update a cached LLM result for the given asset_ref.
1023
- */
1024
- export function upsertLlmCacheEntry(db, assetRef, bodyHash, resultJson, cacheVariant = "") {
1025
- db.prepare(`INSERT INTO llm_enrichment_cache (asset_ref, cache_variant, body_hash, result_json, updated_at)
1026
- VALUES (?, ?, ?, ?, ?)
1027
- ON CONFLICT(asset_ref, cache_variant) DO UPDATE SET
1028
- body_hash = excluded.body_hash,
1029
- result_json = excluded.result_json,
1030
- updated_at = excluded.updated_at`).run(assetRef, cacheVariant, bodyHash, resultJson, Date.now());
1031
- }
1032
- /**
1033
- * Delete LLM cache entries whose asset_ref is no longer present in the
1034
- * `entries` table. Should be called during the cleanup phase of each index
1035
- * run to prevent the cache from growing unboundedly as assets are removed.
1036
- *
1037
- * The join uses a LIKE match against the entries `file_path` column because
1038
- * graph/memory cache refs are absolute file paths, while enrichment cache
1039
- * refs are entry_key strings — we preserve any entry that still has a
1040
- * corresponding row in either the entries table (by entry_key) or that
1041
- * matches a live file_path.
1042
- */
1043
- export function clearStaleCacheEntries(db) {
1044
- bestEffort(() => {
1045
- db.exec(`
1046
- DELETE FROM llm_enrichment_cache
1047
- WHERE asset_ref NOT IN (SELECT file_path FROM entries)
1048
- AND asset_ref NOT IN (SELECT entry_key FROM entries)
1049
- `);
1050
- }, "llm_enrichment_cache may not exist in very old DBs opened without ensureSchema");
1051
- }
1052
- /**
1053
- * Compute a stable SHA-256 hex digest of a UTF-8 string. Used as the body_hash
1054
- * key in `llm_enrichment_cache`. Routed through the runtime boundary so the
1055
- * SQLite layer stays free of direct runtime-specific references.
1056
- */
1057
- export function computeBodyHash(body) {
1058
- return sha256Hex(body);
1059
- }
1060
- /**
1061
- * Reduce a ref to its bare `type:name` form, dropping any `origin//` prefix.
1062
- *
1063
- * usage_events store entry_ref inconsistently: search/show writers persist
1064
- * whatever ref the result carried, which is sometimes stash-prefixed
1065
- * (`origin//type:name`) and sometimes bare (`type:name`). Retrieval counting
1066
- * keys on the bare form so both spellings of the same asset collapse together.
1067
- *
1068
- * Returns the bare form, or the original string when it cannot be parsed (best
1069
- * effort — never throws so a malformed stored ref can't break counting).
1070
- */
1071
- function bareRef(ref) {
1072
- try {
1073
- const parsed = parseAssetRef(ref);
1074
- return `${parsed.type}:${parsed.name}`;
1075
- }
1076
- catch {
1077
- return ref;
1078
- }
1079
- }
1080
- /**
1081
- * Count retrieval events for the given entry refs.
1082
- *
1083
- * Counts `search`, `show`, and `curate` usage events. Returns a
1084
- * Map<inputRef, count> keyed by the *input* ref strings (only those with at
1085
- * least one matching event appear). Used by the improve loop to find
1086
- * high-retrieval assets without feedback.
1087
- *
1088
- * Matching is normalization-aware: each stored `entry_ref` is reduced to its
1089
- * bare `type:name` form before comparison, so a stash-prefixed stored ref
1090
- * (`origin//type:name`) still matches a bare input ref (`type:name`) and vice
1091
- * versa. Previously the raw `entry_ref IN (...)` comparison silently dropped
1092
- * roughly half the signal whenever the two spellings disagreed.
1093
- *
1094
- * `curate` events are included: their per-item rows are written with
1095
- * entry_ref populated (see logCurateEvent), so curation is a real retrieval
1096
- * signal here. Legacy summary-only curate rows with a NULL entry_ref simply
1097
- * contribute nothing.
1098
- *
1099
- * Machine-sourced events (`source` = 'improve' or 'task') are EXCLUDED: this
1100
- * count feeds salience/ranking, and pipeline probe traffic counting as demand
1101
- * creates a self-reinforcing loop (meta-review 05 DRIFT-6). NULL sources
1102
- * (pre-column rows) count as user demand.
1103
- */
1104
- export function getRetrievalCounts(db, refs) {
1105
- if (refs.length === 0)
1106
- return new Map();
1107
- // Map each distinct bare form back to the input ref(s) that produced it so we
1108
- // can re-key DB results (grouped by bare form) onto the caller's ref strings.
1109
- const bareToInputs = new Map();
1110
- for (const ref of refs) {
1111
- const bare = bareRef(ref);
1112
- const existing = bareToInputs.get(bare);
1113
- if (existing)
1114
- existing.push(ref);
1115
- else
1116
- bareToInputs.set(bare, [ref]);
1117
- }
1118
- const bareForms = [...bareToInputs.keys()];
1119
- // Accumulate counts per bare form across chunks before re-keying.
1120
- const countsByBare = new Map();
1121
- // Chunk to stay within SQLITE_MAX_VARIABLE_NUMBER (same pattern as getUtilityScoresByIds).
1122
- for (let i = 0; i < bareForms.length; i += SQLITE_CHUNK_SIZE) {
1123
- const chunk = bareForms.slice(i, i + SQLITE_CHUNK_SIZE);
1124
- const placeholders = chunk.map(() => "?").join(", ");
1125
- // Normalize the stored entry_ref to its bare form inside SQL by stripping
1126
- // everything up to and including the last `//` separator. SQLite has no
1127
- // rfind, but stored origins never themselves contain `//`, so a stash ref
1128
- // has exactly one `//` and `substr(... instr ...)` is exact; bare refs have
1129
- // no `//` and pass through unchanged.
1130
- const rows = db
1131
- .prepare(`SELECT
1132
- CASE
1133
- WHEN instr(entry_ref, '//') > 0
1134
- THEN substr(entry_ref, instr(entry_ref, '//') + 2)
1135
- ELSE entry_ref
1136
- END AS bare_ref,
1137
- COUNT(*) AS cnt
1138
- FROM usage_events
1139
- WHERE event_type IN ('search','show','curate')
1140
- AND entry_ref IS NOT NULL
1141
- AND (source IS NULL OR source NOT IN ('improve','task'))
1142
- AND CASE
1143
- WHEN instr(entry_ref, '//') > 0
1144
- THEN substr(entry_ref, instr(entry_ref, '//') + 2)
1145
- ELSE entry_ref
1146
- END IN (${placeholders})
1147
- GROUP BY bare_ref`)
1148
- .all(...chunk);
1149
- for (const r of rows) {
1150
- countsByBare.set(r.bare_ref, (countsByBare.get(r.bare_ref) ?? 0) + r.cnt);
1151
- }
1152
- }
1153
- // Re-key bare-form counts onto every input ref that maps to that bare form.
1154
- const result = new Map();
1155
- for (const [bare, count] of countsByBare) {
1156
- for (const input of bareToInputs.get(bare) ?? []) {
1157
- result.set(input, count);
1158
- }
1159
- }
1160
- return result;
1161
- }
1162
- /**
1163
- * Apply a MemRL reward signal to a batch of entries via exponential moving
1164
- * average (EMA): next = clamp(current + lr * (reward - current), 0, 1).
1165
- *
1166
- * Wrapped in a single transaction so all bumps succeed or fail together.
1167
- * The indexer (`akm index`) will overwrite these values at next reindex run;
1168
- * bumps are intentionally temporary hints between index runs, not permanent
1169
- * overrides.
1170
- *
1171
- * When `scopeKey` is provided, also writes a scoped bump to
1172
- * `utility_scores_scoped` so per-project usage signals accumulate alongside
1173
- * the global ones. The global table is always updated regardless.
1174
- */
1175
- export function bumpUtilityScoresBatch(db, entryIds, reward, lr = 0.1, scopeKey) {
1176
- if (entryIds.length === 0)
1177
- return;
1178
- db.transaction(() => {
1179
- const { global: scoreMap } = getUtilityScoresByIds(db, entryIds);
1180
- const now = new Date().toISOString();
1181
- const nowMs = Date.now();
1182
- const stmt = db.prepare(`INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
1183
- VALUES (?, ?, 0, 0, 0, ?, ?)
1184
- ON CONFLICT(entry_id) DO UPDATE SET
1185
- utility = excluded.utility,
1186
- updated_at = excluded.updated_at`);
1187
- // Prepare scoped upsert once outside the loop when scopeKey is present.
1188
- const scopedStmt = scopeKey
1189
- ? db.prepare(`INSERT INTO utility_scores_scoped (entry_id, scope_key, utility, last_used_at)
1190
- VALUES (?, ?, ?, ?)
1191
- ON CONFLICT(entry_id, scope_key) DO UPDATE SET
1192
- utility = excluded.utility,
1193
- last_used_at = excluded.last_used_at`)
1194
- : null;
1195
- for (const entryId of entryIds) {
1196
- const existing = scoreMap.get(entryId);
1197
- const current = existing?.utility ?? 0;
1198
- const next = Math.max(0, Math.min(1, current + lr * (reward - current)));
1199
- stmt.run(entryId, next, now, now);
1200
- if (scopedStmt && scopeKey) {
1201
- // Retrieve the current scoped utility so we can apply the same EMA.
1202
- const scopedCurrent = getScopedUtility(db, entryId, scopeKey);
1203
- const scopedNext = Math.max(0, Math.min(1, scopedCurrent + lr * (reward - scopedCurrent)));
1204
- scopedStmt.run(entryId, scopeKey, scopedNext, nowMs);
1205
- }
1206
- }
1207
- })();
1208
- }
1209
- /**
1210
- * Return the current utility value for a single (entry_id, scope_key) pair.
1211
- * Returns 0 when no row exists yet.
1212
- */
1213
- function getScopedUtility(db, entryId, scopeKey) {
1214
- const row = db
1215
- .prepare("SELECT utility FROM utility_scores_scoped WHERE entry_id = ? AND scope_key = ?")
1216
- .get(entryId, scopeKey);
1217
- return row?.utility ?? 0;
1218
- }
1219
- // ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
1220
- /**
1221
- * Return all entries that do not yet have an embedding row.
1222
- * Used by the embedding phase to determine which entries need vectors generated.
1223
- */
1224
- export function getAllEntriesForEmbedding(db) {
1225
- return db
1226
- .prepare(`
1227
- SELECT e.id, e.search_text AS searchText, e.entry_key AS entryKey, e.file_path AS filePath FROM entries e
1228
- WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
1229
- `)
1230
- .all();
1231
- }
1232
- /**
1233
- * Upsert a workflow document record for an indexed entry.
1234
- * Persists the parsed workflow AST as JSON alongside a FNV-1a hash of the
1235
- * source content for future incremental fast-paths.
1236
- */
1237
- export function upsertWorkflowDocument(db, entryId, doc, content) {
1238
- const sourceHash = computeSourceHash(content);
1239
- db.prepare(`INSERT INTO workflow_documents (entry_id, schema_version, document_json, source_path, source_hash, updated_at)
1240
- VALUES (?, ?, ?, ?, ?, ?)
1241
- ON CONFLICT(entry_id) DO UPDATE SET
1242
- schema_version = excluded.schema_version,
1243
- document_json = excluded.document_json,
1244
- source_path = excluded.source_path,
1245
- source_hash = excluded.source_hash,
1246
- updated_at = excluded.updated_at`).run(entryId, doc.schemaVersion, JSON.stringify(doc), doc.source.path, sourceHash, new Date().toISOString());
1247
- }
1248
- /**
1249
- * Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
1250
- * Not security-sensitive; used as an incremental fast-path skip key.
1251
- */
1252
- export function computeSourceHash(content) {
1253
- let hash = 0x811c9dc5;
1254
- for (let i = 0; i < content.length; i++) {
1255
- hash ^= content[i];
1256
- hash = Math.imul(hash, 0x01000193);
1257
- }
1258
- return (hash >>> 0).toString(16);
1259
- }
1260
- /**
1261
- * Return distinct zero-result search queries from the `usage_events` table
1262
- * within the given lookback window.
1263
- *
1264
- * Reads from `usage_events` (event_type = 'search') where the metadata JSON
1265
- * blob contains `resultCount = 0`. The `search_events` table never existed;
1266
- * all errors are caught and an empty array is returned so callers never need
1267
- * to guard against DB schema differences.
1268
- */
1269
- export function getZeroResultSearches(db, sinceDays = 30) {
1270
- const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
1271
- try {
1272
- const rows = db
1273
- .prepare(`SELECT DISTINCT json_extract(metadata, '$.query') AS query
1274
- FROM usage_events
1275
- WHERE event_type = 'search'
1276
- AND created_at >= ?
1277
- AND json_extract(metadata, '$.resultCount') = 0
1278
- ORDER BY created_at DESC LIMIT 20`)
1279
- .all(since);
1280
- return rows.map((r) => r.query).filter((q) => q !== null);
1281
- }
1282
- catch {
1283
- return []; // table may not exist in older DBs
1284
- }
1285
- }
1286
- /**
1287
- * Look up an entry by its integer numeric id.
1288
- * Returns null when no matching row is found.
1289
- */
1290
- export function getEntryByRef(db, type, name) {
1291
- return db.prepare("SELECT id FROM entries WHERE entry_type = ? AND entry_key = ?").get(type, `${type}:${name}`);
1292
- }
1293
- /**
1294
- * Apply accumulated feedback counts to the utility score of an entry, persisting
1295
- * the result. The bounded-step EMA policy itself (MemRL, F-5 / #386,
1296
- * arXiv:2601.03192) lives in {@link computeNextUtility} (feedback/utility-policy);
1297
- * this function only reads the current utility, applies the policy, and writes
1298
- * the new value.
1299
- *
1300
- * A new entry starts at 0.5 (neutral midpoint) before the EMA step is applied.
1301
- * When there is no feedback (both counts zero) the score is left untouched — no
1302
- * DB write. Returns a {@link FeedbackUtilityResult} so the caller can detect a
1303
- * previously high-utility asset crossing below the review threshold and escalate.
1304
- */
1305
- export function applyFeedbackToUtilityScore(db, entryId, positiveCount, negativeCount) {
1306
- const existing = getUtilityScore(db, entryId);
1307
- const previousUtility = existing?.utility ?? 0.5;
1308
- const result = computeNextUtility(previousUtility, positiveCount, negativeCount);
1309
- if (positiveCount === 0 && negativeCount === 0) {
1310
- return result;
1311
- }
1312
- const now = new Date().toISOString();
1313
- db.prepare(`
1314
- INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
1315
- VALUES (?, ?, 0, 0, 0, ?, ?)
1316
- ON CONFLICT(entry_id) DO UPDATE SET
1317
- utility = ?,
1318
- updated_at = ?
1319
- `).run(entryId, result.nextUtility, now, now, result.nextUtility, now);
1320
- return result;
1321
- }
1322
- /**
1323
- * Re-link detached usage_events to their current entry_ids via entry_ref.
1324
- *
1325
- * After a full rebuild, entry IDs change. This restores each event's link
1326
- * using the stable `entry_ref` column so usage history survives a reindex.
1327
- */
1328
- export function relinkUsageEvents(db) {
1329
- bestEffort(() => {
1330
- // Step 1: null out stale entry_ids (entry was deleted, re-keyed, etc).
1331
- // Leaving them in place would let `recomputeUtilityScores` aggregate
1332
- // by an entry_id that no longer exists in `entries`, then trip the FK
1333
- // constraint on the utility_scores INSERT and roll back the entire
1334
- // finalize transaction. Nulled rows can be re-resolved by step 2 below;
1335
- // events whose entry is permanently gone simply stay null and age out
1336
- // via the 90-day retention policy.
1337
- db.exec(`
1338
- UPDATE usage_events
1339
- SET entry_id = NULL
1340
- WHERE entry_id IS NOT NULL
1341
- AND entry_id NOT IN (SELECT id FROM entries)
1342
- `);
1343
- // Step 2: re-resolve any null entry_id from entry_ref against the current
1344
- // entries table, reusing the SAME canonical resolver the read path uses at
1345
- // insert time (`findEntryIdByRef` → `parseAssetRef`). Resolving per DISTINCT
1346
- // ref keeps this O(distinct-refs) indexed lookups instead of the previous
1347
- // O(events × entries) non-indexable `substr(entry_key, …)` scan. It also
1348
- // fixes a silent correctness bug: the old suffix match compared the RAW
1349
- // `entry_ref`, so origin-qualified refs ("source//type:name") never matched
1350
- // an `entry_key` and lost their usage history on every full rebuild.
1351
- const refs = db
1352
- .prepare("SELECT DISTINCT entry_ref AS ref FROM usage_events WHERE entry_id IS NULL AND entry_ref IS NOT NULL")
1353
- .all();
1354
- const update = db.prepare("UPDATE usage_events SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL");
1355
- const relinkTx = db.transaction(() => {
1356
- for (const { ref } of refs) {
1357
- let id;
1358
- try {
1359
- id = findEntryIdByRef(db, ref);
1360
- }
1361
- catch (err) {
1362
- if (err instanceof Error && err.name === "UsageError")
1363
- continue;
1364
- throw err;
1365
- }
1366
- if (id !== undefined)
1367
- update.run(id, ref);
1368
- }
1369
- });
1370
- relinkTx();
1371
- }, "usage_events table may not exist yet during entry_id re-resolution");
1372
- }
1373
- // ── registry_index_cache helpers ─────────────────────────────────────────────
1374
- // The raw SQL for the `registry_index_cache` table now lives in the storage
1375
- // layer (`src/storage/repositories/registry-index-cache-repository.ts`) so the
1376
- // dependency arrow points indexer → storage. These thin re-exports preserve the
1377
- // previously-public symbols for any importer of this module.
1378
- export { getRegistryIndexCache, upsertRegistryIndexCache, } from "../../storage/repositories/registry-index-cache-repository.js";
1379
- /**
1380
- * Walk indexed entries and collect a deduplicated set of tags. When
1381
- * `entryType` is provided, only entries of that type contribute tags.
1382
- *
1383
- * Pure read; never mutates the DB. Used by `akm lessons coverage` (Phase 7A)
1384
- * to compute the diff between all-asset tags and lesson tags. Tags are
1385
- * normalised by trimming and lower-casing, and blank tags are dropped.
1386
- *
1387
- * SQL owner: this module owns ALL raw SQL against the `entries` table (WS5),
1388
- * so the `lessons coverage` read lives here rather than leaking into cli.ts.
1389
- * The result set is fully materialised (`.all()` then iterate) before return.
1390
- */
1391
- export function collectTagSetFromEntries(db, entryType) {
1392
- const tags = new Set();
1393
- const stmt = entryType
1394
- ? db.prepare("SELECT entry_json FROM entries WHERE entry_type = ?")
1395
- : db.prepare("SELECT entry_json FROM entries");
1396
- const rows = (entryType ? stmt.all(entryType) : stmt.all());
1397
- for (const row of rows) {
1398
- let parsed;
1399
- try {
1400
- parsed = JSON.parse(row.entry_json);
1401
- }
1402
- catch {
1403
- continue;
1404
- }
1405
- if (!Array.isArray(parsed.tags))
1406
- continue;
1407
- for (const tag of parsed.tags) {
1408
- if (typeof tag === "string" && tag.trim().length > 0) {
1409
- tags.add(tag.trim().toLowerCase());
1410
- }
1411
- }
1412
- }
1413
- return tags;
1414
- }