akm-cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +712 -34
  3. package/dist/{commands → src/commands}/config-cli.js +47 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +191 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +1 -1
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +71 -7
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +104 -4
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +73 -0
  16. package/dist/{commands → src/commands}/source-add.js +5 -1
  17. package/dist/{commands → src/commands}/source-manage.js +7 -1
  18. package/dist/{core → src/core}/asset-ref.js +5 -5
  19. package/dist/{core → src/core}/asset-spec.js +12 -0
  20. package/dist/{core → src/core}/common.js +1 -1
  21. package/dist/{core → src/core}/config.js +203 -121
  22. package/dist/{core → src/core}/errors.js +4 -0
  23. package/dist/src/core/events.js +239 -0
  24. package/dist/src/core/lesson-lint.js +86 -0
  25. package/dist/src/core/proposals.js +406 -0
  26. package/dist/src/core/warn.js +72 -0
  27. package/dist/{core → src/core}/write-source.js +80 -5
  28. package/dist/{indexer → src/indexer}/db-search.js +114 -24
  29. package/dist/{indexer → src/indexer}/db.js +76 -23
  30. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  31. package/dist/src/indexer/graph-boost.js +179 -0
  32. package/dist/src/indexer/graph-extraction.js +212 -0
  33. package/dist/{indexer → src/indexer}/indexer.js +88 -7
  34. package/dist/{indexer → src/indexer}/matchers.js +1 -1
  35. package/dist/src/indexer/memory-inference.js +263 -0
  36. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  37. package/dist/{indexer → src/indexer}/search-source.js +4 -2
  38. package/dist/src/integrations/agent/config.js +292 -0
  39. package/dist/src/integrations/agent/detect.js +94 -0
  40. package/dist/src/integrations/agent/index.js +17 -0
  41. package/dist/src/integrations/agent/profiles.js +65 -0
  42. package/dist/src/integrations/agent/prompts.js +167 -0
  43. package/dist/src/integrations/agent/spawn.js +272 -0
  44. package/dist/{integrations → src/integrations}/github.js +9 -3
  45. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  46. package/dist/{llm → src/llm}/client.js +33 -2
  47. package/dist/{llm → src/llm}/embedders/remote.js +37 -3
  48. package/dist/src/llm/feature-gate.js +108 -0
  49. package/dist/src/llm/graph-extract.js +107 -0
  50. package/dist/src/llm/index-passes.js +35 -0
  51. package/dist/src/llm/memory-infer.js +86 -0
  52. package/dist/{output → src/output}/cli-hints.js +15 -2
  53. package/dist/{output → src/output}/renderers.js +63 -2
  54. package/dist/src/output/shapes.js +523 -0
  55. package/dist/src/output/text.js +1116 -0
  56. package/dist/{registry → src/registry}/build-index.js +19 -8
  57. package/dist/{registry → src/registry}/factory.js +0 -8
  58. package/dist/{registry → src/registry}/providers/static-index.js +6 -3
  59. package/dist/{registry → src/registry}/resolve.js +68 -2
  60. package/dist/{setup → src/setup}/setup.js +52 -5
  61. package/dist/{sources → src/sources}/providers/git.js +7 -15
  62. package/dist/{wiki → src/wiki}/wiki.js +54 -6
  63. package/dist/{workflows → src/workflows}/runs.js +37 -3
  64. package/dist/tests/add-website-source.test.js +119 -0
  65. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  66. package/dist/tests/agent/agent-config.test.js +221 -0
  67. package/dist/tests/agent/agent-detect.test.js +100 -0
  68. package/dist/tests/agent/agent-spawn.test.js +234 -0
  69. package/dist/tests/agent-output.test.js +186 -0
  70. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  71. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  72. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  73. package/dist/tests/asset-ref.test.js +192 -0
  74. package/dist/tests/asset-registry.test.js +103 -0
  75. package/dist/tests/asset-spec.test.js +241 -0
  76. package/dist/tests/bench/attribution.test.js +996 -0
  77. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  78. package/dist/tests/bench/cleanup.js +234 -0
  79. package/dist/tests/bench/cleanup.test.js +166 -0
  80. package/dist/tests/bench/cli.js +1018 -0
  81. package/dist/tests/bench/cli.test.js +445 -0
  82. package/dist/tests/bench/compare.test.js +556 -0
  83. package/dist/tests/bench/corpus.js +317 -0
  84. package/dist/tests/bench/corpus.test.js +258 -0
  85. package/dist/tests/bench/doctor.js +525 -0
  86. package/dist/tests/bench/driver.js +401 -0
  87. package/dist/tests/bench/driver.test.js +584 -0
  88. package/dist/tests/bench/environment.js +233 -0
  89. package/dist/tests/bench/environment.test.js +199 -0
  90. package/dist/tests/bench/evolve-metrics.js +179 -0
  91. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  92. package/dist/tests/bench/evolve.js +647 -0
  93. package/dist/tests/bench/evolve.test.js +624 -0
  94. package/dist/tests/bench/failure-modes.test.js +349 -0
  95. package/dist/tests/bench/feedback-integrity.test.js +457 -0
  96. package/dist/tests/bench/leakage.test.js +228 -0
  97. package/dist/tests/bench/learning-curve.test.js +134 -0
  98. package/dist/tests/bench/metrics.js +2395 -0
  99. package/dist/tests/bench/metrics.test.js +1150 -0
  100. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  101. package/dist/tests/bench/opencode-config.js +194 -0
  102. package/dist/tests/bench/opencode-config.test.js +370 -0
  103. package/dist/tests/bench/report.js +1885 -0
  104. package/dist/tests/bench/report.test.js +1038 -0
  105. package/dist/tests/bench/run-config.js +355 -0
  106. package/dist/tests/bench/run-config.test.js +298 -0
  107. package/dist/tests/bench/run-curate-test.js +32 -0
  108. package/dist/tests/bench/run-failing-tasks.js +56 -0
  109. package/dist/tests/bench/run-full-bench.js +51 -0
  110. package/dist/tests/bench/run-items36-targeted.js +69 -0
  111. package/dist/tests/bench/run-nano-quick.js +42 -0
  112. package/dist/tests/bench/run-waveg-targeted.js +62 -0
  113. package/dist/tests/bench/runner.js +699 -0
  114. package/dist/tests/bench/runner.test.js +958 -0
  115. package/dist/tests/bench/search-bridge.test.js +331 -0
  116. package/dist/tests/bench/tmp.js +131 -0
  117. package/dist/tests/bench/trajectory.js +116 -0
  118. package/dist/tests/bench/trajectory.test.js +127 -0
  119. package/dist/tests/bench/verifier.js +114 -0
  120. package/dist/tests/bench/verifier.test.js +118 -0
  121. package/dist/tests/bench/workflow-evaluator.js +557 -0
  122. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  123. package/dist/tests/bench/workflow-spec.js +345 -0
  124. package/dist/tests/bench/workflow-spec.test.js +363 -0
  125. package/dist/tests/bench/workflow-trace.js +472 -0
  126. package/dist/tests/bench/workflow-trace.test.js +254 -0
  127. package/dist/tests/benchmark-search-quality.js +536 -0
  128. package/dist/tests/benchmark-suite.js +1441 -0
  129. package/dist/tests/capture-cli.test.js +112 -0
  130. package/dist/tests/cli-errors.test.js +204 -0
  131. package/dist/tests/commands/events.test.js +370 -0
  132. package/dist/tests/commands/history.test.js +418 -0
  133. package/dist/tests/commands/import.test.js +103 -0
  134. package/dist/tests/commands/proposal-cli.test.js +209 -0
  135. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  136. package/dist/tests/commands/remember.test.js +97 -0
  137. package/dist/tests/commands/scope-flags.test.js +300 -0
  138. package/dist/tests/commands/search.test.js +537 -0
  139. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  140. package/dist/tests/commands/show.test.js +294 -0
  141. package/dist/tests/common.test.js +266 -0
  142. package/dist/tests/completions.test.js +142 -0
  143. package/dist/tests/config-cli.test.js +193 -0
  144. package/dist/tests/config-llm-features.test.js +139 -0
  145. package/dist/tests/config.test.js +569 -0
  146. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  147. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  148. package/dist/tests/contracts/spec-helpers.js +46 -0
  149. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  150. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  151. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  152. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  153. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  154. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  155. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  156. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  157. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  158. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  159. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  160. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  161. package/dist/tests/core/write-source.test.js +366 -0
  162. package/dist/tests/curate-command.test.js +87 -0
  163. package/dist/tests/db-scoring.test.js +201 -0
  164. package/dist/tests/db.test.js +654 -0
  165. package/dist/tests/distill-cli-flag.test.js +208 -0
  166. package/dist/tests/distill.test.js +515 -0
  167. package/dist/tests/docker-install.test.js +120 -0
  168. package/dist/tests/e2e.test.js +1419 -0
  169. package/dist/tests/embedder.test.js +340 -0
  170. package/dist/tests/embedding-model-config.test.js +379 -0
  171. package/dist/tests/feedback-command.test.js +172 -0
  172. package/dist/tests/file-context.test.js +552 -0
  173. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  174. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  175. package/dist/tests/fixtures/stashes/load.js +166 -0
  176. package/dist/tests/fixtures/stashes/load.test.js +97 -0
  177. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  178. package/dist/tests/frontmatter.test.js +190 -0
  179. package/dist/tests/fts-field-weighting.test.js +254 -0
  180. package/dist/tests/fuzzy-search.test.js +230 -0
  181. package/dist/tests/git-provider-clone.test.js +45 -0
  182. package/dist/tests/github.test.js +161 -0
  183. package/dist/tests/graph-boost-ranking.test.js +305 -0
  184. package/dist/tests/graph-extraction.test.js +282 -0
  185. package/dist/tests/helpers/usage-events.js +8 -0
  186. package/dist/tests/index-pass-llm.test.js +161 -0
  187. package/dist/tests/indexer.test.js +570 -0
  188. package/dist/tests/info-command.test.js +166 -0
  189. package/dist/tests/init.test.js +69 -0
  190. package/dist/tests/install-script.test.js +246 -0
  191. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  192. package/dist/tests/issue-36-repro.test.js +304 -0
  193. package/dist/tests/issues-191-194.test.js +160 -0
  194. package/dist/tests/lesson-lint.test.js +111 -0
  195. package/dist/tests/llm-client.test.js +115 -0
  196. package/dist/tests/llm-feature-gate.test.js +151 -0
  197. package/dist/tests/llm.test.js +139 -0
  198. package/dist/tests/lockfile.test.js +216 -0
  199. package/dist/tests/manifest.test.js +205 -0
  200. package/dist/tests/markdown.test.js +126 -0
  201. package/dist/tests/matchers-unit.test.js +189 -0
  202. package/dist/tests/memory-inference.test.js +299 -0
  203. package/dist/tests/merge-scoring.test.js +136 -0
  204. package/dist/tests/metadata.test.js +313 -0
  205. package/dist/tests/migration-help.test.js +89 -0
  206. package/dist/tests/origin-resolve.test.js +124 -0
  207. package/dist/tests/output-baseline.test.js +218 -0
  208. package/dist/tests/output-shapes-unit.test.js +478 -0
  209. package/dist/tests/parallel-search.test.js +272 -0
  210. package/dist/tests/parameter-metadata.test.js +365 -0
  211. package/dist/tests/paths.test.js +177 -0
  212. package/dist/tests/progressive-disclosure.test.js +280 -0
  213. package/dist/tests/proposals.test.js +279 -0
  214. package/dist/tests/proposed-quality.test.js +271 -0
  215. package/dist/tests/provider-registry.test.js +32 -0
  216. package/dist/tests/ranking-regression.test.js +548 -0
  217. package/dist/tests/reflect-propose.test.js +455 -0
  218. package/dist/tests/registry-build-index.test.js +394 -0
  219. package/dist/tests/registry-cli.test.js +290 -0
  220. package/dist/tests/registry-index-v2.test.js +430 -0
  221. package/dist/tests/registry-install.test.js +728 -0
  222. package/dist/tests/registry-providers/parity.test.js +189 -0
  223. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  224. package/dist/tests/registry-providers/static-index.test.js +238 -0
  225. package/dist/tests/registry-resolve.test.js +126 -0
  226. package/dist/tests/registry-search.test.js +923 -0
  227. package/dist/tests/remember-frontmatter.test.js +378 -0
  228. package/dist/tests/remember-unit.test.js +123 -0
  229. package/dist/tests/ripgrep-install.test.js +251 -0
  230. package/dist/tests/ripgrep-resolve.test.js +108 -0
  231. package/dist/tests/ripgrep.test.js +163 -0
  232. package/dist/tests/save-command.test.js +94 -0
  233. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  234. package/dist/tests/scoring-pipeline.test.js +648 -0
  235. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  236. package/dist/tests/self-update.test.js +442 -0
  237. package/dist/tests/semantic-search-e2e.test.js +512 -0
  238. package/dist/tests/semantic-status.test.js +471 -0
  239. package/dist/tests/setup-run.integration.js +877 -0
  240. package/dist/tests/setup-wizard.test.js +198 -0
  241. package/dist/tests/setup.test.js +131 -0
  242. package/dist/tests/source-add.test.js +11 -0
  243. package/dist/tests/source-clone.test.js +254 -0
  244. package/dist/tests/source-manage.test.js +366 -0
  245. package/dist/tests/source-providers/filesystem.test.js +82 -0
  246. package/dist/tests/source-providers/git.test.js +252 -0
  247. package/dist/tests/source-providers/website.test.js +128 -0
  248. package/dist/tests/source-qa-fixes.test.js +286 -0
  249. package/dist/tests/source-registry.test.js +350 -0
  250. package/dist/tests/source-resolve.test.js +100 -0
  251. package/dist/tests/source-source.test.js +281 -0
  252. package/dist/tests/source.test.js +533 -0
  253. package/dist/tests/tar-utils-scan.test.js +73 -0
  254. package/dist/tests/toggle-components.test.js +73 -0
  255. package/dist/tests/usage-telemetry.test.js +265 -0
  256. package/dist/tests/utility-scoring.test.js +558 -0
  257. package/dist/tests/vault-load-error.test.js +78 -0
  258. package/dist/tests/vault-qa-fixes.test.js +194 -0
  259. package/dist/tests/vault.test.js +429 -0
  260. package/dist/tests/vector-search.test.js +608 -0
  261. package/dist/tests/walker.test.js +252 -0
  262. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  263. package/dist/tests/wave2-cluster-d.test.js +180 -0
  264. package/dist/tests/wave2-cluster-e.test.js +179 -0
  265. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  266. package/dist/tests/wiki.test.js +529 -0
  267. package/dist/tests/workflow-cli.test.js +271 -0
  268. package/dist/tests/workflow-markdown.test.js +171 -0
  269. package/dist/tests/workflow-path-escape.test.js +132 -0
  270. package/dist/tests/workflow-qa-fixes.test.js +395 -0
  271. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  272. package/docs/README.md +8 -0
  273. package/docs/migration/release-notes/0.7.0.md +244 -0
  274. package/package.json +2 -2
  275. package/dist/core/warn.js +0 -27
  276. package/dist/output/shapes.js +0 -212
  277. package/dist/output/text.js +0 -520
  278. /package/dist/{commands → src/commands}/completions.js +0 -0
  279. /package/dist/{commands → src/commands}/curate.js +0 -0
  280. /package/dist/{commands → src/commands}/info.js +0 -0
  281. /package/dist/{commands → src/commands}/init.js +0 -0
  282. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  283. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  284. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  285. /package/dist/{commands → src/commands}/vault.js +0 -0
  286. /package/dist/{core → src/core}/asset-registry.js +0 -0
  287. /package/dist/{core → src/core}/frontmatter.js +0 -0
  288. /package/dist/{core → src/core}/markdown.js +0 -0
  289. /package/dist/{core → src/core}/paths.js +0 -0
  290. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  291. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  292. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  293. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  294. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  295. /package/dist/{llm → src/llm}/embedder.js +0 -0
  296. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  297. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  298. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  299. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  300. /package/dist/{output → src/output}/context.js +0 -0
  301. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  302. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  303. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  304. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  305. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  306. /package/dist/{registry → src/registry}/types.js +0 -0
  307. /package/dist/{setup → src/setup}/detect.js +0 -0
  308. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  309. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  310. /package/dist/{setup → src/setup}/steps.js +0 -0
  311. /package/dist/{sources → src/sources}/include.js +0 -0
  312. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  313. /package/dist/{sources → src/sources}/provider.js +0 -0
  314. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  315. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  316. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  317. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  318. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  319. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  320. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  321. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  322. /package/dist/{sources → src/sources}/resolve.js +0 -0
  323. /package/dist/{sources → src/sources}/types.js +0 -0
  324. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  325. /package/dist/{version.js → src/version.js} +0 -0
  326. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  327. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  328. /package/dist/{workflows → src/workflows}/db.js +0 -0
  329. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  330. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  331. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  332. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  333. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Tests for the graph-extraction pass (#207).
3
+ *
4
+ * `extractGraphFromBody` is mocked via `mock.module` so no real LLM call
5
+ * is ever made. These tests cover:
6
+ * - eligible-file detection (memory + knowledge .md, inferred children skipped)
7
+ * - the disabled-by-default path (no `akm.llm` configured)
8
+ * - the `index.graph.llm = false` per-pass opt-out
9
+ * - the `llm.features.graph_extraction = false` feature-gate opt-out
10
+ * - graph.json is written under `<stashRoot>/.akm/`
11
+ * - toggling off after a successful run leaves the existing graph.json on disk
12
+ * - read-only cache sources are not extracted (only the primary stash)
13
+ */
14
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
15
+ import fs from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ // ── Module-level LLM stub ───────────────────────────────────────────────────
19
+ let extractor = () => ({
20
+ entities: [],
21
+ relations: [],
22
+ });
23
+ mock.module("../src/llm/graph-extract", () => ({
24
+ extractGraphFromBody: async (_config, body) => extractor(body),
25
+ }));
26
+ // Import AFTER mock.module so the pass picks up the stub.
27
+ const { runGraphExtractionPass, collectEligibleFiles, getGraphFilePath, GRAPH_FILE_SCHEMA_VERSION } = await import("../src/indexer/graph-extraction");
28
+ // ── Fixture helpers ─────────────────────────────────────────────────────────
29
+ let tmpStash = "";
30
+ beforeEach(() => {
31
+ tmpStash = fs.mkdtempSync(path.join(os.tmpdir(), "akm-graph-ext-"));
32
+ fs.mkdirSync(path.join(tmpStash, "memories"), { recursive: true });
33
+ fs.mkdirSync(path.join(tmpStash, "knowledge"), { recursive: true });
34
+ extractor = () => ({ entities: [], relations: [] });
35
+ });
36
+ afterEach(() => {
37
+ if (tmpStash) {
38
+ fs.rmSync(tmpStash, { recursive: true, force: true });
39
+ tmpStash = "";
40
+ }
41
+ });
42
+ function writeFile(rel, frontmatter, body) {
43
+ const fmLines = ["---"];
44
+ for (const [key, value] of Object.entries(frontmatter)) {
45
+ fmLines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
46
+ }
47
+ fmLines.push("---");
48
+ const content = `${fmLines.join("\n")}\n\n${body}\n`;
49
+ const filePath = path.join(tmpStash, rel);
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, content, "utf8");
52
+ return filePath;
53
+ }
54
+ const SAMPLE_LLM = {
55
+ endpoint: "http://localhost:11434/v1/chat/completions",
56
+ model: "llama3.2",
57
+ };
58
+ function configWithLlm(overrides) {
59
+ return {
60
+ semanticSearchMode: "auto",
61
+ llm: { ...SAMPLE_LLM },
62
+ ...overrides,
63
+ };
64
+ }
65
+ function sources() {
66
+ return [{ path: tmpStash }];
67
+ }
68
+ // ── collectEligibleFiles ────────────────────────────────────────────────────
69
+ describe("collectEligibleFiles", () => {
70
+ test("returns empty when neither memories/ nor knowledge/ exists", () => {
71
+ const fresh = fs.mkdtempSync(path.join(os.tmpdir(), "akm-graph-empty-"));
72
+ try {
73
+ expect(collectEligibleFiles(fresh)).toEqual([]);
74
+ }
75
+ finally {
76
+ fs.rmSync(fresh, { recursive: true, force: true });
77
+ }
78
+ });
79
+ test("walks memories/ and knowledge/ markdown files", () => {
80
+ writeFile("memories/m1.md", {}, "Memory body about ServiceA and ServiceB.");
81
+ writeFile("knowledge/k1.md", {}, "Knowledge body about ServiceB.");
82
+ writeFile("memories/sub/m2.md", {}, "Nested memory body.");
83
+ const eligible = collectEligibleFiles(tmpStash);
84
+ const names = eligible.map((e) => path.relative(tmpStash, e.absPath)).sort();
85
+ expect(names).toEqual([
86
+ path.join("knowledge", "k1.md"),
87
+ path.join("memories", "m1.md"),
88
+ path.join("memories", "sub", "m2.md"),
89
+ ]);
90
+ });
91
+ test("skips inferred memory children", () => {
92
+ writeFile("memories/parent.md", {}, "Parent body.");
93
+ writeFile("memories/parent.facts/fact-1.md", { inferred: true, source: "memory:parent" }, "Atomic.");
94
+ const eligible = collectEligibleFiles(tmpStash);
95
+ const names = eligible.map((e) => path.relative(tmpStash, e.absPath));
96
+ expect(names).toContain(path.join("memories", "parent.md"));
97
+ expect(names).not.toContain(path.join("memories", "parent.facts", "fact-1.md"));
98
+ });
99
+ test("skips empty bodies", () => {
100
+ // File with parseable frontmatter and a whitespace-only body. The
101
+ // empty `{}` frontmatter form is degenerate (no delimiters with
102
+ // contents between them), so we use a single key to force a real
103
+ // frontmatter block.
104
+ writeFile("memories/empty.md", { type: "memory" }, " \n\n ");
105
+ expect(collectEligibleFiles(tmpStash)).toEqual([]);
106
+ });
107
+ });
108
+ // ── runGraphExtractionPass — disabled paths ────────────────────────────────
109
+ describe("runGraphExtractionPass — disabled paths", () => {
110
+ test("no-op when no akm.llm is configured", async () => {
111
+ writeFile("memories/m1.md", {}, "Body.");
112
+ extractor = () => {
113
+ throw new Error("must not be called when no llm is configured");
114
+ };
115
+ const result = await runGraphExtractionPass({ semanticSearchMode: "auto" }, sources());
116
+ expect(result.written).toBe(false);
117
+ expect(result.considered).toBe(0);
118
+ expect(fs.existsSync(getGraphFilePath(tmpStash))).toBe(false);
119
+ });
120
+ test("no-op when index.graph.llm = false", async () => {
121
+ writeFile("memories/m1.md", {}, "Body.");
122
+ extractor = () => {
123
+ throw new Error("must not be called when per-pass disabled");
124
+ };
125
+ const cfg = configWithLlm({ index: { graph: { llm: false } } });
126
+ const result = await runGraphExtractionPass(cfg, sources());
127
+ expect(result.written).toBe(false);
128
+ expect(fs.existsSync(getGraphFilePath(tmpStash))).toBe(false);
129
+ });
130
+ test("no-op when llm.features.graph_extraction = false", async () => {
131
+ writeFile("memories/m1.md", {}, "Body.");
132
+ extractor = () => {
133
+ throw new Error("must not be called when feature-gated off");
134
+ };
135
+ const cfg = {
136
+ semanticSearchMode: "auto",
137
+ llm: { ...SAMPLE_LLM, features: { graph_extraction: false } },
138
+ index: { graph: { llm: true } },
139
+ };
140
+ const result = await runGraphExtractionPass(cfg, sources());
141
+ expect(result.written).toBe(false);
142
+ expect(fs.existsSync(getGraphFilePath(tmpStash))).toBe(false);
143
+ });
144
+ test("toggling off after a successful run preserves the existing graph.json", async () => {
145
+ writeFile("memories/m1.md", {}, "Body.");
146
+ extractor = () => ({ entities: ["ServiceA"], relations: [] });
147
+ await runGraphExtractionPass(configWithLlm(), sources());
148
+ const graphPath = getGraphFilePath(tmpStash);
149
+ expect(fs.existsSync(graphPath)).toBe(true);
150
+ const beforeBytes = fs.readFileSync(graphPath, "utf8");
151
+ extractor = () => {
152
+ throw new Error("must not be called when disabled");
153
+ };
154
+ await runGraphExtractionPass(configWithLlm({ index: { graph: { llm: false } } }), sources());
155
+ expect(fs.existsSync(graphPath)).toBe(true);
156
+ expect(fs.readFileSync(graphPath, "utf8")).toBe(beforeBytes);
157
+ });
158
+ });
159
+ // ── runGraphExtractionPass — orthogonal gating (§14 + #208) ────────────────
160
+ describe("runGraphExtractionPass — feature flag and per-pass key are orthogonal", () => {
161
+ test("runs when both gates allow", async () => {
162
+ writeFile("memories/m1.md", {}, "Body.");
163
+ extractor = () => ({ entities: ["E"], relations: [] });
164
+ const cfg = {
165
+ semanticSearchMode: "auto",
166
+ llm: { ...SAMPLE_LLM, features: { graph_extraction: true } },
167
+ };
168
+ const result = await runGraphExtractionPass(cfg, sources());
169
+ expect(result.written).toBe(true);
170
+ expect(result.considered).toBe(1);
171
+ expect(result.extracted).toBe(1);
172
+ });
173
+ test("no-op cleanly when feature + per-pass gates allow but akm.llm is absent (third precondition)", async () => {
174
+ // Three preconditions must ALL hold for the pass to run:
175
+ // 1. `akm.llm` configured (this test removes it)
176
+ // 2. `llm.features.graph_extraction !== false` (true here)
177
+ // 3. `index.graph.llm !== false` (true here)
178
+ // With #1 missing, the pass must short-circuit silently — no error
179
+ // thrown, no graph.json written, no existing graph.json modified.
180
+ writeFile("memories/m1.md", {}, "Body.");
181
+ extractor = () => {
182
+ throw new Error("must not be called when akm.llm is absent");
183
+ };
184
+ const cfg = {
185
+ semanticSearchMode: "auto",
186
+ // No `llm` block at all.
187
+ index: { graph: { llm: true } },
188
+ };
189
+ const graphPath = getGraphFilePath(tmpStash);
190
+ expect(fs.existsSync(graphPath)).toBe(false);
191
+ const result = await runGraphExtractionPass(cfg, sources());
192
+ expect(result.written).toBe(false);
193
+ expect(result.considered).toBe(0);
194
+ expect(result.extracted).toBe(0);
195
+ expect(fs.existsSync(graphPath)).toBe(false);
196
+ });
197
+ test("either gate set to false short-circuits", async () => {
198
+ writeFile("memories/m1.md", {}, "Body.");
199
+ extractor = () => ({ entities: ["E"], relations: [] });
200
+ const featureOff = await runGraphExtractionPass({
201
+ semanticSearchMode: "auto",
202
+ llm: { ...SAMPLE_LLM, features: { graph_extraction: false } },
203
+ }, sources());
204
+ expect(featureOff.written).toBe(false);
205
+ const passOff = await runGraphExtractionPass({
206
+ semanticSearchMode: "auto",
207
+ llm: { ...SAMPLE_LLM, features: { graph_extraction: true } },
208
+ index: { graph: { llm: false } },
209
+ }, sources());
210
+ expect(passOff.written).toBe(false);
211
+ });
212
+ });
213
+ // ── runGraphExtractionPass — enabled path ──────────────────────────────────
214
+ describe("runGraphExtractionPass — enabled", () => {
215
+ test("writes graph.json with schema version + canonicalised entities", async () => {
216
+ writeFile("memories/parent.md", {}, "Body about ServiceA and ServiceB.");
217
+ writeFile("knowledge/k1.md", {}, "Body about ServiceB and ServiceC.");
218
+ extractor = (body) => {
219
+ if (body.includes("ServiceA"))
220
+ return {
221
+ entities: ["ServiceA", "ServiceB"],
222
+ relations: [{ from: "ServiceA", to: "ServiceB", type: "uses" }],
223
+ };
224
+ return {
225
+ entities: ["ServiceB", "ServiceC"],
226
+ relations: [{ from: "ServiceB", to: "ServiceC" }],
227
+ };
228
+ };
229
+ const result = await runGraphExtractionPass(configWithLlm(), sources());
230
+ expect(result.written).toBe(true);
231
+ expect(result.considered).toBe(2);
232
+ expect(result.extracted).toBe(2);
233
+ expect(result.totalEntities).toBe(4);
234
+ expect(result.totalRelations).toBe(2);
235
+ const raw = fs.readFileSync(getGraphFilePath(tmpStash), "utf8");
236
+ const parsed = JSON.parse(raw);
237
+ expect(parsed.schemaVersion).toBe(GRAPH_FILE_SCHEMA_VERSION);
238
+ expect(parsed.stashRoot).toBe(tmpStash);
239
+ expect(parsed.files).toHaveLength(2);
240
+ // Entities are lower-cased at write time so the search-time boost
241
+ // doesn't have to re-canonicalise on every query.
242
+ for (const node of parsed.files) {
243
+ for (const e of node.entities)
244
+ expect(e).toBe(e.toLowerCase());
245
+ for (const r of node.relations) {
246
+ expect(r.from).toBe(r.from.toLowerCase());
247
+ expect(r.to).toBe(r.to.toLowerCase());
248
+ }
249
+ }
250
+ });
251
+ test("files with no extracted entities are omitted but still considered", async () => {
252
+ writeFile("memories/m1.md", {}, "Empty graph body.");
253
+ writeFile("memories/m2.md", {}, "Has entities.");
254
+ extractor = (body) => {
255
+ if (body.includes("Has entities"))
256
+ return { entities: ["X"], relations: [] };
257
+ return { entities: [], relations: [] };
258
+ };
259
+ const result = await runGraphExtractionPass(configWithLlm(), sources());
260
+ expect(result.considered).toBe(2);
261
+ expect(result.extracted).toBe(1);
262
+ expect(result.written).toBe(true);
263
+ const parsed = JSON.parse(fs.readFileSync(getGraphFilePath(tmpStash), "utf8"));
264
+ expect(parsed.files).toHaveLength(1);
265
+ });
266
+ test("does not extract from cache-only sources (only the primary stash)", async () => {
267
+ const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-graph-cache-"));
268
+ try {
269
+ fs.mkdirSync(path.join(cacheDir, "memories"), { recursive: true });
270
+ fs.writeFileSync(path.join(cacheDir, "memories", "cache.md"), "---\n---\n\nCache body about X.\n");
271
+ writeFile("memories/m1.md", {}, "Primary body.");
272
+ extractor = () => ({ entities: ["X"], relations: [] });
273
+ const result = await runGraphExtractionPass(configWithLlm(), [{ path: tmpStash }, { path: cacheDir }]);
274
+ expect(result.considered).toBe(1);
275
+ expect(fs.existsSync(getGraphFilePath(cacheDir))).toBe(false);
276
+ expect(fs.existsSync(getGraphFilePath(tmpStash))).toBe(true);
277
+ }
278
+ finally {
279
+ fs.rmSync(cacheDir, { recursive: true, force: true });
280
+ }
281
+ });
282
+ });
@@ -0,0 +1,8 @@
1
+ import { ensureUsageEventsSchema } from "../../src/indexer/usage-events";
2
+ /**
3
+ * Record a usage event (test-only helper for M-2 utility scoring tests).
4
+ */
5
+ export function recordUsageEvent(db, event) {
6
+ ensureUsageEventsSchema(db);
7
+ db.prepare("INSERT INTO usage_events (event_type, entry_id, query, created_at) VALUES (?, ?, ?, ?)").run(event.eventType, event.entryId, event.query ?? null, event.timestamp ?? new Date().toISOString());
8
+ }
@@ -0,0 +1,161 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { getConfigPath, loadUserConfig, resetConfigCache } from "../src/core/config";
6
+ import { ConfigError } from "../src/core/errors";
7
+ import { resolveIndexPassLLM } from "../src/llm/index-passes";
8
+ // Tests for #208 — unified `akm.llm` config across all index-time passes.
9
+ function makeTmpDir() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), "akm-index-pass-llm-"));
11
+ }
12
+ const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
13
+ let tmpHome = "";
14
+ beforeEach(() => {
15
+ tmpHome = makeTmpDir();
16
+ process.env.XDG_CONFIG_HOME = tmpHome;
17
+ resetConfigCache();
18
+ });
19
+ afterEach(() => {
20
+ if (originalXdgConfigHome === undefined) {
21
+ delete process.env.XDG_CONFIG_HOME;
22
+ }
23
+ else {
24
+ process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
25
+ }
26
+ if (tmpHome) {
27
+ fs.rmSync(tmpHome, { recursive: true, force: true });
28
+ tmpHome = "";
29
+ }
30
+ resetConfigCache();
31
+ });
32
+ function writeUserConfig(raw) {
33
+ const configPath = getConfigPath();
34
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
35
+ fs.writeFileSync(configPath, JSON.stringify(raw, null, 2));
36
+ }
37
+ const SAMPLE_LLM = {
38
+ endpoint: "http://localhost:11434/v1/chat/completions",
39
+ model: "llama3.2",
40
+ };
41
+ describe("resolveIndexPassLLM", () => {
42
+ test("returns undefined when no top-level llm is configured", () => {
43
+ const config = { semanticSearchMode: "auto" };
44
+ expect(resolveIndexPassLLM("enrichment", config)).toBeUndefined();
45
+ expect(resolveIndexPassLLM("graph", config)).toBeUndefined();
46
+ });
47
+ test("returns the shared akm.llm by default for any pass", () => {
48
+ const config = {
49
+ semanticSearchMode: "auto",
50
+ llm: { ...SAMPLE_LLM },
51
+ };
52
+ expect(resolveIndexPassLLM("enrichment", config)).toEqual(SAMPLE_LLM);
53
+ // A future pass plugs in for free — same default, no per-pass wiring.
54
+ expect(resolveIndexPassLLM("memory", config)).toEqual(SAMPLE_LLM);
55
+ expect(resolveIndexPassLLM("graph", config)).toEqual(SAMPLE_LLM);
56
+ });
57
+ test("per-pass `llm: false` opts that pass out, leaving siblings intact", () => {
58
+ const config = {
59
+ semanticSearchMode: "auto",
60
+ llm: { ...SAMPLE_LLM },
61
+ index: {
62
+ enrichment: { llm: false },
63
+ graph: { llm: true },
64
+ },
65
+ };
66
+ expect(resolveIndexPassLLM("enrichment", config)).toBeUndefined();
67
+ expect(resolveIndexPassLLM("graph", config)).toEqual(SAMPLE_LLM);
68
+ // Pass not mentioned at all still defaults to akm.llm.
69
+ expect(resolveIndexPassLLM("memory", config)).toEqual(SAMPLE_LLM);
70
+ });
71
+ test("per-pass `llm: true` is equivalent to default", () => {
72
+ const config = {
73
+ semanticSearchMode: "auto",
74
+ llm: { ...SAMPLE_LLM },
75
+ index: { enrichment: { llm: true } },
76
+ };
77
+ expect(resolveIndexPassLLM("enrichment", config)).toEqual(SAMPLE_LLM);
78
+ });
79
+ });
80
+ describe("config loader: `index` block parsing", () => {
81
+ test("loads valid `index.<pass>.llm` boolean values", () => {
82
+ writeUserConfig({
83
+ llm: SAMPLE_LLM,
84
+ index: {
85
+ enrichment: { llm: false },
86
+ graph: { llm: true },
87
+ },
88
+ });
89
+ const config = loadUserConfig();
90
+ expect(config.index).toEqual({
91
+ enrichment: { llm: false },
92
+ graph: { llm: true },
93
+ });
94
+ });
95
+ test("rejects per-pass provider configuration (duplicate provider path)", () => {
96
+ writeUserConfig({
97
+ llm: SAMPLE_LLM,
98
+ index: {
99
+ enrichment: {
100
+ endpoint: "http://other-host/v1/chat/completions",
101
+ model: "other-model",
102
+ },
103
+ },
104
+ });
105
+ expect(() => loadUserConfig()).toThrow(ConfigError);
106
+ try {
107
+ loadUserConfig();
108
+ }
109
+ catch (err) {
110
+ expect(err).toBeInstanceOf(ConfigError);
111
+ expect(err.code).toBe("INVALID_CONFIG_FILE");
112
+ expect(err.message).toContain("Duplicate LLM provider configuration");
113
+ expect(err.message).toContain("index.enrichment.endpoint");
114
+ }
115
+ });
116
+ test("rejects per-pass `provider`, `apiKey`, `temperature`, etc.", () => {
117
+ for (const key of ["provider", "apiKey", "temperature", "maxTokens", "baseUrl", "capabilities"]) {
118
+ writeUserConfig({
119
+ llm: SAMPLE_LLM,
120
+ index: { enrichment: { [key]: "anything" } },
121
+ });
122
+ resetConfigCache();
123
+ expect(() => loadUserConfig()).toThrow(/Duplicate LLM provider configuration/);
124
+ }
125
+ });
126
+ test("rejects non-boolean `llm` value under a pass", () => {
127
+ writeUserConfig({
128
+ llm: SAMPLE_LLM,
129
+ index: { enrichment: { llm: "off" } },
130
+ });
131
+ expect(() => loadUserConfig()).toThrow(/expected a boolean/);
132
+ });
133
+ test("rejects unknown keys under a pass entry", () => {
134
+ writeUserConfig({
135
+ llm: SAMPLE_LLM,
136
+ index: { enrichment: { foo: true } },
137
+ });
138
+ expect(() => loadUserConfig()).toThrow(/Unknown key `index\.enrichment\.foo`/);
139
+ });
140
+ test("rejects array-shaped `index` block", () => {
141
+ writeUserConfig({
142
+ llm: SAMPLE_LLM,
143
+ // biome-ignore lint/suspicious/noExplicitAny: testing invalid runtime input
144
+ index: [{ llm: false }],
145
+ });
146
+ expect(() => loadUserConfig()).toThrow(/expected an object keyed by pass name/);
147
+ });
148
+ test("rejects non-object pass entry", () => {
149
+ writeUserConfig({
150
+ llm: SAMPLE_LLM,
151
+ index: { enrichment: false },
152
+ });
153
+ expect(() => loadUserConfig()).toThrow(/expected an object like/);
154
+ });
155
+ test("missing `index` block is fine", () => {
156
+ writeUserConfig({ llm: SAMPLE_LLM });
157
+ const config = loadUserConfig();
158
+ expect(config.index).toBeUndefined();
159
+ expect(resolveIndexPassLLM("enrichment", config)).toEqual(SAMPLE_LLM);
160
+ });
161
+ });