akm-cli 0.6.1 → 0.7.0-rc1

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 (319) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +620 -26
  3. package/dist/{commands → src/commands}/config-cli.js +5 -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 +120 -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 +2 -1
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +74 -1
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +44 -0
  16. package/dist/{core → src/core}/asset-ref.js +5 -5
  17. package/dist/{core → src/core}/asset-spec.js +12 -0
  18. package/dist/{core → src/core}/common.js +1 -1
  19. package/dist/{core → src/core}/config.js +175 -121
  20. package/dist/{core → src/core}/errors.js +4 -0
  21. package/dist/src/core/events.js +239 -0
  22. package/dist/src/core/lesson-lint.js +86 -0
  23. package/dist/src/core/proposals.js +406 -0
  24. package/dist/src/core/warn.js +72 -0
  25. package/dist/{core → src/core}/write-source.js +80 -5
  26. package/dist/{indexer → src/indexer}/db-search.js +113 -24
  27. package/dist/{indexer → src/indexer}/db.js +76 -23
  28. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  29. package/dist/src/indexer/graph-boost.js +179 -0
  30. package/dist/src/indexer/graph-extraction.js +212 -0
  31. package/dist/{indexer → src/indexer}/indexer.js +73 -6
  32. package/dist/src/indexer/memory-inference.js +263 -0
  33. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  34. package/dist/src/integrations/agent/config.js +292 -0
  35. package/dist/src/integrations/agent/detect.js +94 -0
  36. package/dist/src/integrations/agent/index.js +17 -0
  37. package/dist/src/integrations/agent/profiles.js +65 -0
  38. package/dist/src/integrations/agent/prompts.js +167 -0
  39. package/dist/src/integrations/agent/spawn.js +221 -0
  40. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  41. package/dist/{llm → src/llm}/client.js +33 -2
  42. package/dist/src/llm/feature-gate.js +108 -0
  43. package/dist/src/llm/graph-extract.js +107 -0
  44. package/dist/src/llm/index-passes.js +35 -0
  45. package/dist/src/llm/memory-infer.js +86 -0
  46. package/dist/{output → src/output}/renderers.js +60 -1
  47. package/dist/src/output/shapes.js +516 -0
  48. package/dist/{output → src/output}/text.js +447 -4
  49. package/dist/{registry → src/registry}/build-index.js +14 -4
  50. package/dist/{registry → src/registry}/factory.js +0 -8
  51. package/dist/{registry → src/registry}/providers/static-index.js +3 -2
  52. package/dist/{registry → src/registry}/resolve.js +68 -2
  53. package/dist/{setup → src/setup}/setup.js +43 -5
  54. package/dist/{sources → src/sources}/providers/git.js +7 -15
  55. package/dist/tests/add-website-source.test.js +119 -0
  56. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  57. package/dist/tests/agent/agent-config.test.js +221 -0
  58. package/dist/tests/agent/agent-detect.test.js +100 -0
  59. package/dist/tests/agent/agent-spawn.test.js +234 -0
  60. package/dist/tests/agent-output.test.js +186 -0
  61. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  62. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  63. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  64. package/dist/tests/asset-ref.test.js +192 -0
  65. package/dist/tests/asset-registry.test.js +103 -0
  66. package/dist/tests/asset-spec.test.js +241 -0
  67. package/dist/tests/bench/attribution.test.js +995 -0
  68. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  69. package/dist/tests/bench/cleanup.js +203 -0
  70. package/dist/tests/bench/cleanup.test.js +166 -0
  71. package/dist/tests/bench/cli.js +683 -0
  72. package/dist/tests/bench/cli.test.js +177 -0
  73. package/dist/tests/bench/compare.test.js +556 -0
  74. package/dist/tests/bench/corpus.js +314 -0
  75. package/dist/tests/bench/corpus.test.js +258 -0
  76. package/dist/tests/bench/driver.js +346 -0
  77. package/dist/tests/bench/driver.test.js +443 -0
  78. package/dist/tests/bench/evolve-metrics.js +179 -0
  79. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  80. package/dist/tests/bench/evolve.js +580 -0
  81. package/dist/tests/bench/evolve.test.js +616 -0
  82. package/dist/tests/bench/failure-modes.test.js +300 -0
  83. package/dist/tests/bench/feedback-integrity.test.js +456 -0
  84. package/dist/tests/bench/leakage.test.js +125 -0
  85. package/dist/tests/bench/learning-curve.test.js +133 -0
  86. package/dist/tests/bench/metrics.js +2319 -0
  87. package/dist/tests/bench/metrics.test.js +1144 -0
  88. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  89. package/dist/tests/bench/report.js +1821 -0
  90. package/dist/tests/bench/report.test.js +989 -0
  91. package/dist/tests/bench/runner.js +536 -0
  92. package/dist/tests/bench/runner.test.js +958 -0
  93. package/dist/tests/bench/search-bridge.test.js +331 -0
  94. package/dist/tests/bench/tmp.js +41 -0
  95. package/dist/tests/bench/trajectory.js +116 -0
  96. package/dist/tests/bench/trajectory.test.js +127 -0
  97. package/dist/tests/bench/verifier.js +109 -0
  98. package/dist/tests/bench/verifier.test.js +118 -0
  99. package/dist/tests/bench/workflow-evaluator.js +557 -0
  100. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  101. package/dist/tests/bench/workflow-spec.js +358 -0
  102. package/dist/tests/bench/workflow-spec.test.js +363 -0
  103. package/dist/tests/bench/workflow-trace.js +438 -0
  104. package/dist/tests/bench/workflow-trace.test.js +254 -0
  105. package/dist/tests/benchmark-search-quality.js +536 -0
  106. package/dist/tests/benchmark-suite.js +1441 -0
  107. package/dist/tests/capture-cli.test.js +112 -0
  108. package/dist/tests/cli-errors.test.js +203 -0
  109. package/dist/tests/commands/events.test.js +370 -0
  110. package/dist/tests/commands/history.test.js +223 -0
  111. package/dist/tests/commands/import.test.js +103 -0
  112. package/dist/tests/commands/proposal-cli.test.js +209 -0
  113. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  114. package/dist/tests/commands/remember.test.js +97 -0
  115. package/dist/tests/commands/scope-flags.test.js +300 -0
  116. package/dist/tests/commands/search.test.js +537 -0
  117. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  118. package/dist/tests/commands/show.test.js +294 -0
  119. package/dist/tests/common.test.js +266 -0
  120. package/dist/tests/completions.test.js +142 -0
  121. package/dist/tests/config-cli.test.js +193 -0
  122. package/dist/tests/config-llm-features.test.js +139 -0
  123. package/dist/tests/config.test.js +544 -0
  124. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  125. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  126. package/dist/tests/contracts/spec-helpers.js +46 -0
  127. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  128. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  129. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  130. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  131. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  132. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  133. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  134. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  135. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  136. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  137. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  138. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  139. package/dist/tests/core/write-source.test.js +366 -0
  140. package/dist/tests/curate-command.test.js +87 -0
  141. package/dist/tests/db-scoring.test.js +201 -0
  142. package/dist/tests/db.test.js +654 -0
  143. package/dist/tests/distill-cli-flag.test.js +208 -0
  144. package/dist/tests/distill.test.js +515 -0
  145. package/dist/tests/docker-install.test.js +120 -0
  146. package/dist/tests/e2e.test.js +1398 -0
  147. package/dist/tests/embedder.test.js +340 -0
  148. package/dist/tests/embedding-model-config.test.js +379 -0
  149. package/dist/tests/feedback-command.test.js +172 -0
  150. package/dist/tests/file-context.test.js +552 -0
  151. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  152. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  153. package/dist/tests/fixtures/stashes/load.js +166 -0
  154. package/dist/tests/fixtures/stashes/load.test.js +88 -0
  155. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  156. package/dist/tests/frontmatter.test.js +190 -0
  157. package/dist/tests/fts-field-weighting.test.js +254 -0
  158. package/dist/tests/fuzzy-search.test.js +230 -0
  159. package/dist/tests/git-provider-clone.test.js +45 -0
  160. package/dist/tests/github.test.js +161 -0
  161. package/dist/tests/graph-boost-ranking.test.js +305 -0
  162. package/dist/tests/graph-extraction.test.js +282 -0
  163. package/dist/tests/helpers/usage-events.js +8 -0
  164. package/dist/tests/index-pass-llm.test.js +161 -0
  165. package/dist/tests/indexer.test.js +559 -0
  166. package/dist/tests/info-command.test.js +166 -0
  167. package/dist/tests/init.test.js +69 -0
  168. package/dist/tests/install-script.test.js +246 -0
  169. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  170. package/dist/tests/issue-36-repro.test.js +304 -0
  171. package/dist/tests/issues-191-194.test.js +160 -0
  172. package/dist/tests/lesson-lint.test.js +111 -0
  173. package/dist/tests/llm-client.test.js +115 -0
  174. package/dist/tests/llm-feature-gate.test.js +151 -0
  175. package/dist/tests/llm.test.js +139 -0
  176. package/dist/tests/lockfile.test.js +216 -0
  177. package/dist/tests/manifest.test.js +205 -0
  178. package/dist/tests/markdown.test.js +126 -0
  179. package/dist/tests/matchers-unit.test.js +189 -0
  180. package/dist/tests/memory-inference.test.js +299 -0
  181. package/dist/tests/merge-scoring.test.js +136 -0
  182. package/dist/tests/metadata.test.js +313 -0
  183. package/dist/tests/migration-help.test.js +89 -0
  184. package/dist/tests/origin-resolve.test.js +124 -0
  185. package/dist/tests/output-baseline.test.js +217 -0
  186. package/dist/tests/output-shapes-unit.test.js +476 -0
  187. package/dist/tests/parallel-search.test.js +272 -0
  188. package/dist/tests/parameter-metadata.test.js +365 -0
  189. package/dist/tests/paths.test.js +177 -0
  190. package/dist/tests/progressive-disclosure.test.js +280 -0
  191. package/dist/tests/proposals.test.js +279 -0
  192. package/dist/tests/proposed-quality.test.js +271 -0
  193. package/dist/tests/provider-registry.test.js +32 -0
  194. package/dist/tests/ranking-regression.test.js +548 -0
  195. package/dist/tests/reflect-propose.test.js +455 -0
  196. package/dist/tests/registry-build-index.test.js +378 -0
  197. package/dist/tests/registry-cli.test.js +290 -0
  198. package/dist/tests/registry-index-v2.test.js +430 -0
  199. package/dist/tests/registry-install.test.js +728 -0
  200. package/dist/tests/registry-providers/parity.test.js +189 -0
  201. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  202. package/dist/tests/registry-providers/static-index.test.js +204 -0
  203. package/dist/tests/registry-resolve.test.js +126 -0
  204. package/dist/tests/registry-search.test.js +723 -0
  205. package/dist/tests/remember-frontmatter.test.js +380 -0
  206. package/dist/tests/remember-unit.test.js +123 -0
  207. package/dist/tests/ripgrep-install.test.js +251 -0
  208. package/dist/tests/ripgrep-resolve.test.js +108 -0
  209. package/dist/tests/ripgrep.test.js +163 -0
  210. package/dist/tests/save-command.test.js +94 -0
  211. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  212. package/dist/tests/scoring-pipeline.test.js +648 -0
  213. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  214. package/dist/tests/self-update.test.js +442 -0
  215. package/dist/tests/semantic-search-e2e.test.js +512 -0
  216. package/dist/tests/semantic-status.test.js +471 -0
  217. package/dist/tests/setup-run.integration.js +877 -0
  218. package/dist/tests/setup-wizard.test.js +198 -0
  219. package/dist/tests/setup.test.js +131 -0
  220. package/dist/tests/source-add.test.js +11 -0
  221. package/dist/tests/source-clone.test.js +254 -0
  222. package/dist/tests/source-manage.test.js +366 -0
  223. package/dist/tests/source-providers/filesystem.test.js +82 -0
  224. package/dist/tests/source-providers/git.test.js +252 -0
  225. package/dist/tests/source-providers/website.test.js +128 -0
  226. package/dist/tests/source-qa-fixes.test.js +268 -0
  227. package/dist/tests/source-registry.test.js +350 -0
  228. package/dist/tests/source-resolve.test.js +100 -0
  229. package/dist/tests/source-source.test.js +221 -0
  230. package/dist/tests/source.test.js +533 -0
  231. package/dist/tests/tar-utils-scan.test.js +73 -0
  232. package/dist/tests/toggle-components.test.js +73 -0
  233. package/dist/tests/usage-telemetry.test.js +265 -0
  234. package/dist/tests/utility-scoring.test.js +558 -0
  235. package/dist/tests/vault-load-error.test.js +78 -0
  236. package/dist/tests/vault-qa-fixes.test.js +194 -0
  237. package/dist/tests/vault.test.js +429 -0
  238. package/dist/tests/vector-search.test.js +608 -0
  239. package/dist/tests/walker.test.js +252 -0
  240. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  241. package/dist/tests/wave2-cluster-d.test.js +180 -0
  242. package/dist/tests/wave2-cluster-e.test.js +179 -0
  243. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  244. package/dist/tests/wiki.test.js +529 -0
  245. package/dist/tests/workflow-cli.test.js +271 -0
  246. package/dist/tests/workflow-markdown.test.js +171 -0
  247. package/dist/tests/workflow-path-escape.test.js +132 -0
  248. package/dist/tests/workflow-qa-fixes.test.js +377 -0
  249. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  250. package/docs/README.md +8 -0
  251. package/docs/migration/release-notes/0.7.0.md +244 -0
  252. package/package.json +2 -2
  253. package/dist/core/warn.js +0 -27
  254. package/dist/output/shapes.js +0 -212
  255. /package/dist/{commands → src/commands}/completions.js +0 -0
  256. /package/dist/{commands → src/commands}/curate.js +0 -0
  257. /package/dist/{commands → src/commands}/info.js +0 -0
  258. /package/dist/{commands → src/commands}/init.js +0 -0
  259. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  260. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  261. /package/dist/{commands → src/commands}/source-add.js +0 -0
  262. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  263. /package/dist/{commands → src/commands}/source-manage.js +0 -0
  264. /package/dist/{commands → src/commands}/vault.js +0 -0
  265. /package/dist/{core → src/core}/asset-registry.js +0 -0
  266. /package/dist/{core → src/core}/frontmatter.js +0 -0
  267. /package/dist/{core → src/core}/markdown.js +0 -0
  268. /package/dist/{core → src/core}/paths.js +0 -0
  269. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  270. /package/dist/{indexer → src/indexer}/matchers.js +0 -0
  271. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  272. /package/dist/{indexer → src/indexer}/search-source.js +0 -0
  273. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  274. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  275. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  276. /package/dist/{integrations → src/integrations}/github.js +0 -0
  277. /package/dist/{llm → src/llm}/embedder.js +0 -0
  278. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  279. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  280. /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
  281. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  282. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  283. /package/dist/{output → src/output}/cli-hints.js +0 -0
  284. /package/dist/{output → src/output}/context.js +0 -0
  285. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  286. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  287. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  288. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  289. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  290. /package/dist/{registry → src/registry}/types.js +0 -0
  291. /package/dist/{setup → src/setup}/detect.js +0 -0
  292. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  293. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  294. /package/dist/{setup → src/setup}/steps.js +0 -0
  295. /package/dist/{sources → src/sources}/include.js +0 -0
  296. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  297. /package/dist/{sources → src/sources}/provider.js +0 -0
  298. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  299. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  300. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  301. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  302. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  303. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  304. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  305. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  306. /package/dist/{sources → src/sources}/resolve.js +0 -0
  307. /package/dist/{sources → src/sources}/types.js +0 -0
  308. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  309. /package/dist/{version.js → src/version.js} +0 -0
  310. /package/dist/{wiki → src/wiki}/wiki.js +0 -0
  311. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  312. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  313. /package/dist/{workflows → src/workflows}/db.js +0 -0
  314. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  315. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  316. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  317. /package/dist/{workflows → src/workflows}/runs.js +0 -0
  318. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  319. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * Ranking regression tests for akm search system.
3
+ *
4
+ * Uses the shared `ranking-baseline` fixture under
5
+ * tests/fixtures/stashes/ranking-baseline/ to validate search ranking
6
+ * invariants: score differentiation, exact name matching, type ranking,
7
+ * fuzzy/prefix matching, score preservation, and provider merge behavior.
8
+ *
9
+ * The fixture stash is materialised once in beforeAll via the shared
10
+ * `loadFixtureStash` helper, then re-indexed in place through the internal
11
+ * indexer DB API so all tests share the same index.
12
+ */
13
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { akmSearch } from "../src/commands/search";
18
+ import { saveConfig } from "../src/core/config";
19
+ import { getDbPath } from "../src/core/paths";
20
+ import { closeDatabase, openDatabase, rebuildFts, setMeta, upsertEntry } from "../src/indexer/db";
21
+ import { buildSearchText } from "../src/indexer/search-fields";
22
+ import { loadFixtureStash } from "./fixtures/stashes/load";
23
+ // Local test helper — mirrors the pre-v1 mergeStashHits logic that was removed
24
+ // from production code when the OpenViking provider was dropped (Phase 1).
25
+ function mergeStashHits(localHits, additionalHits, limit) {
26
+ if (additionalHits.length === 0)
27
+ return localHits.slice(0, limit);
28
+ const localKeys = new Set();
29
+ for (const h of localHits)
30
+ localKeys.add(h.path ?? h.ref ?? h.name);
31
+ const providerOnly = additionalHits.filter((h) => !localKeys.has(h.path ?? h.ref ?? h.name));
32
+ return [...localHits, ...providerOnly].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
33
+ }
34
+ // ── Fixture path ────────────────────────────────────────────────────────────
35
+ let FIXTURE_STASH;
36
+ let fixtureCleanup;
37
+ // ── Temp directory tracking ─────────────────────────────────────────────────
38
+ const createdTmpDirs = [];
39
+ function createTmpDir(prefix = "akm-ranking-") {
40
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
41
+ createdTmpDirs.push(dir);
42
+ return dir;
43
+ }
44
+ // ── Environment isolation ───────────────────────────────────────────────────
45
+ let originalXdgCacheHome;
46
+ let originalXdgConfigHome;
47
+ let originalAkmStashDir;
48
+ let testCacheDir;
49
+ let testConfigDir;
50
+ beforeAll(async () => {
51
+ originalXdgCacheHome = process.env.XDG_CACHE_HOME;
52
+ originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
53
+ originalAkmStashDir = process.env.AKM_STASH_DIR;
54
+ testCacheDir = createTmpDir("akm-ranking-cache-");
55
+ testConfigDir = createTmpDir("akm-ranking-config-");
56
+ // Materialise the shared ranking-baseline fixture into a tmp dir. This
57
+ // test rebuilds the index from scratch via `buildFixtureIndex()` below
58
+ // against its own XDG_CACHE_HOME, so we skip the helper's `akm index`
59
+ // spawn (~200-300ms saved per run).
60
+ const loaded = loadFixtureStash("ranking-baseline", { skipIndex: true });
61
+ FIXTURE_STASH = loaded.stashDir;
62
+ fixtureCleanup = loaded.cleanup;
63
+ process.env.XDG_CACHE_HOME = testCacheDir;
64
+ process.env.XDG_CONFIG_HOME = testConfigDir;
65
+ process.env.AKM_STASH_DIR = FIXTURE_STASH;
66
+ saveConfig({
67
+ semanticSearchMode: "off",
68
+ sources: [{ type: "filesystem", path: FIXTURE_STASH }],
69
+ registries: [],
70
+ });
71
+ buildFixtureIndex();
72
+ });
73
+ afterAll(() => {
74
+ if (originalXdgCacheHome === undefined)
75
+ delete process.env.XDG_CACHE_HOME;
76
+ else
77
+ process.env.XDG_CACHE_HOME = originalXdgCacheHome;
78
+ if (originalXdgConfigHome === undefined)
79
+ delete process.env.XDG_CONFIG_HOME;
80
+ else
81
+ process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
82
+ if (originalAkmStashDir === undefined)
83
+ delete process.env.AKM_STASH_DIR;
84
+ else
85
+ process.env.AKM_STASH_DIR = originalAkmStashDir;
86
+ fixtureCleanup?.();
87
+ for (const dir of createdTmpDirs) {
88
+ fs.rmSync(dir, { recursive: true, force: true });
89
+ }
90
+ });
91
+ // ── Index builder ───────────────────────────────────────────────────────────
92
+ /**
93
+ * Walk the fixture stash, read .stash.json files, and index all entries
94
+ * directly into the SQLite database.
95
+ */
96
+ function buildFixtureIndex() {
97
+ const dbPath = getDbPath();
98
+ const db = openDatabase(dbPath);
99
+ try {
100
+ const stashJsonPaths = findStashJsonFiles(FIXTURE_STASH);
101
+ for (const stashJsonPath of stashJsonPaths) {
102
+ const dirPath = path.dirname(stashJsonPath);
103
+ const raw = JSON.parse(fs.readFileSync(stashJsonPath, "utf8"));
104
+ if (!raw || !Array.isArray(raw.entries))
105
+ continue;
106
+ const stash = { entries: raw.entries };
107
+ for (const entry of stash.entries) {
108
+ const entryPath = entry.filename ? path.join(dirPath, entry.filename) : dirPath;
109
+ const entryKey = `${FIXTURE_STASH}:${entry.type}:${entry.name}`;
110
+ const searchText = buildSearchText(entry);
111
+ let entryWithSize = entry;
112
+ try {
113
+ const size = fs.statSync(entryPath).size;
114
+ entryWithSize = { ...entry, fileSize: size };
115
+ }
116
+ catch {
117
+ // File might not exist for some entries
118
+ }
119
+ upsertEntry(db, entryKey, dirPath, entryPath, FIXTURE_STASH, entryWithSize, searchText);
120
+ }
121
+ }
122
+ rebuildFts(db);
123
+ setMeta(db, "stashDir", FIXTURE_STASH);
124
+ setMeta(db, "builtAt", new Date().toISOString());
125
+ setMeta(db, "stashDirs", JSON.stringify([FIXTURE_STASH]));
126
+ setMeta(db, "hasEmbeddings", "0");
127
+ }
128
+ finally {
129
+ closeDatabase(db);
130
+ }
131
+ }
132
+ function findStashJsonFiles(dir) {
133
+ const results = [];
134
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ const fullPath = path.join(dir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ results.push(...findStashJsonFiles(fullPath));
139
+ }
140
+ else if (entry.name === ".stash.json") {
141
+ results.push(fullPath);
142
+ }
143
+ }
144
+ return results;
145
+ }
146
+ // ── Helpers ─────────────────────────────────────────────────────────────────
147
+ async function search(query, limit = 20) {
148
+ const result = await akmSearch({ query, source: "stash", limit });
149
+ return result.hits.filter((h) => h.type !== "registry");
150
+ }
151
+ function findHit(hits, name) {
152
+ return hits.find((h) => h.name === name);
153
+ }
154
+ /** Assert that a hit exists and return it (avoids non-null assertions). */
155
+ function expectHit(hits, name) {
156
+ const hit = findHit(hits, name);
157
+ expect(hit).toBeDefined();
158
+ // biome-ignore lint/style/noNonNullAssertion: guarded by expect above
159
+ return hit;
160
+ }
161
+ /** Get the score of a hit, asserting it is defined. */
162
+ function scoreOf(hit) {
163
+ expect(hit.score).toBeDefined();
164
+ return hit.score ?? 0;
165
+ }
166
+ function rankOf(hits, name) {
167
+ const idx = hits.findIndex((h) => h.name === name);
168
+ return idx === -1 ? Infinity : idx + 1; // 1-based rank
169
+ }
170
+ // ── Tests ───────────────────────────────────────────────────────────────────
171
+ describe("Score differentiation", () => {
172
+ test('"docker homelab" returns skill:docker-homelab in top 3', async () => {
173
+ const hits = await search("docker homelab");
174
+ expect(hits.length).toBeGreaterThanOrEqual(2);
175
+ // docker-homelab should appear in the top results (within top 3)
176
+ // Sub-references also contain "docker-homelab" in their name, so they
177
+ // may rank highly on FTS name-field matching.
178
+ const skillRank = rankOf(hits, "docker-homelab");
179
+ expect(skillRank).toBeLessThanOrEqual(3);
180
+ // The skill should have a meaningful score (not RRF-compressed)
181
+ const skillHit = expectHit(hits, "docker-homelab");
182
+ expect(scoreOf(skillHit)).toBeGreaterThan(0.5);
183
+ });
184
+ test('"docker" returns docker-homelab and docker-clean', async () => {
185
+ const hits = await search("docker");
186
+ expect(hits.length).toBeGreaterThanOrEqual(2);
187
+ // Both docker-related assets should appear in results
188
+ expectHit(hits, "docker-homelab");
189
+ expectHit(hits, "docker-clean");
190
+ });
191
+ test('"svelte component" -> skill:svelte-components ranks #1, above sub-references', async () => {
192
+ const hits = await search("svelte component");
193
+ expect(hits.length).toBeGreaterThanOrEqual(1);
194
+ const skillRank = rankOf(hits, "svelte-components");
195
+ expect(skillRank).toBe(1);
196
+ // Sub-reference should rank below the skill
197
+ const refHit = findHit(hits, "svelte-components/references/web-components");
198
+ if (refHit) {
199
+ const refRank = rankOf(hits, "svelte-components/references/web-components");
200
+ expect(refRank).toBeGreaterThan(skillRank);
201
+ }
202
+ });
203
+ test('"code review" -> command or agent ranks above knowledge docs', async () => {
204
+ const hits = await search("code review");
205
+ expect(hits.length).toBeGreaterThanOrEqual(2);
206
+ // Find the top-ranked actionable asset (skill, command, or agent)
207
+ const actionableTypes = new Set(["skill", "command", "agent"]);
208
+ const topActionable = hits.find((h) => actionableTypes.has(h.type));
209
+ expect(topActionable).toBeDefined();
210
+ // Find the top-ranked knowledge doc
211
+ const topKnowledge = hits.find((h) => h.type === "knowledge");
212
+ if (topKnowledge && topActionable) {
213
+ const actionableRank = rankOf(hits, topActionable.name);
214
+ const knowledgeRank = rankOf(hits, topKnowledge.name);
215
+ expect(actionableRank).toBeLessThan(knowledgeRank);
216
+ }
217
+ });
218
+ test('"mem0 search" -> script:mem0-search ranks #1', async () => {
219
+ const hits = await search("mem0 search");
220
+ expect(hits.length).toBeGreaterThanOrEqual(1);
221
+ expect(rankOf(hits, "mem0-search")).toBe(1);
222
+ });
223
+ });
224
+ describe("Exact/near-exact name matching", () => {
225
+ test('"docker-homelab" (exact) -> skill:docker-homelab appears in top 3', async () => {
226
+ const hits = await search("docker-homelab");
227
+ expect(hits.length).toBeGreaterThanOrEqual(2);
228
+ // The skill entry and its sub-references all contain "docker-homelab"
229
+ // in their names. The skill gets a name-match boost but sub-references
230
+ // also match on FTS name field. Verify the skill is in the top 3.
231
+ const skillRank = rankOf(hits, "docker-homelab");
232
+ expect(skillRank).toBeLessThanOrEqual(3);
233
+ // The skill should have a strong score
234
+ const skillHit = expectHit(hits, "docker-homelab");
235
+ expect(scoreOf(skillHit)).toBeGreaterThan(0.5);
236
+ });
237
+ test('"mem0-search" (exact) -> script:mem0-search is #1', async () => {
238
+ const hits = await search("mem0-search");
239
+ expect(hits.length).toBeGreaterThanOrEqual(1);
240
+ expect(hits[0].name).toBe("mem0-search");
241
+ });
242
+ test('"security-review" (exact) -> command:security-review is #1', async () => {
243
+ const hits = await search("security-review");
244
+ expect(hits.length).toBeGreaterThanOrEqual(1);
245
+ expect(hits[0].name).toBe("security-review");
246
+ expect(hits[0].type).toBe("command");
247
+ });
248
+ test('"k8s-deploy" (exact) -> skill:k8s-deploy is #1', async () => {
249
+ const hits = await search("k8s-deploy");
250
+ expect(hits.length).toBeGreaterThanOrEqual(1);
251
+ expect(hits[0].name).toBe("k8s-deploy");
252
+ expect(hits[0].type).toBe("skill");
253
+ });
254
+ test('"code-reviewer" (exact) -> agent:code-reviewer is #1', async () => {
255
+ const hits = await search("code-reviewer");
256
+ expect(hits.length).toBeGreaterThanOrEqual(1);
257
+ expect(hits[0].name).toBe("code-reviewer");
258
+ expect(hits[0].type).toBe("agent");
259
+ });
260
+ });
261
+ describe("Type ranking", () => {
262
+ test('for "deploy", skills/commands/scripts rank above knowledge docs', async () => {
263
+ const hits = await search("deploy");
264
+ expect(hits.length).toBeGreaterThanOrEqual(2);
265
+ const actionableTypes = new Set(["skill", "command", "agent", "script"]);
266
+ const topActionable = hits.find((h) => actionableTypes.has(h.type));
267
+ const topKnowledge = hits.find((h) => h.type === "knowledge");
268
+ expect(topActionable).toBeDefined();
269
+ if (topKnowledge && topActionable) {
270
+ expect(rankOf(hits, topActionable.name)).toBeLessThan(rankOf(hits, topKnowledge.name));
271
+ }
272
+ });
273
+ test('for "review", agents/commands/skills rank above knowledge docs', async () => {
274
+ const hits = await search("review");
275
+ expect(hits.length).toBeGreaterThanOrEqual(2);
276
+ const actionableTypes = new Set(["skill", "command", "agent"]);
277
+ const topActionable = hits.find((h) => actionableTypes.has(h.type));
278
+ const topKnowledge = hits.find((h) => h.type === "knowledge");
279
+ expect(topActionable).toBeDefined();
280
+ if (topKnowledge && topActionable) {
281
+ expect(rankOf(hits, topActionable.name)).toBeLessThan(rankOf(hits, topKnowledge.name));
282
+ }
283
+ });
284
+ });
285
+ describe("Fuzzy/prefix matching", () => {
286
+ test('"kube" finds k8s-deploy via alias', async () => {
287
+ const hits = await search("kube");
288
+ expectHit(hits, "k8s-deploy");
289
+ });
290
+ test('"dock" finds docker-homelab via prefix', async () => {
291
+ const hits = await search("dock");
292
+ expectHit(hits, "docker-homelab");
293
+ });
294
+ test('"incident" finds the runbook', async () => {
295
+ const hits = await search("incident");
296
+ expectHit(hits, "incident-response-runbook");
297
+ });
298
+ });
299
+ describe("Score preservation (not RRF-flattened)", () => {
300
+ test("top result score > 0.5 (not capped at 0.0164)", async () => {
301
+ const hits = await search("docker homelab");
302
+ expect(hits.length).toBeGreaterThanOrEqual(1);
303
+ expect(scoreOf(hits[0])).toBeGreaterThan(0.5);
304
+ });
305
+ test("top result for exact name query has strong differentiation", async () => {
306
+ // Use a query that uniquely targets one asset.
307
+ // Per the locked v1 contract (CLAUDE.md / spec §9), SearchHit.score is
308
+ // bounded in [0,1]. An exact-name match accumulates large additive
309
+ // boosts that are clamped at the final emit step, so the top score for
310
+ // a uniquely-matching exact-name query must reach the ceiling (1.0).
311
+ const hits = await search("mem0 search");
312
+ expect(hits.length).toBeGreaterThanOrEqual(1);
313
+ const topScore = scoreOf(hits[0]);
314
+ expect(topScore).toBe(1);
315
+ // If there are additional results, the top should be at least as high.
316
+ // Below-ceiling differentiation is asserted by the broader
317
+ // "scores are monotonically decreasing" case below; here we just
318
+ // confirm the top hit reaches the maximum.
319
+ if (hits.length >= 2) {
320
+ expect(topScore).toBeGreaterThanOrEqual(scoreOf(hits[1]));
321
+ }
322
+ });
323
+ test("scores are monotonically decreasing", async () => {
324
+ const hits = await search("docker");
325
+ for (let i = 1; i < hits.length; i++) {
326
+ const prev = hits[i - 1].score ?? 0;
327
+ const curr = hits[i].score ?? 0;
328
+ expect(prev).toBeGreaterThanOrEqual(curr);
329
+ }
330
+ });
331
+ test("scores are not compressed to a narrow range", async () => {
332
+ const hits = await search("docker");
333
+ expect(hits.length).toBeGreaterThanOrEqual(3);
334
+ const topScore = scoreOf(hits[0]);
335
+ const lastScore = scoreOf(hits[hits.length - 1]);
336
+ const range = topScore - lastScore;
337
+ // Score range should be meaningful, not compressed to ~0.001 like RRF.
338
+ // Per the locked v1 contract (CLAUDE.md / spec §9), scores are bounded
339
+ // in [0,1] — multiple top hits on a popular query may all clamp to 1.0,
340
+ // which collapses the visible top-end differential. The bottom of the
341
+ // range still shows clear separation from the top, well above what RRF
342
+ // would compress to (~0.001).
343
+ expect(range).toBeGreaterThan(0.01);
344
+ });
345
+ });
346
+ describe("Provider merge (score not destroyed)", () => {
347
+ test("when additional provider hits exist, local scores are preserved", () => {
348
+ const localHits = [
349
+ {
350
+ type: "skill",
351
+ name: "local-skill-1",
352
+ path: "/test/skills/local-1/SKILL.md",
353
+ ref: "skill:local-skill-1",
354
+ origin: null,
355
+ score: 2.5,
356
+ },
357
+ {
358
+ type: "command",
359
+ name: "local-cmd-1",
360
+ path: "/test/commands/local-1.md",
361
+ ref: "command:local-cmd-1",
362
+ origin: null,
363
+ score: 1.8,
364
+ },
365
+ {
366
+ type: "knowledge",
367
+ name: "local-doc-1",
368
+ path: "/test/knowledge/local-1.md",
369
+ ref: "knowledge:local-doc-1",
370
+ origin: null,
371
+ score: 0.9,
372
+ },
373
+ ];
374
+ const additionalHits = [
375
+ {
376
+ type: "skill",
377
+ name: "remote-skill-1",
378
+ path: "/remote/skills/remote-1/SKILL.md",
379
+ ref: "skill:remote-skill-1",
380
+ origin: "remote",
381
+ score: 0.85,
382
+ },
383
+ ];
384
+ const merged = mergeStashHits(localHits, additionalHits, 20);
385
+ // Local hits should retain their original scores
386
+ const mergedLocal1 = expectHit(merged, "local-skill-1");
387
+ expect(mergedLocal1.score).toBe(2.5);
388
+ const mergedLocal2 = expectHit(merged, "local-cmd-1");
389
+ expect(mergedLocal2.score).toBe(1.8);
390
+ });
391
+ test("provider hits sort fairly by score alongside local hits", () => {
392
+ const localHits = [
393
+ {
394
+ type: "skill",
395
+ name: "local-high",
396
+ path: "/test/skills/high/SKILL.md",
397
+ ref: "skill:local-high",
398
+ origin: null,
399
+ score: 2.0,
400
+ },
401
+ {
402
+ type: "skill",
403
+ name: "local-low",
404
+ path: "/test/skills/low/SKILL.md",
405
+ ref: "skill:local-low",
406
+ origin: null,
407
+ score: 0.5,
408
+ },
409
+ ];
410
+ const additionalHits = [
411
+ {
412
+ type: "skill",
413
+ name: "remote-1",
414
+ path: "/remote/skills/1/SKILL.md",
415
+ ref: "skill:remote-1",
416
+ origin: "remote",
417
+ score: 1.0, // Normalized provider score between local-high and local-low
418
+ },
419
+ ];
420
+ const merged = mergeStashHits(localHits, additionalHits, 20);
421
+ // Provider hit keeps its original score and sorts by score
422
+ const remoteRank = merged.findIndex((h) => h.name === "remote-1") + 1;
423
+ const localHighRank = merged.findIndex((h) => h.name === "local-high") + 1;
424
+ const localLowRank = merged.findIndex((h) => h.name === "local-low") + 1;
425
+ // remote-1 (1.0) should rank between local-high (2.0) and local-low (0.5)
426
+ expect(remoteRank).toBeGreaterThan(localHighRank);
427
+ expect(remoteRank).toBeLessThan(localLowRank);
428
+ });
429
+ test("duplicate provider hits are deduplicated (local version wins)", () => {
430
+ const localHits = [
431
+ {
432
+ type: "skill",
433
+ name: "shared-skill",
434
+ path: "/test/skills/shared/SKILL.md",
435
+ ref: "skill:shared-skill",
436
+ origin: null,
437
+ score: 2.0,
438
+ },
439
+ ];
440
+ const additionalHits = [
441
+ {
442
+ type: "skill",
443
+ name: "shared-skill",
444
+ path: "/test/skills/shared/SKILL.md", // Same path = duplicate
445
+ ref: "skill:shared-skill",
446
+ origin: "remote",
447
+ score: 0.5,
448
+ },
449
+ ];
450
+ const merged = mergeStashHits(localHits, additionalHits, 20);
451
+ // Only one instance of the shared skill should appear
452
+ const sharedHits = merged.filter((h) => h.name === "shared-skill");
453
+ expect(sharedHits.length).toBe(1);
454
+ // And it should have the local score
455
+ expect(sharedHits[0].score).toBe(2.0);
456
+ });
457
+ test("merge preserves sort order by score descending", () => {
458
+ const localHits = [
459
+ { type: "skill", name: "a", path: "/a", ref: "skill:a", origin: null, score: 3.0 },
460
+ { type: "skill", name: "b", path: "/b", ref: "skill:b", origin: null, score: 1.0 },
461
+ ];
462
+ const additionalHits = [
463
+ { type: "skill", name: "c", path: "/c", ref: "skill:c", origin: "remote", score: 2.0 },
464
+ ];
465
+ const merged = mergeStashHits(localHits, additionalHits, 20);
466
+ for (let i = 1; i < merged.length; i++) {
467
+ const prev = merged[i - 1].score ?? 0;
468
+ const curr = merged[i].score ?? 0;
469
+ expect(prev).toBeGreaterThanOrEqual(curr);
470
+ }
471
+ });
472
+ });
473
+ describe("Cross-type search consistency", () => {
474
+ test("searching for 'docker' returns docker-homelab and docker-clean", async () => {
475
+ const hits = await search("docker");
476
+ expect(hits.length).toBeGreaterThanOrEqual(2);
477
+ const dockerNames = hits.map((h) => h.name);
478
+ expect(dockerNames).toContain("docker-homelab");
479
+ expect(dockerNames).toContain("docker-clean");
480
+ });
481
+ test("multi-word queries narrow results appropriately", async () => {
482
+ const narrowHits = await search("deploy check");
483
+ // The narrow query should return deploy-check at a high rank
484
+ const deployCheckRank = rankOf(narrowHits, "deploy-check");
485
+ expect(deployCheckRank).toBeLessThanOrEqual(3);
486
+ });
487
+ test("searching for svelte returns both skill and agent", async () => {
488
+ const hits = await search("svelte");
489
+ const svelteNames = hits.map((h) => h.name);
490
+ expect(svelteNames).toContain("svelte-components");
491
+ expect(svelteNames).toContain("svelte-expert");
492
+ });
493
+ test("searching for 'release' finds the release-manager command", async () => {
494
+ const hits = await search("release");
495
+ const releaseHit = expectHit(hits, "release-manager");
496
+ expect(releaseHit.type).toBe("command");
497
+ });
498
+ });
499
+ describe("Metadata signal strength", () => {
500
+ test("skill with rich metadata appears in results for broad queries", async () => {
501
+ // docker-homelab has rich tags, aliases, searchHints, and curated quality
502
+ const hits = await search("container management");
503
+ const skillHit = expectHit(hits, "docker-homelab");
504
+ expect(scoreOf(skillHit)).toBeGreaterThan(0);
505
+ });
506
+ test("curated quality assets include the curated metadata boost reason", async () => {
507
+ const hits = await search("kubernetes deploy");
508
+ expect(hits.length).toBeGreaterThanOrEqual(1);
509
+ const k8sHit = expectHit(hits, "k8s-deploy");
510
+ expect(k8sHit.whyMatched).toContain("curated metadata boost");
511
+ });
512
+ test("searchHints contribute to matching", async () => {
513
+ // "troubleshoot docker" is a search hint on docker-homelab
514
+ const hits = await search("troubleshoot docker");
515
+ const skillHit = expectHit(hits, "docker-homelab");
516
+ expect(skillHit.whyMatched).toContain("matched searchHints");
517
+ });
518
+ test("aliases contribute to matching", async () => {
519
+ // "docker-compose" is an alias for docker-homelab
520
+ const hits = await search("docker compose");
521
+ const skillHit = expectHit(hits, "docker-homelab");
522
+ expect(skillHit.whyMatched).toContain("matched aliases");
523
+ });
524
+ test("tags contribute to matching", async () => {
525
+ const hits = await search("homelab");
526
+ const skillHit = expectHit(hits, "docker-homelab");
527
+ expect(skillHit.whyMatched).toContain("matched tags");
528
+ });
529
+ });
530
+ describe("Empty and edge case queries", () => {
531
+ test("empty query returns all entries", async () => {
532
+ const result = await akmSearch({ query: "", source: "stash" });
533
+ expect(result.hits.length).toBeGreaterThan(0);
534
+ });
535
+ test("query with no matches returns empty results with tip", async () => {
536
+ const result = await akmSearch({ query: "xyznonexistent123", source: "stash" });
537
+ const hits = result.hits.filter((h) => h.type !== "registry");
538
+ expect(hits.length).toBe(0);
539
+ });
540
+ test("single character query returns results when prefix matches", async () => {
541
+ // Single char queries are too short for prefix expansion (< 3 chars)
542
+ // but may still match on exact tokens
543
+ const result = await akmSearch({ query: "k", source: "stash" });
544
+ // This may or may not return results depending on FTS tokenizer behavior
545
+ // The important thing is it does not crash
546
+ expect(result).toBeDefined();
547
+ });
548
+ });