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