@booklib/core 2.0.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 (374) hide show
  1. package/.cursor/rules/booklib-standards.mdc +40 -0
  2. package/.gemini/context.md +372 -0
  3. package/AGENTS.md +166 -0
  4. package/CHANGELOG.md +226 -0
  5. package/CLAUDE.md +81 -0
  6. package/CODE_OF_CONDUCT.md +31 -0
  7. package/CONTRIBUTING.md +304 -0
  8. package/LICENSE +21 -0
  9. package/PLAN.md +28 -0
  10. package/README.ja.md +198 -0
  11. package/README.ko.md +198 -0
  12. package/README.md +503 -0
  13. package/README.pt-BR.md +198 -0
  14. package/README.uk.md +241 -0
  15. package/README.zh-CN.md +198 -0
  16. package/SECURITY.md +9 -0
  17. package/agents/architecture-reviewer.md +136 -0
  18. package/agents/booklib-reviewer.md +90 -0
  19. package/agents/data-reviewer.md +107 -0
  20. package/agents/jvm-reviewer.md +146 -0
  21. package/agents/python-reviewer.md +128 -0
  22. package/agents/rust-reviewer.md +115 -0
  23. package/agents/ts-reviewer.md +110 -0
  24. package/agents/ui-reviewer.md +117 -0
  25. package/assets/logo.svg +36 -0
  26. package/bin/booklib-mcp.js +304 -0
  27. package/bin/booklib.js +1705 -0
  28. package/bin/skills.cjs +1292 -0
  29. package/booklib-router.mdc +36 -0
  30. package/booklib.config.json +19 -0
  31. package/commands/animation-at-work.md +10 -0
  32. package/commands/clean-code-reviewer.md +10 -0
  33. package/commands/data-intensive-patterns.md +10 -0
  34. package/commands/data-pipelines.md +10 -0
  35. package/commands/design-patterns.md +10 -0
  36. package/commands/domain-driven-design.md +10 -0
  37. package/commands/effective-java.md +10 -0
  38. package/commands/effective-kotlin.md +10 -0
  39. package/commands/effective-python.md +10 -0
  40. package/commands/effective-typescript.md +10 -0
  41. package/commands/kotlin-in-action.md +10 -0
  42. package/commands/lean-startup.md +10 -0
  43. package/commands/microservices-patterns.md +10 -0
  44. package/commands/programming-with-rust.md +10 -0
  45. package/commands/refactoring-ui.md +10 -0
  46. package/commands/rust-in-action.md +10 -0
  47. package/commands/skill-router.md +10 -0
  48. package/commands/spring-boot-in-action.md +10 -0
  49. package/commands/storytelling-with-data.md +10 -0
  50. package/commands/system-design-interview.md +10 -0
  51. package/commands/using-asyncio-python.md +10 -0
  52. package/commands/web-scraping-python.md +10 -0
  53. package/community/registry.json +1616 -0
  54. package/hooks/hooks.json +23 -0
  55. package/hooks/posttooluse-capture.mjs +67 -0
  56. package/hooks/suggest.js +153 -0
  57. package/lib/agent-behaviors.js +40 -0
  58. package/lib/agent-detector.js +96 -0
  59. package/lib/config-loader.js +39 -0
  60. package/lib/conflict-resolver.js +148 -0
  61. package/lib/context-builder.js +574 -0
  62. package/lib/discovery-engine.js +298 -0
  63. package/lib/doctor/hook-installer.js +83 -0
  64. package/lib/doctor/usage-tracker.js +87 -0
  65. package/lib/engine/ai-features.js +253 -0
  66. package/lib/engine/auditor.js +103 -0
  67. package/lib/engine/bm25-index.js +178 -0
  68. package/lib/engine/capture.js +120 -0
  69. package/lib/engine/corrections.js +198 -0
  70. package/lib/engine/doctor.js +195 -0
  71. package/lib/engine/graph-injector.js +137 -0
  72. package/lib/engine/graph.js +161 -0
  73. package/lib/engine/handoff.js +405 -0
  74. package/lib/engine/indexer.js +242 -0
  75. package/lib/engine/parser.js +53 -0
  76. package/lib/engine/query-expander.js +42 -0
  77. package/lib/engine/reranker.js +40 -0
  78. package/lib/engine/rrf.js +59 -0
  79. package/lib/engine/scanner.js +151 -0
  80. package/lib/engine/searcher.js +139 -0
  81. package/lib/engine/session-coordinator.js +306 -0
  82. package/lib/engine/session-manager.js +429 -0
  83. package/lib/engine/synthesizer.js +70 -0
  84. package/lib/installer.js +70 -0
  85. package/lib/instinct-block.js +33 -0
  86. package/lib/mcp-config-writer.js +88 -0
  87. package/lib/paths.js +57 -0
  88. package/lib/profiles/design.md +19 -0
  89. package/lib/profiles/general.md +16 -0
  90. package/lib/profiles/research-analysis.md +22 -0
  91. package/lib/profiles/software-development.md +23 -0
  92. package/lib/profiles/writing-content.md +19 -0
  93. package/lib/project-initializer.js +916 -0
  94. package/lib/registry/skills.js +102 -0
  95. package/lib/registry-searcher.js +99 -0
  96. package/lib/rules/rules-manager.js +169 -0
  97. package/lib/skill-fetcher.js +333 -0
  98. package/lib/well-known-builder.js +70 -0
  99. package/lib/wizard/index.js +404 -0
  100. package/lib/wizard/integration-detector.js +41 -0
  101. package/lib/wizard/project-detector.js +100 -0
  102. package/lib/wizard/prompt.js +156 -0
  103. package/lib/wizard/registry-embeddings.js +107 -0
  104. package/lib/wizard/skill-recommender.js +69 -0
  105. package/llms-full.txt +254 -0
  106. package/llms.txt +70 -0
  107. package/package.json +45 -0
  108. package/research-reports/2026-04-01-current-architecture.md +160 -0
  109. package/research-reports/IDEAS.md +93 -0
  110. package/rules/common/clean-code.md +42 -0
  111. package/rules/java/effective-java.md +42 -0
  112. package/rules/kotlin/effective-kotlin.md +37 -0
  113. package/rules/python/effective-python.md +38 -0
  114. package/rules/rust/rust.md +37 -0
  115. package/rules/typescript/effective-typescript.md +42 -0
  116. package/scripts/gen-llms-full.mjs +36 -0
  117. package/scripts/gen-og.mjs +142 -0
  118. package/scripts/validate-frontmatter.js +25 -0
  119. package/skills/animation-at-work/SKILL.md +270 -0
  120. package/skills/animation-at-work/assets/example_asset.txt +1 -0
  121. package/skills/animation-at-work/evals/evals.json +44 -0
  122. package/skills/animation-at-work/evals/results.json +13 -0
  123. package/skills/animation-at-work/examples/after.md +64 -0
  124. package/skills/animation-at-work/examples/before.md +35 -0
  125. package/skills/animation-at-work/references/api_reference.md +369 -0
  126. package/skills/animation-at-work/references/review-checklist.md +79 -0
  127. package/skills/animation-at-work/scripts/audit_animations.py +295 -0
  128. package/skills/animation-at-work/scripts/example.py +1 -0
  129. package/skills/clean-code-reviewer/SKILL.md +444 -0
  130. package/skills/clean-code-reviewer/audit.json +35 -0
  131. package/skills/clean-code-reviewer/evals/evals.json +185 -0
  132. package/skills/clean-code-reviewer/evals/results.json +13 -0
  133. package/skills/clean-code-reviewer/examples/after.md +48 -0
  134. package/skills/clean-code-reviewer/examples/before.md +33 -0
  135. package/skills/clean-code-reviewer/references/api_reference.md +158 -0
  136. package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
  137. package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
  138. package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
  139. package/skills/data-intensive-patterns/SKILL.md +267 -0
  140. package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
  141. package/skills/data-intensive-patterns/evals/evals.json +54 -0
  142. package/skills/data-intensive-patterns/evals/results.json +13 -0
  143. package/skills/data-intensive-patterns/examples/after.md +61 -0
  144. package/skills/data-intensive-patterns/examples/before.md +38 -0
  145. package/skills/data-intensive-patterns/references/api_reference.md +34 -0
  146. package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
  147. package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
  148. package/skills/data-intensive-patterns/scripts/adr.py +213 -0
  149. package/skills/data-intensive-patterns/scripts/example.py +1 -0
  150. package/skills/data-pipelines/SKILL.md +259 -0
  151. package/skills/data-pipelines/assets/example_asset.txt +1 -0
  152. package/skills/data-pipelines/evals/evals.json +45 -0
  153. package/skills/data-pipelines/evals/results.json +13 -0
  154. package/skills/data-pipelines/examples/after.md +97 -0
  155. package/skills/data-pipelines/examples/before.md +37 -0
  156. package/skills/data-pipelines/references/api_reference.md +301 -0
  157. package/skills/data-pipelines/references/review-checklist.md +181 -0
  158. package/skills/data-pipelines/scripts/example.py +1 -0
  159. package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
  160. package/skills/design-patterns/SKILL.md +271 -0
  161. package/skills/design-patterns/assets/example_asset.txt +1 -0
  162. package/skills/design-patterns/evals/evals.json +46 -0
  163. package/skills/design-patterns/evals/results.json +13 -0
  164. package/skills/design-patterns/examples/after.md +52 -0
  165. package/skills/design-patterns/examples/before.md +29 -0
  166. package/skills/design-patterns/references/api_reference.md +1 -0
  167. package/skills/design-patterns/references/patterns-catalog.md +726 -0
  168. package/skills/design-patterns/references/review-checklist.md +173 -0
  169. package/skills/design-patterns/scripts/example.py +1 -0
  170. package/skills/design-patterns/scripts/scaffold.py +807 -0
  171. package/skills/domain-driven-design/SKILL.md +142 -0
  172. package/skills/domain-driven-design/assets/example_asset.txt +1 -0
  173. package/skills/domain-driven-design/evals/evals.json +48 -0
  174. package/skills/domain-driven-design/evals/results.json +13 -0
  175. package/skills/domain-driven-design/examples/after.md +80 -0
  176. package/skills/domain-driven-design/examples/before.md +43 -0
  177. package/skills/domain-driven-design/references/api_reference.md +1 -0
  178. package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
  179. package/skills/domain-driven-design/references/review-checklist.md +158 -0
  180. package/skills/domain-driven-design/scripts/example.py +1 -0
  181. package/skills/domain-driven-design/scripts/scaffold.py +421 -0
  182. package/skills/effective-java/SKILL.md +227 -0
  183. package/skills/effective-java/assets/example_asset.txt +1 -0
  184. package/skills/effective-java/evals/evals.json +46 -0
  185. package/skills/effective-java/evals/results.json +13 -0
  186. package/skills/effective-java/examples/after.md +83 -0
  187. package/skills/effective-java/examples/before.md +37 -0
  188. package/skills/effective-java/references/api_reference.md +1 -0
  189. package/skills/effective-java/references/items-catalog.md +955 -0
  190. package/skills/effective-java/references/review-checklist.md +216 -0
  191. package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
  192. package/skills/effective-java/scripts/example.py +1 -0
  193. package/skills/effective-kotlin/SKILL.md +271 -0
  194. package/skills/effective-kotlin/assets/example_asset.txt +1 -0
  195. package/skills/effective-kotlin/audit.json +29 -0
  196. package/skills/effective-kotlin/evals/evals.json +45 -0
  197. package/skills/effective-kotlin/evals/results.json +13 -0
  198. package/skills/effective-kotlin/examples/after.md +36 -0
  199. package/skills/effective-kotlin/examples/before.md +38 -0
  200. package/skills/effective-kotlin/references/api_reference.md +1 -0
  201. package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
  202. package/skills/effective-kotlin/references/review-checklist.md +126 -0
  203. package/skills/effective-kotlin/scripts/example.py +1 -0
  204. package/skills/effective-python/SKILL.md +441 -0
  205. package/skills/effective-python/evals/evals.json +44 -0
  206. package/skills/effective-python/evals/results.json +13 -0
  207. package/skills/effective-python/examples/after.md +56 -0
  208. package/skills/effective-python/examples/before.md +40 -0
  209. package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
  210. package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
  211. package/skills/effective-python/ref-03-functions.md +186 -0
  212. package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
  213. package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
  214. package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
  215. package/skills/effective-python/ref-07-concurrency.md +213 -0
  216. package/skills/effective-python/ref-08-robustness-performance.md +248 -0
  217. package/skills/effective-python/ref-09-testing-debugging.md +253 -0
  218. package/skills/effective-python/ref-10-collaboration.md +175 -0
  219. package/skills/effective-python/references/api_reference.md +218 -0
  220. package/skills/effective-python/references/practices-catalog.md +483 -0
  221. package/skills/effective-python/references/review-checklist.md +190 -0
  222. package/skills/effective-python/scripts/lint.py +173 -0
  223. package/skills/effective-typescript/SKILL.md +262 -0
  224. package/skills/effective-typescript/audit.json +29 -0
  225. package/skills/effective-typescript/evals/evals.json +37 -0
  226. package/skills/effective-typescript/evals/results.json +13 -0
  227. package/skills/effective-typescript/examples/after.md +70 -0
  228. package/skills/effective-typescript/examples/before.md +47 -0
  229. package/skills/effective-typescript/references/api_reference.md +118 -0
  230. package/skills/effective-typescript/references/practices-catalog.md +371 -0
  231. package/skills/effective-typescript/scripts/review.py +169 -0
  232. package/skills/kotlin-in-action/SKILL.md +261 -0
  233. package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
  234. package/skills/kotlin-in-action/evals/evals.json +43 -0
  235. package/skills/kotlin-in-action/evals/results.json +13 -0
  236. package/skills/kotlin-in-action/examples/after.md +53 -0
  237. package/skills/kotlin-in-action/examples/before.md +39 -0
  238. package/skills/kotlin-in-action/references/api_reference.md +1 -0
  239. package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
  240. package/skills/kotlin-in-action/references/review-checklist.md +204 -0
  241. package/skills/kotlin-in-action/scripts/example.py +1 -0
  242. package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
  243. package/skills/lean-startup/SKILL.md +160 -0
  244. package/skills/lean-startup/assets/example_asset.txt +1 -0
  245. package/skills/lean-startup/evals/evals.json +43 -0
  246. package/skills/lean-startup/evals/results.json +13 -0
  247. package/skills/lean-startup/examples/after.md +80 -0
  248. package/skills/lean-startup/examples/before.md +34 -0
  249. package/skills/lean-startup/references/api_reference.md +319 -0
  250. package/skills/lean-startup/references/review-checklist.md +137 -0
  251. package/skills/lean-startup/scripts/example.py +1 -0
  252. package/skills/lean-startup/scripts/new_experiment.py +286 -0
  253. package/skills/microservices-patterns/SKILL.md +384 -0
  254. package/skills/microservices-patterns/evals/evals.json +45 -0
  255. package/skills/microservices-patterns/evals/results.json +13 -0
  256. package/skills/microservices-patterns/examples/after.md +69 -0
  257. package/skills/microservices-patterns/examples/before.md +40 -0
  258. package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
  259. package/skills/microservices-patterns/references/review-checklist.md +169 -0
  260. package/skills/microservices-patterns/scripts/new_service.py +583 -0
  261. package/skills/programming-with-rust/SKILL.md +209 -0
  262. package/skills/programming-with-rust/evals/evals.json +37 -0
  263. package/skills/programming-with-rust/evals/results.json +13 -0
  264. package/skills/programming-with-rust/examples/after.md +107 -0
  265. package/skills/programming-with-rust/examples/before.md +59 -0
  266. package/skills/programming-with-rust/references/api_reference.md +152 -0
  267. package/skills/programming-with-rust/references/practices-catalog.md +335 -0
  268. package/skills/programming-with-rust/scripts/review.py +142 -0
  269. package/skills/refactoring-ui/SKILL.md +362 -0
  270. package/skills/refactoring-ui/assets/example_asset.txt +1 -0
  271. package/skills/refactoring-ui/evals/evals.json +45 -0
  272. package/skills/refactoring-ui/evals/results.json +13 -0
  273. package/skills/refactoring-ui/examples/after.md +85 -0
  274. package/skills/refactoring-ui/examples/before.md +58 -0
  275. package/skills/refactoring-ui/references/api_reference.md +355 -0
  276. package/skills/refactoring-ui/references/review-checklist.md +114 -0
  277. package/skills/refactoring-ui/scripts/audit_css.py +250 -0
  278. package/skills/refactoring-ui/scripts/example.py +1 -0
  279. package/skills/rust-in-action/SKILL.md +350 -0
  280. package/skills/rust-in-action/evals/evals.json +38 -0
  281. package/skills/rust-in-action/evals/results.json +13 -0
  282. package/skills/rust-in-action/examples/after.md +156 -0
  283. package/skills/rust-in-action/examples/before.md +56 -0
  284. package/skills/rust-in-action/references/practices-catalog.md +346 -0
  285. package/skills/rust-in-action/scripts/review.py +147 -0
  286. package/skills/skill-router/SKILL.md +186 -0
  287. package/skills/skill-router/evals/evals.json +38 -0
  288. package/skills/skill-router/evals/results.json +13 -0
  289. package/skills/skill-router/examples/after.md +63 -0
  290. package/skills/skill-router/examples/before.md +39 -0
  291. package/skills/skill-router/references/api_reference.md +24 -0
  292. package/skills/skill-router/references/routing-heuristics.md +89 -0
  293. package/skills/skill-router/references/skill-catalog.md +174 -0
  294. package/skills/skill-router/scripts/route.py +266 -0
  295. package/skills/spring-boot-in-action/SKILL.md +340 -0
  296. package/skills/spring-boot-in-action/evals/evals.json +39 -0
  297. package/skills/spring-boot-in-action/evals/results.json +13 -0
  298. package/skills/spring-boot-in-action/examples/after.md +185 -0
  299. package/skills/spring-boot-in-action/examples/before.md +84 -0
  300. package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
  301. package/skills/spring-boot-in-action/scripts/review.py +184 -0
  302. package/skills/storytelling-with-data/SKILL.md +241 -0
  303. package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
  304. package/skills/storytelling-with-data/evals/evals.json +47 -0
  305. package/skills/storytelling-with-data/evals/results.json +13 -0
  306. package/skills/storytelling-with-data/examples/after.md +50 -0
  307. package/skills/storytelling-with-data/examples/before.md +33 -0
  308. package/skills/storytelling-with-data/references/api_reference.md +379 -0
  309. package/skills/storytelling-with-data/references/review-checklist.md +111 -0
  310. package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
  311. package/skills/storytelling-with-data/scripts/example.py +1 -0
  312. package/skills/system-design-interview/SKILL.md +233 -0
  313. package/skills/system-design-interview/assets/example_asset.txt +1 -0
  314. package/skills/system-design-interview/evals/evals.json +46 -0
  315. package/skills/system-design-interview/evals/results.json +13 -0
  316. package/skills/system-design-interview/examples/after.md +94 -0
  317. package/skills/system-design-interview/examples/before.md +27 -0
  318. package/skills/system-design-interview/references/api_reference.md +582 -0
  319. package/skills/system-design-interview/references/review-checklist.md +201 -0
  320. package/skills/system-design-interview/scripts/example.py +1 -0
  321. package/skills/system-design-interview/scripts/new_design.py +421 -0
  322. package/skills/using-asyncio-python/SKILL.md +290 -0
  323. package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
  324. package/skills/using-asyncio-python/evals/evals.json +43 -0
  325. package/skills/using-asyncio-python/evals/results.json +13 -0
  326. package/skills/using-asyncio-python/examples/after.md +68 -0
  327. package/skills/using-asyncio-python/examples/before.md +39 -0
  328. package/skills/using-asyncio-python/references/api_reference.md +267 -0
  329. package/skills/using-asyncio-python/references/review-checklist.md +149 -0
  330. package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
  331. package/skills/using-asyncio-python/scripts/example.py +1 -0
  332. package/skills/web-scraping-python/SKILL.md +280 -0
  333. package/skills/web-scraping-python/assets/example_asset.txt +1 -0
  334. package/skills/web-scraping-python/evals/evals.json +46 -0
  335. package/skills/web-scraping-python/evals/results.json +13 -0
  336. package/skills/web-scraping-python/examples/after.md +109 -0
  337. package/skills/web-scraping-python/examples/before.md +40 -0
  338. package/skills/web-scraping-python/references/api_reference.md +393 -0
  339. package/skills/web-scraping-python/references/review-checklist.md +163 -0
  340. package/skills/web-scraping-python/scripts/example.py +1 -0
  341. package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
  342. package/skills/writing-plans/audit.json +34 -0
  343. package/tests/agent-detector.test.js +83 -0
  344. package/tests/corrections.test.js +245 -0
  345. package/tests/doctor/hook-installer.test.js +72 -0
  346. package/tests/doctor/usage-tracker.test.js +140 -0
  347. package/tests/engine/benchmark-eval.test.js +31 -0
  348. package/tests/engine/bm25-index.test.js +85 -0
  349. package/tests/engine/capture-command.test.js +35 -0
  350. package/tests/engine/capture.test.js +17 -0
  351. package/tests/engine/graph-augmented-search.test.js +107 -0
  352. package/tests/engine/graph-injector.test.js +44 -0
  353. package/tests/engine/graph.test.js +216 -0
  354. package/tests/engine/hybrid-searcher.test.js +74 -0
  355. package/tests/engine/indexer-bm25.test.js +37 -0
  356. package/tests/engine/mcp-tools.test.js +73 -0
  357. package/tests/engine/project-initializer-mcp.test.js +99 -0
  358. package/tests/engine/query-expander.test.js +36 -0
  359. package/tests/engine/reranker.test.js +51 -0
  360. package/tests/engine/rrf.test.js +49 -0
  361. package/tests/engine/srag-prefix.test.js +47 -0
  362. package/tests/instinct-block.test.js +23 -0
  363. package/tests/mcp-config-writer.test.js +60 -0
  364. package/tests/project-initializer-new-agents.test.js +48 -0
  365. package/tests/rules/rules-manager.test.js +230 -0
  366. package/tests/well-known-builder.test.js +40 -0
  367. package/tests/wizard/integration-detector.test.js +31 -0
  368. package/tests/wizard/project-detector.test.js +51 -0
  369. package/tests/wizard/prompt-session.test.js +61 -0
  370. package/tests/wizard/prompt.test.js +16 -0
  371. package/tests/wizard/registry-embeddings.test.js +35 -0
  372. package/tests/wizard/skill-recommender.test.js +34 -0
  373. package/tests/wizard/slot-count.test.js +25 -0
  374. package/vercel.json +21 -0
@@ -0,0 +1,245 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ levelFromMentions, cosine, loadCorrections, listCorrections,
8
+ removeCorrection, rebuildLearnedSection, MARKER_START, MARKER_END,
9
+ addCorrection,
10
+ } from '../lib/engine/corrections.js';
11
+
12
+ function tmpHome() {
13
+ const dir = mkdtempSync(join(tmpdir(), 'booklib-test-'));
14
+ mkdirSync(join(dir, '.booklib'), { recursive: true });
15
+ mkdirSync(join(dir, '.claude'), { recursive: true });
16
+ return dir;
17
+ }
18
+
19
+ function seedCorrections(home, entries) {
20
+ const p = join(home, '.booklib', 'corrections.jsonl');
21
+ writeFileSync(p, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
22
+ }
23
+
24
+ test('levelFromMentions: 1→1, 3→2, 5→3, 10→4', () => {
25
+ assert.equal(levelFromMentions(1), 1);
26
+ assert.equal(levelFromMentions(2), 1);
27
+ assert.equal(levelFromMentions(3), 2);
28
+ assert.equal(levelFromMentions(4), 2);
29
+ assert.equal(levelFromMentions(5), 3);
30
+ assert.equal(levelFromMentions(9), 3);
31
+ assert.equal(levelFromMentions(10), 4);
32
+ assert.equal(levelFromMentions(99), 4);
33
+ });
34
+
35
+ test('cosine: identical vectors → 1.0', () => {
36
+ const v = [0.5, 0.5, 0.5, 0.5];
37
+ assert.ok(Math.abs(cosine(v, v) - 1.0) < 1e-9);
38
+ });
39
+
40
+ test('cosine: orthogonal vectors → 0.0', () => {
41
+ const a = [1, 0];
42
+ const b = [0, 1];
43
+ assert.ok(Math.abs(cosine(a, b)) < 1e-9);
44
+ });
45
+
46
+ test('loadCorrections: returns [] when file missing', () => {
47
+ const home = tmpHome();
48
+ assert.deepEqual(loadCorrections(home), []);
49
+ });
50
+
51
+ test('loadCorrections: skips corrupt lines, preserves valid ones', () => {
52
+ const home = tmpHome();
53
+ const valid = { id: 'abc', text: 'use const', mentions: 1, level: 1,
54
+ sessions: [], firstSeen: '', lastSeen: '' };
55
+ // One corrupt line mixed with one valid line
56
+ writeFileSync(
57
+ join(home, '.booklib', 'corrections.jsonl'),
58
+ 'not json\n' + JSON.stringify(valid) + '\n'
59
+ );
60
+ const result = loadCorrections(home);
61
+ assert.equal(result.length, 1);
62
+ assert.equal(result[0].id, 'abc');
63
+ });
64
+
65
+ test('loadCorrections: parses valid JSONL', () => {
66
+ const home = tmpHome();
67
+ const entry = { id: 'abc123', text: 'use const', mentions: 2, level: 1,
68
+ sessions: [], firstSeen: '2026-01-01', lastSeen: '2026-01-01' };
69
+ seedCorrections(home, [entry]);
70
+ const result = loadCorrections(home);
71
+ assert.equal(result.length, 1);
72
+ assert.equal(result[0].id, 'abc123');
73
+ });
74
+
75
+ test('listCorrections: sorted by mentions descending', () => {
76
+ const home = tmpHome();
77
+ const a = { id: 'a', text: 'A', mentions: 1, level: 1, sessions: [], firstSeen: '', lastSeen: '' };
78
+ const b = { id: 'b', text: 'B', mentions: 9, level: 3, sessions: [], firstSeen: '', lastSeen: '' };
79
+ const c = { id: 'c', text: 'C', mentions: 4, level: 2, sessions: [], firstSeen: '', lastSeen: '' };
80
+ seedCorrections(home, [a, b, c]);
81
+ const sorted = listCorrections(home);
82
+ assert.equal(sorted[0].id, 'b');
83
+ assert.equal(sorted[1].id, 'c');
84
+ assert.equal(sorted[2].id, 'a');
85
+ });
86
+
87
+ test('removeCorrection: returns removed entry', () => {
88
+ const home = tmpHome();
89
+ const entry = { id: 'xyz', text: 'no var', mentions: 2, level: 1,
90
+ sessions: [], firstSeen: '', lastSeen: '' };
91
+ seedCorrections(home, [entry]);
92
+ const removed = removeCorrection('xyz', home);
93
+ assert.equal(removed.id, 'xyz');
94
+ assert.equal(loadCorrections(home).length, 0);
95
+ });
96
+
97
+ test('removeCorrection: returns null for unknown id', () => {
98
+ const home = tmpHome();
99
+ seedCorrections(home, []);
100
+ assert.equal(removeCorrection('nope', home), null);
101
+ });
102
+
103
+ test('rebuildLearnedSection: writes section with level-3+ corrections', () => {
104
+ const home = tmpHome();
105
+ const entries = [
106
+ { id: 'a', text: 'always use const', mentions: 5, level: 3, sessions: [], firstSeen: '', lastSeen: '' },
107
+ { id: 'b', text: 'no magic numbers', mentions: 1, level: 1, sessions: [], firstSeen: '', lastSeen: '' },
108
+ ];
109
+ seedCorrections(home, entries);
110
+ rebuildLearnedSection(home);
111
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
112
+ assert.ok(content.includes(MARKER_START));
113
+ assert.ok(content.includes('always use const'));
114
+ assert.ok(!content.includes('no magic numbers'));
115
+ assert.ok(content.includes(MARKER_END));
116
+ });
117
+
118
+ test('rebuildLearnedSection: replaces existing section idempotently', () => {
119
+ const home = tmpHome();
120
+ const entries = [
121
+ { id: 'a', text: 'use const', mentions: 5, level: 3, sessions: [], firstSeen: '', lastSeen: '' },
122
+ ];
123
+ seedCorrections(home, entries);
124
+ rebuildLearnedSection(home);
125
+ rebuildLearnedSection(home);
126
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
127
+ const count = (content.match(/booklib-learned-start/g) || []).length;
128
+ assert.equal(count, 1);
129
+ });
130
+
131
+ test('rebuildLearnedSection: removes section when no level-3+ corrections', () => {
132
+ const home = tmpHome();
133
+ seedCorrections(home, [
134
+ { id: 'a', text: 'use const', mentions: 5, level: 3, sessions: [], firstSeen: '', lastSeen: '' },
135
+ ]);
136
+ rebuildLearnedSection(home);
137
+ seedCorrections(home, [
138
+ { id: 'a', text: 'use const', mentions: 1, level: 1, sessions: [], firstSeen: '', lastSeen: '' },
139
+ ]);
140
+ rebuildLearnedSection(home);
141
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
142
+ assert.ok(!content.includes(MARKER_START));
143
+ });
144
+
145
+ test('rebuildLearnedSection: preserves existing CLAUDE.md content', () => {
146
+ const home = tmpHome();
147
+ const existing = '# My existing rules\n\nSome content here.\n';
148
+ writeFileSync(join(home, '.claude', 'CLAUDE.md'), existing);
149
+ seedCorrections(home, [
150
+ { id: 'a', text: 'use const', mentions: 5, level: 3, sessions: [], firstSeen: '', lastSeen: '' },
151
+ ]);
152
+ rebuildLearnedSection(home);
153
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
154
+ assert.ok(content.includes('# My existing rules'));
155
+ assert.ok(content.includes(MARKER_START));
156
+ });
157
+
158
+ // ── addCorrection tests (use injected embedFn to avoid loading real model) ────
159
+
160
+ function makeEmbedFn(map) {
161
+ return async (text) => {
162
+ if (map[text]) return map[text];
163
+ const v = new Array(8).fill(0);
164
+ v[text.length % 8] = 1;
165
+ return v;
166
+ };
167
+ }
168
+
169
+ test('addCorrection: new correction stored at level 1', async () => {
170
+ const home = tmpHome();
171
+ const embedFn = makeEmbedFn({});
172
+ const result = await addCorrection('use const not var', home, embedFn);
173
+ assert.equal(result.mentions, 1);
174
+ assert.equal(result.level, 1);
175
+ assert.equal(result.wasExisting, false);
176
+ assert.equal(loadCorrections(home).length, 1);
177
+ });
178
+
179
+ test('addCorrection: identical text increments existing', async () => {
180
+ const home = tmpHome();
181
+ const vec = [1, 0, 0, 0, 0, 0, 0, 0];
182
+ const embedFn = makeEmbedFn({ 'use const': vec, 'use const not var': vec });
183
+ await addCorrection('use const', home, embedFn);
184
+ const result = await addCorrection('use const not var', home, embedFn);
185
+ assert.equal(result.wasExisting, true);
186
+ assert.equal(result.mentions, 2);
187
+ assert.equal(loadCorrections(home).length, 1);
188
+ });
189
+
190
+ test('addCorrection: different text creates new entry', async () => {
191
+ const home = tmpHome();
192
+ const embedFn = makeEmbedFn({
193
+ 'use const': [1, 0, 0, 0, 0, 0, 0, 0],
194
+ 'no magic numbers': [0, 1, 0, 0, 0, 0, 0, 0],
195
+ });
196
+ await addCorrection('use const', home, embedFn);
197
+ await addCorrection('no magic numbers', home, embedFn);
198
+ assert.equal(loadCorrections(home).length, 2);
199
+ });
200
+
201
+ test('addCorrection: reaching level 3 triggers CLAUDE.md rebuild', async () => {
202
+ const home = tmpHome();
203
+ const vec = [1, 0, 0, 0, 0, 0, 0, 0];
204
+ const embedFn = makeEmbedFn({ 'use const': vec });
205
+ seedCorrections(home, [{
206
+ id: 'test1', text: 'use const', mentions: 4, level: 2,
207
+ sessions: [], firstSeen: '', lastSeen: '',
208
+ }]);
209
+ await addCorrection('use const', home, embedFn);
210
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
211
+ assert.ok(content.includes(MARKER_START));
212
+ assert.ok(content.includes('use const'));
213
+ });
214
+
215
+ test('addCorrection: does not expose embedding field in return value', async () => {
216
+ const home = tmpHome();
217
+ const embedFn = makeEmbedFn({ 'test rule': [1, 0, 0, 0, 0, 0, 0, 0] });
218
+ const result = await addCorrection('test rule', home, embedFn);
219
+ assert.ok(!('embedding' in result));
220
+ });
221
+
222
+ test('addCorrection: backfilled embedding is persisted to disk', async (t) => {
223
+ const tmpHome = mkdtempSync(join(tmpdir(), 'bl-test-'));
224
+ const bookLibDir = join(tmpHome, '.booklib');
225
+ mkdirSync(bookLibDir, { recursive: true });
226
+
227
+ // Seed entry with NO embedding field
228
+ const entry = {
229
+ id: 'aabbcc01', text: 'use const', mentions: 1, level: 1,
230
+ sessions: ['s1'], firstSeen: '2024-01-01T00:00:00.000Z', lastSeen: '2024-01-01T00:00:00.000Z',
231
+ };
232
+ writeFileSync(join(bookLibDir, 'corrections.jsonl'), JSON.stringify(entry) + '\n');
233
+
234
+ const embedMap = { 'use const': [1, 0, 0], 'use const again': [1, 0, 0] };
235
+ const embedFn = makeEmbedFn(embedMap);
236
+
237
+ // Call add with text that deduplicates (same vector)
238
+ await addCorrection('use const again', tmpHome, embedFn);
239
+
240
+ // Reload from disk and verify embedding was persisted
241
+ const loaded = loadCorrections(tmpHome);
242
+ assert.equal(loaded.length, 1);
243
+ assert.ok(loaded[0].embedding, 'embedding should be persisted to disk after backfill');
244
+ assert.deepEqual(loaded[0].embedding, [1, 0, 0]);
245
+ });
@@ -0,0 +1,72 @@
1
+ // tests/doctor/hook-installer.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtempSync, rmSync, mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { installTrackingHook } from '../../lib/doctor/hook-installer.js';
8
+
9
+ function tmp() { return mkdtempSync(join(tmpdir(), 'booklib-hook-')); }
10
+
11
+ test('installTrackingHook writes track-usage.mjs', () => {
12
+ const home = tmp();
13
+ installTrackingHook(home);
14
+ const scriptPath = join(home, '.booklib', 'track-usage.mjs');
15
+ assert.ok(existsSync(scriptPath));
16
+ const content = readFileSync(scriptPath, 'utf8');
17
+ assert.ok(content.includes('usage.json'));
18
+ assert.ok(content.includes('skill'));
19
+ rmSync(home, { recursive: true });
20
+ });
21
+
22
+ test('installTrackingHook creates settings.json with Skill hook', () => {
23
+ const home = tmp();
24
+ mkdirSync(join(home, '.claude'), { recursive: true });
25
+ installTrackingHook(home);
26
+ const settings = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
27
+ assert.ok(Array.isArray(settings.hooks?.PreToolUse));
28
+ const entry = settings.hooks.PreToolUse.find(e => e.matcher === 'Skill');
29
+ assert.ok(entry);
30
+ assert.ok(entry.hooks[0].command.includes('track-usage.mjs'));
31
+ rmSync(home, { recursive: true });
32
+ });
33
+
34
+ test('installTrackingHook preserves existing hooks', () => {
35
+ const home = tmp();
36
+ mkdirSync(join(home, '.claude'), { recursive: true });
37
+ const settingsPath = join(home, '.claude', 'settings.json');
38
+ writeFileSync(settingsPath, JSON.stringify({
39
+ hooks: {
40
+ PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'echo bash' }] }]
41
+ }
42
+ }, null, 2));
43
+ installTrackingHook(home);
44
+ const updated = JSON.parse(readFileSync(settingsPath, 'utf8'));
45
+ assert.ok(updated.hooks.PreToolUse.find(e => e.matcher === 'Bash'));
46
+ assert.ok(updated.hooks.PreToolUse.find(e => e.matcher === 'Skill'));
47
+ rmSync(home, { recursive: true });
48
+ });
49
+
50
+ test('installTrackingHook reports alreadyInstalled on second call', () => {
51
+ const home = tmp();
52
+ installTrackingHook(home);
53
+ const result = installTrackingHook(home);
54
+ assert.equal(result.alreadyInstalled, true);
55
+ rmSync(home, { recursive: true });
56
+ });
57
+
58
+ test('installTrackingHook returns scriptPath and settingsPath', () => {
59
+ const home = tmp();
60
+ const result = installTrackingHook(home);
61
+ assert.ok(result.scriptPath.endsWith('track-usage.mjs'));
62
+ assert.ok(result.settingsPath.endsWith('settings.json'));
63
+ rmSync(home, { recursive: true });
64
+ });
65
+
66
+ test('installTrackingHook throws on corrupted settings.json', () => {
67
+ const home = tmp();
68
+ mkdirSync(join(home, '.claude'), { recursive: true });
69
+ writeFileSync(join(home, '.claude', 'settings.json'), 'not valid json');
70
+ assert.throws(() => installTrackingHook(home), SyntaxError);
71
+ rmSync(home, { recursive: true });
72
+ });
@@ -0,0 +1,140 @@
1
+ // tests/doctor/usage-tracker.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { appendUsage, readUsage, summarize } from '../../lib/doctor/usage-tracker.js';
8
+
9
+ function tmp() { return mkdtempSync(join(tmpdir(), 'booklib-doctor-')); }
10
+
11
+ test('readUsage returns [] when file does not exist', () => {
12
+ const dir = tmp();
13
+ const result = readUsage(join(dir, 'usage.json'));
14
+ assert.deepEqual(result, []);
15
+ rmSync(dir, { recursive: true });
16
+ });
17
+
18
+ test('appendUsage creates file and adds entry', () => {
19
+ const dir = tmp();
20
+ const file = join(dir, 'usage.json');
21
+ appendUsage('effective-python', file);
22
+ const entries = readUsage(file);
23
+ assert.equal(entries.length, 1);
24
+ assert.equal(entries[0].skill, 'effective-python');
25
+ assert.ok(typeof entries[0].timestamp === 'string');
26
+ rmSync(dir, { recursive: true });
27
+ });
28
+
29
+ test('appendUsage accumulates multiple entries', () => {
30
+ const dir = tmp();
31
+ const file = join(dir, 'usage.json');
32
+ appendUsage('effective-python', file);
33
+ appendUsage('effective-python', file);
34
+ appendUsage('clean-code-reviewer', file);
35
+ const entries = readUsage(file);
36
+ assert.equal(entries.length, 3);
37
+ rmSync(dir, { recursive: true });
38
+ });
39
+
40
+ test('summarize returns entry for every installed skill', () => {
41
+ const entries = [
42
+ { skill: 'effective-python', timestamp: new Date().toISOString() },
43
+ ];
44
+ const result = summarize(entries, ['effective-python', 'design-patterns']);
45
+ assert.equal(result.length, 2);
46
+ const names = result.map(r => r.name);
47
+ assert.ok(names.includes('effective-python'));
48
+ assert.ok(names.includes('design-patterns'));
49
+ });
50
+
51
+ test('summarize counts uses correctly', () => {
52
+ const now = new Date().toISOString();
53
+ const entries = [
54
+ { skill: 'effective-python', timestamp: now },
55
+ { skill: 'effective-python', timestamp: now },
56
+ { skill: 'clean-code-reviewer', timestamp: now },
57
+ ];
58
+ const result = summarize(entries, ['effective-python', 'clean-code-reviewer']);
59
+ const py = result.find(r => r.name === 'effective-python');
60
+ const cc = result.find(r => r.name === 'clean-code-reviewer');
61
+ assert.equal(py.uses, 2);
62
+ assert.equal(cc.uses, 1);
63
+ });
64
+
65
+ test('summarize sets lastUsed to most recent timestamp', () => {
66
+ const old = new Date('2026-01-01T00:00:00Z').toISOString();
67
+ const recent = new Date('2026-03-28T00:00:00Z').toISOString();
68
+ const entries = [
69
+ { skill: 'effective-python', timestamp: old },
70
+ { skill: 'effective-python', timestamp: recent },
71
+ ];
72
+ const result = summarize(entries, ['effective-python']);
73
+ const py = result.find(r => r.name === 'effective-python');
74
+ assert.equal(py.lastUsed.toISOString(), recent);
75
+ });
76
+
77
+ test('summarize sets lastUsed null for never-used skill', () => {
78
+ const result = summarize([], ['design-patterns']);
79
+ assert.equal(result[0].lastUsed, null);
80
+ assert.equal(result[0].uses, 0);
81
+ });
82
+
83
+ test('summarize flags 0-use skill installed >30 days ago as remove', () => {
84
+ const installDate = new Date(Date.now() - 47 * 24 * 60 * 60 * 1000);
85
+ const result = summarize([], ['design-patterns'], { 'design-patterns': installDate });
86
+ assert.equal(result[0].suggestion, 'remove');
87
+ });
88
+
89
+ test('summarize flags <2 uses in 60 days as low-activity', () => {
90
+ const oldDate = new Date(Date.now() - 62 * 24 * 60 * 60 * 1000).toISOString();
91
+ const entries = [{ skill: 'effective-java', timestamp: oldDate }];
92
+ const result = summarize(entries, ['effective-java']);
93
+ assert.equal(result[0].suggestion, 'low-activity');
94
+ });
95
+
96
+ test('summarize healthy skill has suggestion null', () => {
97
+ const entries = Array.from({ length: 5 }, () => ({
98
+ skill: 'effective-python',
99
+ timestamp: new Date().toISOString(),
100
+ }));
101
+ const result = summarize(entries, ['effective-python']);
102
+ assert.equal(result[0].suggestion, null);
103
+ });
104
+
105
+ test('appendUsage creates nested directories if absent', () => {
106
+ const dir = tmp();
107
+ const file = join(dir, 'deep', 'nested', 'usage.json');
108
+ appendUsage('effective-python', file);
109
+ const entries = readUsage(file);
110
+ assert.equal(entries.length, 1);
111
+ rmSync(dir, { recursive: true });
112
+ });
113
+
114
+ test('summarize sorts healthy skills before suggestions', () => {
115
+ const now = new Date().toISOString();
116
+ const oldDate = new Date(Date.now() - 62 * 24 * 60 * 60 * 1000).toISOString();
117
+ // design-patterns installed 20 days ago (established, no recent use → low-activity)
118
+ const installDates = { 'design-patterns': new Date(Date.now() - 20 * 24 * 60 * 60 * 1000) };
119
+ const entries = [
120
+ { skill: 'effective-python', timestamp: now },
121
+ { skill: 'effective-python', timestamp: now },
122
+ { skill: 'effective-python', timestamp: now },
123
+ { skill: 'effective-java', timestamp: oldDate },
124
+ ];
125
+ const result = summarize(entries, ['effective-python', 'effective-java', 'design-patterns'], installDates);
126
+ // healthy first
127
+ assert.equal(result[0].suggestion, null);
128
+ assert.equal(result[0].name, 'effective-python');
129
+ // suggestions after
130
+ assert.ok(result.slice(1).every(r => r.suggestion !== null));
131
+ });
132
+
133
+ test('readUsage returns [] for corrupt file', () => {
134
+ const dir = tmp();
135
+ const file = join(dir, 'usage.json');
136
+ writeFileSync(file, 'not valid json{{{');
137
+ const result = readUsage(file);
138
+ assert.deepEqual(result, []);
139
+ rmSync(dir, { recursive: true });
140
+ });
@@ -0,0 +1,31 @@
1
+ // tests/engine/benchmark-eval.test.js
2
+ import { describe, it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { computeMRR, computeRecall, computeNDCG } from '../../benchmark/run-eval.js';
5
+
6
+ describe('eval metrics', () => {
7
+ it('MRR@5: rank 1 hit returns 1.0', () => {
8
+ assert.strictEqual(computeMRR([['a', 'b', 'c']], [['a']], 5), 1.0);
9
+ });
10
+
11
+ it('MRR@5: rank 2 hit returns 0.5', () => {
12
+ assert.strictEqual(computeMRR([['x', 'a', 'b']], [['a']], 5), 0.5);
13
+ });
14
+
15
+ it('MRR@5: no hit returns 0', () => {
16
+ assert.strictEqual(computeMRR([['x', 'y', 'z']], [['a']], 5), 0);
17
+ });
18
+
19
+ it('Recall@5: all relevant in top 5 returns 1.0', () => {
20
+ assert.strictEqual(computeRecall([['a', 'b', 'c']], [['a', 'b']], 5), 1.0);
21
+ });
22
+
23
+ it('Recall@5: no relevant returns 0', () => {
24
+ assert.strictEqual(computeRecall([['x', 'y', 'z']], [['a']], 5), 0);
25
+ });
26
+
27
+ it('NDCG@5: perfect ranking returns 1.0', () => {
28
+ const score = computeNDCG([['a', 'b']], [['a', 'b']], 5);
29
+ assert.ok(Math.abs(score - 1.0) < 0.01);
30
+ });
31
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ import { BM25Index } from '../../lib/engine/bm25-index.js';
7
+
8
+ const CHUNKS = [
9
+ { text: 'null safety kotlin avoid null pointer exception', metadata: { name: 'effective-kotlin' } },
10
+ { text: 'typescript strict mode null undefined checks', metadata: { name: 'effective-typescript' } },
11
+ { text: 'java generics wildcards bounded type parameters', metadata: { name: 'effective-java' } },
12
+ { text: 'kotlin data class immutable copy pattern', metadata: { name: 'effective-kotlin' } },
13
+ { text: 'clean code naming variables functions clear intent', metadata: { name: 'clean-code-reviewer' } },
14
+ ];
15
+
16
+ describe('BM25Index', () => {
17
+ it('returns higher score for documents containing query terms', () => {
18
+ const idx = new BM25Index();
19
+ idx.build(CHUNKS);
20
+ const results = idx.search('kotlin null safety', 3);
21
+ assert.ok(results.length > 0);
22
+ assert.strictEqual(results[0].metadata.name, 'effective-kotlin');
23
+ assert.ok(results[0].score > 0);
24
+ });
25
+
26
+ it('returns results sorted by score descending', () => {
27
+ const idx = new BM25Index();
28
+ idx.build(CHUNKS);
29
+ const results = idx.search('kotlin', 5);
30
+ for (let i = 1; i < results.length; i++) {
31
+ assert.ok(results[i - 1].score >= results[i].score);
32
+ }
33
+ });
34
+
35
+ it('returns empty array for query with no matching terms', () => {
36
+ const idx = new BM25Index();
37
+ idx.build(CHUNKS);
38
+ const results = idx.search('xyzzy completely unknown term', 5);
39
+ assert.strictEqual(results.length, 0);
40
+ });
41
+
42
+ it('save and load round-trip preserves search results', () => {
43
+ const tmpFile = path.join(os.tmpdir(), `bm25-test-${Date.now()}.json`);
44
+ try {
45
+ const idx = new BM25Index();
46
+ idx.build(CHUNKS);
47
+ idx.save(tmpFile);
48
+ const loaded = BM25Index.load(tmpFile);
49
+ const original = idx.search('kotlin null', 3);
50
+ const restored = loaded.search('kotlin null', 3);
51
+ assert.deepEqual(original.map(r => r.metadata.name), restored.map(r => r.metadata.name));
52
+ } finally {
53
+ fs.unlinkSync(tmpFile);
54
+ }
55
+ });
56
+
57
+ it('add() appends a document and it becomes searchable', () => {
58
+ const idx = new BM25Index();
59
+ idx.build(CHUNKS);
60
+ idx.add({ text: 'python dataclass immutable frozen field', metadata: { name: 'effective-python' } });
61
+ const results = idx.search('python dataclass', 3);
62
+ assert.ok(results.some(r => r.metadata.name === 'effective-python'));
63
+ });
64
+
65
+ it('search on empty index returns empty array', () => {
66
+ const idx = new BM25Index();
67
+ const results = idx.search('kotlin', 5);
68
+ assert.deepEqual(results, []);
69
+ });
70
+
71
+ it('search with all short tokens returns empty array', () => {
72
+ const idx = new BM25Index();
73
+ idx.build(CHUNKS);
74
+ const results = idx.search('a b', 5);
75
+ assert.strictEqual(results.length, 0);
76
+ });
77
+
78
+ it('add() works on a fresh index without prior build()', () => {
79
+ const idx = new BM25Index();
80
+ idx.add({ text: 'kotlin extension function', metadata: { name: 'effective-kotlin' } });
81
+ const results = idx.search('kotlin extension', 3);
82
+ assert.ok(results.length > 0);
83
+ assert.strictEqual(results[0].metadata.name, 'effective-kotlin');
84
+ });
85
+ });
@@ -0,0 +1,35 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parseCaptureLinkArgs } from '../../lib/engine/graph.js';
4
+
5
+ test('parseCaptureLinkArgs returns empty array for empty string', () => {
6
+ assert.deepStrictEqual(parseCaptureLinkArgs(''), []);
7
+ });
8
+
9
+ test('parseCaptureLinkArgs parses single link', () => {
10
+ const result = parseCaptureLinkArgs('effective-kotlin:applies-to');
11
+ assert.deepStrictEqual(result, [{ to: 'effective-kotlin', type: 'applies-to' }]);
12
+ });
13
+
14
+ test('parseCaptureLinkArgs parses multiple links', () => {
15
+ const result = parseCaptureLinkArgs('effective-kotlin:applies-to,design-patterns:see-also');
16
+ assert.deepStrictEqual(result, [
17
+ { to: 'effective-kotlin', type: 'applies-to' },
18
+ { to: 'design-patterns', type: 'see-also' },
19
+ ]);
20
+ });
21
+
22
+ test('parseCaptureLinkArgs trims whitespace', () => {
23
+ const result = parseCaptureLinkArgs(' effective-kotlin : applies-to ');
24
+ assert.deepStrictEqual(result, [{ to: 'effective-kotlin', type: 'applies-to' }]);
25
+ });
26
+
27
+ test('parseCaptureLinkArgs skips malformed pairs without colon', () => {
28
+ const result = parseCaptureLinkArgs('effective-kotlin,design-patterns:see-also');
29
+ assert.deepStrictEqual(result, [{ to: 'design-patterns', type: 'see-also' }]);
30
+ });
31
+
32
+ test('parseCaptureLinkArgs handles skill names with hyphens', () => {
33
+ const result = parseCaptureLinkArgs('clean-code-reviewer:see-also');
34
+ assert.deepStrictEqual(result, [{ to: 'clean-code-reviewer', type: 'see-also' }]);
35
+ });
@@ -0,0 +1,17 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildDictatePrompt, buildSummarizePrompt } from '../../lib/engine/capture.js';
4
+
5
+ test('buildDictatePrompt contains raw text and required fields', () => {
6
+ const prompt = buildDictatePrompt('rough ideas about auth');
7
+ assert.ok(prompt.includes('rough ideas about auth'));
8
+ assert.ok(prompt.includes('title'));
9
+ assert.ok(prompt.includes('tags'));
10
+ });
11
+
12
+ test('buildSummarizePrompt contains conversation and required sections', () => {
13
+ const prompt = buildSummarizePrompt('User: question\nAssistant: answer', 'My title');
14
+ assert.ok(prompt.includes('User: question'));
15
+ assert.ok(prompt.includes('Key Decisions'));
16
+ assert.ok(prompt.includes('My title'));
17
+ });