@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,107 @@
1
+ import { test, describe } 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 { BookLibIndexer } from '../../lib/engine/indexer.js';
7
+ import { BookLibSearcher } from '../../lib/engine/searcher.js';
8
+
9
+ async function buildTestIndex(tmpDir) {
10
+ const indexDir = path.join(tmpDir, 'index');
11
+ const skillsDir = path.join(tmpDir, 'skills');
12
+
13
+ const skills = [
14
+ { name: 'effective-kotlin', text: 'Kotlin null safety val immutable data class sealed class' },
15
+ { name: 'design-patterns', text: 'Design patterns singleton factory observer decorator strategy' },
16
+ { name: 'clean-code-reviewer', text: 'clean code naming functions variables single responsibility' },
17
+ ];
18
+
19
+ for (const skill of skills) {
20
+ const dir = path.join(skillsDir, skill.name);
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), `---
23
+ name: ${skill.name}
24
+ description: Test skill
25
+ version: "1.0"
26
+ tags: [test]
27
+ license: MIT
28
+ ---
29
+ ${skill.text}
30
+ `);
31
+ }
32
+
33
+ const indexer = new BookLibIndexer(indexDir);
34
+ await indexer.indexDirectory(skillsDir, true, { quiet: true });
35
+ return indexDir;
36
+ }
37
+
38
+ function writeEdge(bookLibDir, edge) {
39
+ const graphDir = path.join(bookLibDir, 'knowledge');
40
+ fs.mkdirSync(graphDir, { recursive: true });
41
+ fs.appendFileSync(path.join(graphDir, 'graph.jsonl'), JSON.stringify(edge) + '\n', 'utf8');
42
+ }
43
+
44
+ describe('graph-augmented search', () => {
45
+ test('useGraph: false returns no graph-linked results', async () => {
46
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-graph-'));
47
+ const indexDir = await buildTestIndex(tmpDir);
48
+ writeEdge(path.dirname(indexDir), {
49
+ from: 'insight_abc', to: 'design-patterns', type: 'applies-to', weight: 1.0, created: '2026-03-31',
50
+ });
51
+ const searcher = new BookLibSearcher(indexDir);
52
+ const results = await searcher.search('kotlin immutable', 5, 0, { useGraph: false });
53
+ assert.ok(results.every(r => r.metadata?.source !== 'graph'), 'no graph-linked results expected');
54
+ });
55
+
56
+ test('useGraph: true appends linked skill when edge exists', async () => {
57
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-graph-'));
58
+ const indexDir = await buildTestIndex(tmpDir);
59
+ const bookLibDir = path.dirname(indexDir);
60
+ // Point to a skill that is not indexed, so it cannot appear in top-k on its own
61
+ writeEdge(bookLibDir, {
62
+ from: 'effective-kotlin', to: 'not-indexed-skill', type: 'applies-to', weight: 1.0, created: '2026-03-31',
63
+ });
64
+ const searcher = new BookLibSearcher(indexDir);
65
+ const results = await searcher.search('kotlin immutable', 5, 0, { useGraph: true });
66
+ const graphResults = results.filter(r => r.metadata?.source === 'graph');
67
+ assert.ok(graphResults.length > 0, 'should have at least one graph-linked result');
68
+ assert.strictEqual(graphResults[0].metadata.name, 'not-indexed-skill');
69
+ assert.strictEqual(graphResults[0].metadata.edgeType, 'applies-to');
70
+ });
71
+
72
+ test('useGraph: true does not duplicate results already in top-k', async () => {
73
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-graph-dedup-'));
74
+ const indexDir = await buildTestIndex(tmpDir);
75
+ const bookLibDir = path.dirname(indexDir);
76
+ writeEdge(bookLibDir, {
77
+ from: 'effective-kotlin', to: 'effective-kotlin', type: 'see-also', weight: 1.0, created: '2026-03-31',
78
+ });
79
+ const searcher = new BookLibSearcher(indexDir);
80
+ const results = await searcher.search('kotlin immutable', 5, 0, { useGraph: true });
81
+ const names = results.map(r => r.metadata?.name).filter(Boolean);
82
+ const unique = new Set(names);
83
+ assert.strictEqual(unique.size, names.length, 'no duplicate skill names in results');
84
+ });
85
+
86
+ test('useGraph: true ignores non-discovery edge types (contradicts)', async () => {
87
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-graph-edgetype-'));
88
+ const indexDir = await buildTestIndex(tmpDir);
89
+ const bookLibDir = path.dirname(indexDir);
90
+ writeEdge(bookLibDir, {
91
+ from: 'effective-kotlin', to: 'design-patterns', type: 'contradicts', weight: 1.0, created: '2026-03-31',
92
+ });
93
+ const searcher = new BookLibSearcher(indexDir);
94
+ const results = await searcher.search('kotlin immutable', 5, 0, { useGraph: true });
95
+ const graphResults = results.filter(r => r.metadata?.source === 'graph');
96
+ assert.strictEqual(graphResults.length, 0, 'contradicts edge should not produce graph-linked results');
97
+ });
98
+
99
+ test('useGraph: true returns normal results when graph.jsonl is absent', async () => {
100
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-graph-nofile-'));
101
+ const indexDir = await buildTestIndex(tmpDir);
102
+ const searcher = new BookLibSearcher(indexDir);
103
+ const results = await searcher.search('kotlin immutable', 5, 0, { useGraph: true });
104
+ assert.ok(results.length > 0, 'should return normal results');
105
+ assert.ok(results.every(r => r.metadata?.source !== 'graph'), 'no graph results when file absent');
106
+ });
107
+ });
@@ -0,0 +1,44 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { findOwningComponents, scoreAndRankNodes } from '../../lib/engine/graph-injector.js';
4
+
5
+ test('findOwningComponents matches file path against component glob', () => {
6
+ const components = [
7
+ { id: 'comp_auth', paths: ['src/auth/**'], title: 'Auth' },
8
+ { id: 'comp_pay', paths: ['src/payments/**'], title: 'Payments' },
9
+ ];
10
+ const result = findOwningComponents('src/auth/middleware.js', components);
11
+ assert.equal(result.length, 1);
12
+ assert.equal(result[0].id, 'comp_auth');
13
+ });
14
+
15
+ test('findOwningComponents returns empty array when no match', () => {
16
+ const components = [
17
+ { id: 'comp_auth', paths: ['src/auth/**'], title: 'Auth' },
18
+ ];
19
+ const result = findOwningComponents('src/payments/stripe.js', components);
20
+ assert.equal(result.length, 0);
21
+ });
22
+
23
+ test('findOwningComponents matches multiple components', () => {
24
+ const components = [
25
+ { id: 'comp_auth', paths: ['src/auth/**', '**/middleware*'], title: 'Auth' },
26
+ { id: 'comp_core', paths: ['src/**'], title: 'Core' },
27
+ ];
28
+ const result = findOwningComponents('src/auth/util.js', components);
29
+ const ids = result.map(c => c.id);
30
+ assert.ok(ids.includes('comp_auth'));
31
+ assert.ok(ids.includes('comp_core'));
32
+ });
33
+
34
+ test('scoreAndRankNodes deduplicates by id keeping highest score', () => {
35
+ const nodes = [
36
+ { id: 'a', score: 0.9, text: 'foo', hop: 0 },
37
+ { id: 'a', score: 0.8, text: 'foo', hop: 1 },
38
+ { id: 'b', score: 0.7, text: 'bar', hop: 0 },
39
+ ];
40
+ const ranked = scoreAndRankNodes(nodes);
41
+ assert.equal(ranked.filter(n => n.id === 'a').length, 1);
42
+ assert.equal(ranked[0].id, 'a');
43
+ assert.equal(ranked[0].score, 0.9);
44
+ });
@@ -0,0 +1,216 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path, { join } from 'node:path';
6
+ import {
7
+ resolveKnowledgePaths,
8
+ generateNodeId,
9
+ serializeNode,
10
+ saveNode,
11
+ loadNode,
12
+ parseNodeFrontmatter,
13
+ listNodes,
14
+ appendEdge,
15
+ loadEdges,
16
+ traverseEdges,
17
+ resolveNodeRef,
18
+ } from '../../lib/engine/graph.js';
19
+
20
+ function tmpDir() {
21
+ return mkdtempSync(join(tmpdir(), 'booklib-graph-test-'));
22
+ }
23
+
24
+ test('generateNodeId returns prefixed hex string', () => {
25
+ const id = generateNodeId('node');
26
+ assert.match(id, /^node_[a-f0-9]{8}$/);
27
+ });
28
+
29
+ test('generateNodeId with component prefix', () => {
30
+ const id = generateNodeId('comp');
31
+ assert.match(id, /^comp_[a-f0-9]{8}$/);
32
+ });
33
+
34
+ test('serializeNode produces valid frontmatter markdown', () => {
35
+ const md = serializeNode({
36
+ id: 'node_abc',
37
+ type: 'research',
38
+ title: 'JWT patterns',
39
+ content: 'Some findings.',
40
+ tags: ['auth', 'jwt'],
41
+ sources: ['https://example.com'],
42
+ confidence: 'high',
43
+ });
44
+ assert.ok(md.startsWith('---\n'));
45
+ assert.ok(md.includes('node_abc'));
46
+ assert.ok(md.includes('research'));
47
+ assert.ok(md.includes('JWT patterns'));
48
+ assert.ok(md.includes('Some findings.'));
49
+ });
50
+
51
+ test('saveNode writes file and loadNode reads it back', () => {
52
+ const dir = tmpDir();
53
+ const nodesDir = join(dir, 'nodes');
54
+ const id = 'node_test01';
55
+ const content = serializeNode({ id, type: 'note', title: 'Test', content: 'hello' });
56
+ const filePath = saveNode(content, id, { nodesDir });
57
+ assert.ok(existsSync(filePath));
58
+ const loaded = loadNode(id, { nodesDir });
59
+ assert.equal(loaded, content);
60
+ rmSync(dir, { recursive: true });
61
+ });
62
+
63
+ test('loadNode returns null for missing node', () => {
64
+ const dir = tmpDir();
65
+ const result = loadNode('nonexistent', { nodesDir: join(dir, 'nodes') });
66
+ assert.equal(result, null);
67
+ rmSync(dir, { recursive: true });
68
+ });
69
+
70
+ test('parseNodeFrontmatter extracts fields and body', () => {
71
+ const md = `---\nid: node_x\ntype: research\ntitle: JWT\ntags:\n - auth\n---\n\nContent here.`;
72
+ const parsed = parseNodeFrontmatter(md);
73
+ assert.equal(parsed.id, 'node_x');
74
+ assert.equal(parsed.type, 'research');
75
+ assert.equal(parsed.title, 'JWT');
76
+ assert.deepEqual(parsed.tags, ['auth']);
77
+ assert.equal(parsed.body, 'Content here.');
78
+ });
79
+
80
+ test('listNodes returns all node IDs', () => {
81
+ const dir = tmpDir();
82
+ const nodesDir = join(dir, 'nodes');
83
+ saveNode(serializeNode({ id: 'node_a1b2c3d4', type: 'note', title: 'A', content: '' }), 'node_a1b2c3d4', { nodesDir });
84
+ saveNode(serializeNode({ id: 'node_b2c3d4e5', type: 'note', title: 'B', content: '' }), 'node_b2c3d4e5', { nodesDir });
85
+ const ids = listNodes({ nodesDir });
86
+ assert.ok(ids.includes('node_a1b2c3d4'));
87
+ assert.ok(ids.includes('node_b2c3d4e5'));
88
+ rmSync(dir, { recursive: true });
89
+ });
90
+
91
+ test('appendEdge writes edge to graph.jsonl', () => {
92
+ const dir = tmpDir();
93
+ const graphFile = join(dir, 'graph.jsonl');
94
+ const edge = { from: 'node_a', to: 'node_b', type: 'implements', weight: 1.0 };
95
+ appendEdge(edge, { graphFile });
96
+ const lines = readFileSync(graphFile, 'utf8').trim().split('\n');
97
+ assert.equal(lines.length, 1);
98
+ assert.deepEqual(JSON.parse(lines[0]), edge);
99
+ rmSync(dir, { recursive: true });
100
+ });
101
+
102
+ test('loadEdges returns all edges from graph.jsonl', () => {
103
+ const dir = tmpDir();
104
+ const graphFile = join(dir, 'graph.jsonl');
105
+ appendEdge({ from: 'a', to: 'b', type: 'see-also', weight: 1.0 }, { graphFile });
106
+ appendEdge({ from: 'b', to: 'c', type: 'extends', weight: 0.8 }, { graphFile });
107
+ const edges = loadEdges({ graphFile });
108
+ assert.equal(edges.length, 2);
109
+ assert.equal(edges[0].from, 'a');
110
+ assert.equal(edges[1].from, 'b');
111
+ rmSync(dir, { recursive: true });
112
+ });
113
+
114
+ test('loadEdges returns empty array when file missing', () => {
115
+ const dir = tmpDir();
116
+ const edges = loadEdges({ graphFile: join(dir, 'missing.jsonl') });
117
+ assert.deepEqual(edges, []);
118
+ rmSync(dir, { recursive: true });
119
+ });
120
+
121
+ test('traverseEdges finds 1-hop neighbours', () => {
122
+ const edges = [
123
+ { from: 'a', to: 'b', type: 'implements', weight: 1.0 },
124
+ { from: 'a', to: 'c', type: 'see-also', weight: 0.9 },
125
+ { from: 'd', to: 'e', type: 'extends', weight: 0.8 },
126
+ ];
127
+ const result = traverseEdges('a', edges, 1);
128
+ const ids = result.map(r => r.id);
129
+ assert.ok(ids.includes('b'));
130
+ assert.ok(ids.includes('c'));
131
+ assert.ok(!ids.includes('d'));
132
+ });
133
+
134
+ test('traverseEdges does not revisit nodes (no infinite cycles)', () => {
135
+ const edges = [
136
+ { from: 'a', to: 'b', type: 'see-also', weight: 1.0 },
137
+ { from: 'b', to: 'a', type: 'see-also', weight: 1.0 },
138
+ ];
139
+ const result = traverseEdges('a', edges, 3);
140
+ const ids = result.map(r => r.id);
141
+ assert.ok(!ids.includes('a'));
142
+ });
143
+
144
+ test('traverseEdges follows edges in both directions', () => {
145
+ const edges = [
146
+ { from: 'x', to: 'target', type: 'applies-to', weight: 1.0 },
147
+ ];
148
+ const result = traverseEdges('target', edges, 1);
149
+ const ids = result.map(r => r.id);
150
+ assert.ok(ids.includes('x'));
151
+ });
152
+
153
+ test('serializeNode preserves body content passed via stdin path', async (t) => {
154
+ const result = serializeNode({ id: 'node_test01', type: 'note', title: 'My note', content: 'body text here' });
155
+ assert.ok(result.includes('body text here'), 'body text is in the serialized node');
156
+ assert.ok(result.includes('title: My note'), 'title is in frontmatter');
157
+ });
158
+
159
+ test('resolveNodeRef — returns exact ID unchanged', (t) => {
160
+ const tmpNodeDir = mkdtempSync(path.join(tmpdir(), 'ref-test-'));
161
+ const opts = { nodesDir: tmpNodeDir };
162
+ const id = 'node_aaaabbbb';
163
+ saveNode(serializeNode({ id, type: 'note', title: 'Auth decisions' }), id, opts);
164
+ const result = resolveNodeRef(id, opts);
165
+ assert.strictEqual(result, id);
166
+ rmSync(tmpNodeDir, { recursive: true });
167
+ });
168
+
169
+ test('resolveNodeRef — finds node by partial title', (t) => {
170
+ const tmpNodeDir = mkdtempSync(path.join(tmpdir(), 'ref-test2-'));
171
+ const opts = { nodesDir: tmpNodeDir };
172
+ const id = 'node_ccccdddd';
173
+ saveNode(serializeNode({ id, type: 'note', title: 'JWT refresh patterns' }), id, opts);
174
+ const result = resolveNodeRef('jwt refresh', opts);
175
+ assert.strictEqual(result, id);
176
+ rmSync(tmpNodeDir, { recursive: true });
177
+ });
178
+
179
+ test('resolveNodeRef — throws when no match found', async (t) => {
180
+ const { resolveNodeRef } = await import('../../lib/engine/graph.js');
181
+ const tmpNodeDir = mkdtempSync(path.join(tmpdir(), 'ref-err-test-'));
182
+ assert.throws(
183
+ () => resolveNodeRef('nonexistent query', { nodesDir: tmpNodeDir }),
184
+ /No node found/
185
+ );
186
+ rmSync(tmpNodeDir, { recursive: true });
187
+ });
188
+
189
+ test('resolveNodeRef — throws with match list when ambiguous', async (t) => {
190
+ const { resolveNodeRef, saveNode, serializeNode } = await import('../../lib/engine/graph.js');
191
+ const tmpNodeDir = mkdtempSync(path.join(tmpdir(), 'ref-ambig-test-'));
192
+ const opts = { nodesDir: tmpNodeDir };
193
+ saveNode(serializeNode({ id: 'node_auth01', type: 'note', title: 'Auth session handling' }), 'node_auth01', opts);
194
+ saveNode(serializeNode({ id: 'node_auth02', type: 'note', title: 'Auth token refresh' }), 'node_auth02', opts);
195
+ assert.throws(
196
+ () => resolveNodeRef('auth', opts),
197
+ /Multiple nodes match/
198
+ );
199
+ rmSync(tmpNodeDir, { recursive: true });
200
+ });
201
+
202
+ test('research success message does not tell user to manually re-run index', async (t) => {
203
+ const { readFileSync } = await import('node:fs');
204
+ const src = readFileSync(new URL('../../bin/booklib.js', import.meta.url), 'utf8');
205
+ const staleMessage = 'run booklib index to update the search index';
206
+ assert.ok(!src.includes(staleMessage), 'stale re-index message removed from research case');
207
+ });
208
+
209
+ test('bin/booklib.js contains component hint for --file with no graph context', async (t) => {
210
+ const { readFileSync } = await import('node:fs');
211
+ const src = readFileSync(new URL('../../bin/booklib.js', import.meta.url), 'utf8');
212
+ assert.ok(
213
+ src.includes('booklib component add'),
214
+ 'bin/booklib.js contains component hint for --file with no graph context'
215
+ );
216
+ });
@@ -0,0 +1,74 @@
1
+ // tests/engine/hybrid-searcher.test.js
2
+ import { describe, it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import { BookLibIndexer } from '../../lib/engine/indexer.js';
8
+ import { BookLibSearcher } from '../../lib/engine/searcher.js';
9
+
10
+ async function buildTestIndex(tmpDir) {
11
+ const indexDir = path.join(tmpDir, 'index');
12
+ const skillsDir = path.join(tmpDir, 'skills');
13
+
14
+ const skills = [
15
+ { name: 'effective-kotlin', text: 'Kotlin null safety val immutable data class sealed class' },
16
+ { name: 'effective-typescript', text: 'TypeScript strict null checks undefined type narrowing' },
17
+ { name: 'clean-code-reviewer', text: 'clean code naming functions variables single responsibility' },
18
+ { name: 'effective-java', text: 'Java generics builder pattern equals hashCode immutable' },
19
+ ];
20
+
21
+ for (const skill of skills) {
22
+ const dir = path.join(skillsDir, skill.name);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), `---
25
+ name: ${skill.name}
26
+ description: Test skill
27
+ version: "1.0"
28
+ tags: [test]
29
+ license: MIT
30
+ ---
31
+ ${skill.text}
32
+ `);
33
+ }
34
+
35
+ const indexer = new BookLibIndexer(indexDir);
36
+ await indexer.indexDirectory(skillsDir, true, { quiet: true });
37
+ return indexDir;
38
+ }
39
+
40
+ describe('BookLibSearcher hybrid pipeline', () => {
41
+ it('returns results for a query with bm25.json present', async () => {
42
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-hybrid-'));
43
+ const indexDir = await buildTestIndex(tmpDir);
44
+ const searcher = new BookLibSearcher(indexDir);
45
+
46
+ const results = await searcher.search('kotlin null safety', 3, 0);
47
+ assert.ok(results.length > 0, 'should return results');
48
+ assert.ok(results[0].score !== undefined);
49
+ assert.ok(results[0].text !== undefined);
50
+ assert.ok(results[0].metadata !== undefined);
51
+ });
52
+
53
+ it('falls back to vector search when bm25.json is missing', async () => {
54
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-noBm25-'));
55
+ const indexDir = await buildTestIndex(tmpDir);
56
+
57
+ const bm25Path = path.join(path.dirname(indexDir), 'bm25.json');
58
+ if (fs.existsSync(bm25Path)) fs.unlinkSync(bm25Path);
59
+
60
+ const searcher = new BookLibSearcher(indexDir);
61
+ const results = await searcher.search('kotlin null safety', 3, 0);
62
+ assert.ok(results.length > 0, 'should fall back to vector search');
63
+ });
64
+
65
+ it('top result is relevant to query', async () => {
66
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-relevance-'));
67
+ const indexDir = await buildTestIndex(tmpDir);
68
+ const searcher = new BookLibSearcher(indexDir);
69
+
70
+ const results = await searcher.search('kotlin immutable data', 5, 0);
71
+ assert.ok(results.length > 0);
72
+ assert.strictEqual(results[0].metadata.name, 'effective-kotlin');
73
+ });
74
+ });
@@ -0,0 +1,37 @@
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 { BookLibIndexer } from '../../lib/engine/indexer.js';
7
+ import { BM25Index } from '../../lib/engine/bm25-index.js';
8
+
9
+ describe('BookLibIndexer BM25 co-build', () => {
10
+ it('creates bm25.json alongside vectra index after indexDirectory', async () => {
11
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-indexer-'));
12
+ const indexDir = path.join(tmpDir, 'index');
13
+ const skillsDir = path.join(tmpDir, 'skills');
14
+ const testSkillDir = path.join(skillsDir, 'test-skill');
15
+ fs.mkdirSync(testSkillDir, { recursive: true });
16
+ fs.writeFileSync(path.join(testSkillDir, 'SKILL.md'), `---
17
+ name: test-skill
18
+ description: A test skill about kotlin null safety
19
+ version: "1.0"
20
+ tags: [kotlin]
21
+ license: MIT
22
+ ---
23
+ Kotlin null safety prevents null pointer exceptions.
24
+ `);
25
+
26
+ const indexer = new BookLibIndexer(indexDir);
27
+ await indexer.indexDirectory(skillsDir, true, { quiet: true });
28
+
29
+ const bm25Path = path.join(path.dirname(indexDir), 'bm25.json');
30
+ assert.ok(fs.existsSync(bm25Path), 'bm25.json should exist after indexDirectory');
31
+
32
+ const idx = BM25Index.load(bm25Path);
33
+ const results = idx.search('kotlin null', 3);
34
+ assert.ok(results.length > 0, 'loaded BM25 index should return results');
35
+ assert.ok(results[0].score > 0);
36
+ });
37
+ });
@@ -0,0 +1,73 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import { mkdtempSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ // ── get_context ───────────────────────────────────────────────────────────────
8
+
9
+ test('ContextBuilder.build returns a non-empty string for a valid task', async (t) => {
10
+ const { ContextBuilder } = await import('../../lib/context-builder.js');
11
+ const builder = new ContextBuilder();
12
+ const result = await builder.build('implement null safety in Kotlin');
13
+ assert.ok(typeof result === 'string', 'result is a string');
14
+ assert.ok(result.length > 50, 'result has meaningful content');
15
+ });
16
+
17
+ test('ContextBuilder.buildWithGraph returns skill context even with no file', async (t) => {
18
+ const { ContextBuilder } = await import('../../lib/context-builder.js');
19
+ const builder = new ContextBuilder();
20
+ const result = await builder.buildWithGraph('implement null safety in Kotlin', null);
21
+ assert.ok(typeof result === 'string', 'result is a string');
22
+ assert.ok(result.length > 50, 'result has meaningful content');
23
+ });
24
+
25
+ // ── create_note ───────────────────────────────────────────────────────────────
26
+
27
+ test('serializeNode + saveNode creates a readable note node', async (t) => {
28
+ const { serializeNode, saveNode, loadNode, parseNodeFrontmatter, generateNodeId } = await import('../../lib/engine/graph.js');
29
+ const tmpDir = mkdtempSync(path.join(tmpdir(), 'mcp-note-'));
30
+ const id = generateNodeId('node');
31
+ const content = serializeNode({ id, type: 'note', title: 'MCP test note', content: 'body text' });
32
+ const filePath = saveNode(content, id, { nodesDir: tmpDir });
33
+ const raw = loadNode(id, { nodesDir: tmpDir });
34
+ const parsed = parseNodeFrontmatter(raw);
35
+ assert.strictEqual(parsed.title, 'MCP test note');
36
+ assert.ok(parsed.body.includes('body text'));
37
+ });
38
+
39
+ // ── search_knowledge ─────────────────────────────────────────────────────────
40
+
41
+ test('BookLibSearcher.search returns an array (empty or populated)', async (t) => {
42
+ const { BookLibSearcher } = await import('../../lib/engine/searcher.js');
43
+ const searcher = new BookLibSearcher();
44
+ try {
45
+ const results = await searcher.search('null safety', 3);
46
+ assert.ok(Array.isArray(results), 'results is an array');
47
+ } catch (err) {
48
+ // Index not built in test env — acceptable
49
+ assert.ok(err.message.includes('booklib index'), 'informative error message');
50
+ }
51
+ });
52
+
53
+ // ── list_nodes + link_nodes ───────────────────────────────────────────────────
54
+
55
+ test('listNodes returns array of IDs from a temp dir', async (t) => {
56
+ const { listNodes, saveNode, serializeNode, generateNodeId } = await import('../../lib/engine/graph.js');
57
+ const tmpDir = mkdtempSync(path.join(tmpdir(), 'mcp-list-'));
58
+ const id = generateNodeId('node');
59
+ saveNode(serializeNode({ id, type: 'note', title: 'List test' }), id, { nodesDir: tmpDir });
60
+ const ids = listNodes({ nodesDir: tmpDir });
61
+ assert.ok(ids.includes(id), 'saved node appears in list');
62
+ });
63
+
64
+ test('resolveNodeRef resolves partial title to node ID in temp dir', async (t) => {
65
+ const { saveNode, serializeNode, generateNodeId, resolveNodeRef } = await import('../../lib/engine/graph.js');
66
+ const tmpDir = mkdtempSync(path.join(tmpdir(), 'mcp-link-'));
67
+ const idA = generateNodeId('node');
68
+ const idB = generateNodeId('node');
69
+ saveNode(serializeNode({ id: idA, type: 'note', title: 'Source note' }), idA, { nodesDir: tmpDir });
70
+ saveNode(serializeNode({ id: idB, type: 'note', title: 'Auth component' }), idB, { nodesDir: tmpDir });
71
+ const resolvedA = resolveNodeRef('Source note', { nodesDir: tmpDir });
72
+ assert.strictEqual(resolvedA, idA);
73
+ });
@@ -0,0 +1,99 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { mkdtempSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { ProjectInitializer } from '../../lib/project-initializer.js';
8
+
9
+ function makeInit(dir) {
10
+ return new ProjectInitializer({ projectCwd: dir });
11
+ }
12
+
13
+ // ── Per-tool output ───────────────────────────────────────────────────────────
14
+
15
+ test('generateMcpConfigs writes .claude/settings.json for claude', async () => {
16
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
17
+ await makeInit(dir).generateMcpConfigs({ tools: ['claude'] });
18
+ const settings = JSON.parse(fs.readFileSync(path.join(dir, '.claude', 'settings.json'), 'utf8'));
19
+ assert.deepStrictEqual(settings.mcpServers.booklib, { command: 'booklib-mcp', args: [] });
20
+ fs.rmSync(dir, { recursive: true });
21
+ });
22
+
23
+ test('generateMcpConfigs writes .cursor/mcp.json for cursor', async () => {
24
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
25
+ await makeInit(dir).generateMcpConfigs({ tools: ['cursor'] });
26
+ const mcp = JSON.parse(fs.readFileSync(path.join(dir, '.cursor', 'mcp.json'), 'utf8'));
27
+ assert.deepStrictEqual(mcp.mcpServers.booklib, { command: 'booklib-mcp', args: [] });
28
+ fs.rmSync(dir, { recursive: true });
29
+ });
30
+
31
+ test('generateMcpConfigs writes .gemini/settings.json for gemini', async () => {
32
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
33
+ await makeInit(dir).generateMcpConfigs({ tools: ['gemini'] });
34
+ const settings = JSON.parse(fs.readFileSync(path.join(dir, '.gemini', 'settings.json'), 'utf8'));
35
+ assert.deepStrictEqual(settings.mcpServers.booklib, { command: 'booklib-mcp', args: [] });
36
+ fs.rmSync(dir, { recursive: true });
37
+ });
38
+
39
+ test('generateMcpConfigs writes .codex/config.toml for codex', async () => {
40
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
41
+ await makeInit(dir).generateMcpConfigs({ tools: ['codex'] });
42
+ const toml = fs.readFileSync(path.join(dir, '.codex', 'config.toml'), 'utf8');
43
+ assert.ok(toml.includes('[mcp_servers.booklib]'), 'has section header');
44
+ assert.ok(toml.includes('command = "booklib-mcp"'), 'has command');
45
+ assert.ok(toml.includes('args = []'), 'has args');
46
+ fs.rmSync(dir, { recursive: true });
47
+ });
48
+
49
+ test('generateMcpConfigs writes .zed/settings.json for zed', async () => {
50
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
51
+ await makeInit(dir).generateMcpConfigs({ tools: ['zed'] });
52
+ const settings = JSON.parse(fs.readFileSync(path.join(dir, '.zed', 'settings.json'), 'utf8'));
53
+ assert.deepStrictEqual(
54
+ settings['context_servers']['booklib-mcp'],
55
+ { command: { path: 'booklib-mcp', args: [] } }
56
+ );
57
+ fs.rmSync(dir, { recursive: true });
58
+ });
59
+
60
+ test('generateMcpConfigs writes .continue/mcpServers/booklib.yaml for continue', async () => {
61
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
62
+ await makeInit(dir).generateMcpConfigs({ tools: ['continue'] });
63
+ const yaml = fs.readFileSync(path.join(dir, '.continue', 'mcpServers', 'booklib.yaml'), 'utf8');
64
+ assert.ok(yaml.includes('name: booklib'), 'has name');
65
+ assert.ok(yaml.includes('command: booklib-mcp'), 'has command');
66
+ assert.ok(yaml.includes('args: []'), 'has args');
67
+ fs.rmSync(dir, { recursive: true });
68
+ });
69
+
70
+ // ── Merge behaviour ───────────────────────────────────────────────────────────
71
+
72
+ test('generateMcpConfigs merges into existing JSON without overwriting other servers', async () => {
73
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
74
+ const claudeDir = path.join(dir, '.claude');
75
+ fs.mkdirSync(claudeDir, { recursive: true });
76
+ const existing = { mcpServers: { 'other-server': { command: 'other', args: [] } } };
77
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(existing));
78
+
79
+ await makeInit(dir).generateMcpConfigs({ tools: ['claude'] });
80
+
81
+ const settings = JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf8'));
82
+ assert.deepStrictEqual(settings.mcpServers['other-server'], { command: 'other', args: [] });
83
+ assert.deepStrictEqual(settings.mcpServers.booklib, { command: 'booklib-mcp', args: [] });
84
+ fs.rmSync(dir, { recursive: true });
85
+ });
86
+
87
+ test('generateMcpConfigs appends booklib section into existing TOML without altering other content', async () => {
88
+ const dir = mkdtempSync(path.join(tmpdir(), 'bl-mcp-'));
89
+ const codexDir = path.join(dir, '.codex');
90
+ fs.mkdirSync(codexDir, { recursive: true });
91
+ fs.writeFileSync(path.join(codexDir, 'config.toml'), '[other_service]\nkey = "value"\n');
92
+
93
+ await makeInit(dir).generateMcpConfigs({ tools: ['codex'] });
94
+
95
+ const toml = fs.readFileSync(path.join(codexDir, 'config.toml'), 'utf8');
96
+ assert.ok(toml.includes('[other_service]'), 'preserves existing content');
97
+ assert.ok(toml.includes('[mcp_servers.booklib]'), 'appends booklib section');
98
+ fs.rmSync(dir, { recursive: true });
99
+ });