@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,36 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { expandQuery, extractKeywords } from '../../lib/engine/query-expander.js';
4
+
5
+ describe('expandQuery', () => {
6
+ it('returns original query in result', () => {
7
+ assert.strictEqual(expandQuery('null safety kotlin').original, 'null safety kotlin');
8
+ });
9
+
10
+ it('returns at least one expanded query variant', () => {
11
+ const result = expandQuery('null safety kotlin');
12
+ assert.ok(result.expanded.length >= 1);
13
+ });
14
+
15
+ it('keywords strips stopwords', () => {
16
+ const result = expandQuery('how to handle null values in Kotlin');
17
+ assert.ok(!result.keywords.includes('how'));
18
+ assert.ok(!result.keywords.includes('to'));
19
+ assert.ok(!result.keywords.includes('in'));
20
+ assert.ok(result.keywords.includes('handle'));
21
+ assert.ok(result.keywords.includes('null'));
22
+ assert.ok(result.keywords.includes('values'));
23
+ assert.ok(result.keywords.includes('kotlin'));
24
+ });
25
+
26
+ it('handles single-word query without error', () => {
27
+ const result = expandQuery('kotlin');
28
+ assert.strictEqual(result.original, 'kotlin');
29
+ assert.ok(result.expanded.length >= 1);
30
+ });
31
+
32
+ it('expanded variants do not include the original query verbatim', () => {
33
+ const result = expandQuery('null safety kotlin');
34
+ assert.ok(!result.expanded.includes('null safety kotlin'));
35
+ });
36
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Reranker } from '../../lib/engine/reranker.js';
4
+
5
+ const CANDIDATES = [
6
+ { score: 0.5, text: 'kotlin null safety avoid exceptions', metadata: { name: 'effective-kotlin' } },
7
+ { score: 0.6, text: 'java null pointer best practices', metadata: { name: 'effective-java' } },
8
+ { score: 0.4, text: 'typescript undefined checking', metadata: { name: 'effective-typescript' } },
9
+ ];
10
+
11
+ function makeRerankerWithMock(scoreFn) {
12
+ const reranker = new Reranker();
13
+ let callIndex = 0;
14
+ reranker._pipeline = async (inputs) => {
15
+ return inputs.map(() => [{ label: 'LABEL_1', score: scoreFn(callIndex++) }]);
16
+ };
17
+ return reranker;
18
+ }
19
+
20
+ describe('Reranker', () => {
21
+ it('returns same number of candidates', async () => {
22
+ const reranker = makeRerankerWithMock(() => 0.5);
23
+ const result = await reranker.rerank('null safety', CANDIDATES);
24
+ assert.strictEqual(result.length, CANDIDATES.length);
25
+ });
26
+
27
+ it('sorts results by reranker score descending', async () => {
28
+ const scores = [0.9, 0.3, 0.6];
29
+ const reranker = makeRerankerWithMock((i) => scores[i]);
30
+ const result = await reranker.rerank('null safety', CANDIDATES);
31
+ for (let i = 1; i < result.length; i++) {
32
+ assert.ok(result[i - 1].score >= result[i].score);
33
+ }
34
+ });
35
+
36
+ it('preserves text and metadata on each result', async () => {
37
+ const reranker = makeRerankerWithMock(() => 0.5);
38
+ const result = await reranker.rerank('null safety', CANDIDATES);
39
+ const names = result.map(r => r.metadata.name);
40
+ assert.ok(names.includes('effective-kotlin'));
41
+ assert.ok(names.includes('effective-java'));
42
+ assert.ok(names.includes('effective-typescript'));
43
+ });
44
+
45
+ it('returns empty array for empty candidates without loading model', async () => {
46
+ const reranker = new Reranker();
47
+ // No _pipeline set — would throw if accessed
48
+ const result = await reranker.rerank('query', []);
49
+ assert.deepEqual(result, []);
50
+ });
51
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { reciprocalRankFusion } from '../../lib/engine/rrf.js';
4
+
5
+ const makeList = (...names) => names.map((name, i) => ({
6
+ score: 1 - i * 0.1,
7
+ text: `text for ${name}`,
8
+ metadata: { name },
9
+ }));
10
+
11
+ describe('reciprocalRankFusion', () => {
12
+ it('merges two lists and returns items from both', () => {
13
+ const result = reciprocalRankFusion([makeList('a','b','c'), makeList('b','c','d')]);
14
+ const names = result.map(r => r.metadata.name);
15
+ assert.ok(names.includes('a') && names.includes('b') && names.includes('c') && names.includes('d'));
16
+ });
17
+
18
+ it('items appearing in multiple lists score higher than single-list items', () => {
19
+ const listA = makeList('shared', 'unique-a');
20
+ const listB = makeList('shared', 'unique-b');
21
+ const result = reciprocalRankFusion([listA, listB]);
22
+ const sharedScore = result.find(r => r.metadata.name === 'shared').score;
23
+ const uniqueAScore = result.find(r => r.metadata.name === 'unique-a').score;
24
+ const uniqueBScore = result.find(r => r.metadata.name === 'unique-b').score;
25
+ assert.ok(sharedScore > uniqueAScore && sharedScore > uniqueBScore);
26
+ });
27
+
28
+ it('respects weights — higher weight list contributes more', () => {
29
+ const result = reciprocalRankFusion([makeList('only-in-a'), makeList('only-in-b')], { weights: [2, 1] });
30
+ const scoreA = result.find(r => r.metadata.name === 'only-in-a').score;
31
+ const scoreB = result.find(r => r.metadata.name === 'only-in-b').score;
32
+ assert.ok(scoreA > scoreB);
33
+ });
34
+
35
+ it('deduplicates items that appear in multiple lists', () => {
36
+ const result = reciprocalRankFusion([makeList('dup','x'), makeList('dup','y')]);
37
+ assert.strictEqual(result.filter(r => r.metadata.name === 'dup').length, 1);
38
+ });
39
+
40
+ it('returns results sorted by score descending', () => {
41
+ const result = reciprocalRankFusion([makeList('a','b','c'), makeList('c','b','a')]);
42
+ for (let i = 1; i < result.length; i++) assert.ok(result[i-1].score >= result[i].score);
43
+ });
44
+
45
+ it('handles empty input lists', () => {
46
+ assert.deepEqual(reciprocalRankFusion([]), []);
47
+ assert.deepEqual(reciprocalRankFusion([[]]), []);
48
+ });
49
+ });
@@ -0,0 +1,47 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildMetadataPrefix } from '../../lib/engine/indexer.js';
4
+
5
+ describe('buildMetadataPrefix', () => {
6
+ test('returns prefix with skill name, type, and tags', () => {
7
+ const prefix = buildMetadataPrefix({
8
+ name: 'effective-kotlin',
9
+ type: 'anti_patterns',
10
+ tags: ['kotlin', 'jvm'],
11
+ });
12
+ assert.strictEqual(prefix, '[skill:effective-kotlin] [type:anti_patterns] [tags:kotlin,jvm] ');
13
+ });
14
+
15
+ test('returns prefix with name only when type and tags are missing', () => {
16
+ const prefix = buildMetadataPrefix({ name: 'clean-code-reviewer' });
17
+ assert.strictEqual(prefix, '[skill:clean-code-reviewer] ');
18
+ });
19
+
20
+ test('returns empty string when metadata has no relevant fields', () => {
21
+ const prefix = buildMetadataPrefix({ filePath: 'some/path.md' });
22
+ assert.strictEqual(prefix, '');
23
+ });
24
+
25
+ test('omits tags bracket when tags array is empty', () => {
26
+ const prefix = buildMetadataPrefix({ name: 'effective-java', type: 'summary', tags: [] });
27
+ assert.strictEqual(prefix, '[skill:effective-java] [type:summary] ');
28
+ });
29
+
30
+ test('handles knowledge node metadata (title instead of name)', () => {
31
+ const prefix = buildMetadataPrefix({
32
+ title: 'Null Object Pattern',
33
+ type: 'insight',
34
+ tags: ['kotlin', 'patterns'],
35
+ });
36
+ assert.strictEqual(prefix, '[skill:Null Object Pattern] [type:insight] [tags:kotlin,patterns] ');
37
+ });
38
+
39
+ test('prefers name over title when both present', () => {
40
+ const prefix = buildMetadataPrefix({
41
+ name: 'effective-kotlin',
42
+ title: 'Some Title',
43
+ type: 'summary',
44
+ });
45
+ assert.strictEqual(prefix, '[skill:effective-kotlin] [type:summary] ');
46
+ });
47
+ });
@@ -0,0 +1,23 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { renderInstinctBlock } from '../lib/instinct-block.js';
4
+
5
+ test('MCP-capable tool gets MCP tool names', () => {
6
+ const block = renderInstinctBlock('claude');
7
+ assert.ok(block.includes('search_skills'), 'should reference MCP tool name');
8
+ assert.ok(block.includes('create_note'), 'should reference create_note');
9
+ assert.ok(!block.includes('booklib search'), 'should not reference CLI command');
10
+ });
11
+
12
+ test('non-MCP tool gets CLI commands', () => {
13
+ const block = renderInstinctBlock('junie');
14
+ assert.ok(block.includes('booklib search'), 'should reference CLI command');
15
+ assert.ok(!block.includes('search_skills'), 'should not reference MCP tool name');
16
+ });
17
+
18
+ test('all MCP tools get MCP version', () => {
19
+ for (const tool of ['claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf', 'roo-code', 'goose']) {
20
+ const block = renderInstinctBlock(tool);
21
+ assert.ok(block.includes('search_skills'), `${tool} should get MCP version`);
22
+ }
23
+ });
@@ -0,0 +1,60 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { writeMCPConfig, MCP_CAPABLE } from '../lib/mcp-config-writer.js';
7
+
8
+ function tmpDir() {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'booklib-mcp-test-'));
10
+ }
11
+
12
+ test('writes claude MCP config with mcpServers key', () => {
13
+ const cwd = tmpDir();
14
+ writeMCPConfig('claude', cwd);
15
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
16
+ assert.ok(config.mcpServers.booklib);
17
+ assert.strictEqual(config.mcpServers.booklib.command, 'booklib-mcp');
18
+ fs.rmSync(cwd, { recursive: true });
19
+ });
20
+
21
+ test('writes copilot MCP config with servers key', () => {
22
+ const cwd = tmpDir();
23
+ writeMCPConfig('copilot', cwd);
24
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, '.vscode', 'mcp.json'), 'utf8'));
25
+ assert.ok(config.servers.booklib, 'copilot should use "servers" root key');
26
+ assert.ok(!config.mcpServers, 'copilot should NOT have mcpServers');
27
+ fs.rmSync(cwd, { recursive: true });
28
+ });
29
+
30
+ test('writes roo-code MCP config', () => {
31
+ const cwd = tmpDir();
32
+ writeMCPConfig('roo-code', cwd);
33
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, '.roo', 'mcp.json'), 'utf8'));
34
+ assert.ok(config.mcpServers.booklib);
35
+ fs.rmSync(cwd, { recursive: true });
36
+ });
37
+
38
+ test('merges with existing JSON config', () => {
39
+ const cwd = tmpDir();
40
+ const configDir = path.join(cwd, '.cursor');
41
+ fs.mkdirSync(configDir, { recursive: true });
42
+ fs.writeFileSync(path.join(configDir, 'mcp.json'), JSON.stringify({ mcpServers: { other: { command: 'other' } } }));
43
+ writeMCPConfig('cursor', cwd);
44
+ const config = JSON.parse(fs.readFileSync(path.join(configDir, 'mcp.json'), 'utf8'));
45
+ assert.ok(config.mcpServers.booklib, 'booklib should be added');
46
+ assert.ok(config.mcpServers.other, 'existing server should be preserved');
47
+ fs.rmSync(cwd, { recursive: true });
48
+ });
49
+
50
+ test('returns null for non-MCP tool', () => {
51
+ const result = writeMCPConfig('junie', '/tmp');
52
+ assert.strictEqual(result, null);
53
+ });
54
+
55
+ test('MCP_CAPABLE includes 10 tools', () => {
56
+ assert.strictEqual(MCP_CAPABLE.size, 10);
57
+ assert.ok(MCP_CAPABLE.has('copilot'));
58
+ assert.ok(MCP_CAPABLE.has('roo-code'));
59
+ assert.ok(!MCP_CAPABLE.has('junie'));
60
+ });
@@ -0,0 +1,48 @@
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 from 'node:path';
6
+ import { ProjectInitializer } from '../lib/project-initializer.js';
7
+
8
+ function tmpDir() {
9
+ return mkdtempSync(path.join(tmpdir(), 'booklib-init-test-'));
10
+ }
11
+
12
+ const NEW_AGENTS = [
13
+ { target: 'roo-code', file: '.roo/rules/booklib-standards.md' },
14
+ { target: 'openhands', file: '.openhands/instructions.md' },
15
+ { target: 'junie', file: '.junie/guidelines.md' },
16
+ { target: 'goose', file: '.goose/context.md' },
17
+ { target: 'opencode', file: '.opencode/instructions.md' },
18
+ { target: 'letta', file: '.letta/skills/booklib.md' },
19
+ ];
20
+
21
+ for (const { target, file } of NEW_AGENTS) {
22
+ test(`${target}: writes to ${file}`, async () => {
23
+ const cwd = tmpDir();
24
+ const init = new ProjectInitializer({ projectCwd: cwd });
25
+ await init.init({ skills: ['effective-kotlin'], target });
26
+ assert.ok(existsSync(path.join(cwd, file)), `Missing ${file}`);
27
+ rmSync(cwd, { recursive: true });
28
+ });
29
+
30
+ test(`${target}: written file contains booklib marker`, async () => {
31
+ const cwd = tmpDir();
32
+ const init = new ProjectInitializer({ projectCwd: cwd });
33
+ await init.init({ skills: ['effective-kotlin'], target });
34
+ const content = readFileSync(path.join(cwd, file), 'utf8');
35
+ assert.ok(content.includes('booklib-standards-start'), `Missing marker in ${file}`);
36
+ rmSync(cwd, { recursive: true });
37
+ });
38
+ }
39
+
40
+ test('ALL_TARGETS includes all new agents', async () => {
41
+ const cwd = tmpDir();
42
+ const init = new ProjectInitializer({ projectCwd: cwd });
43
+ const written = await init.init({ skills: ['effective-kotlin'], target: 'all' });
44
+ for (const { file } of NEW_AGENTS) {
45
+ assert.ok(written.includes(file), `all target missed ${file}`);
46
+ }
47
+ rmSync(cwd, { recursive: true });
48
+ });
@@ -0,0 +1,230 @@
1
+ // tests/rules/rules-manager.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import {
5
+ mkdtempSync, rmSync, mkdirSync, writeFileSync,
6
+ existsSync, readFileSync,
7
+ } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { tmpdir } from 'node:os';
10
+ import { listAvailable, installRule, status } from '../../lib/rules/rules-manager.js';
11
+
12
+ function tmp() { return mkdtempSync(join(tmpdir(), 'booklib-rules-')); }
13
+
14
+ // ─── listAvailable ───────────────────────────────────────────────────────────
15
+
16
+ test('listAvailable returns entries for each language directory', () => {
17
+ const result = listAvailable();
18
+ assert.ok(Array.isArray(result));
19
+ assert.ok(result.length > 0);
20
+ const langs = result.map(r => r.lang);
21
+ assert.ok(langs.includes('python'));
22
+ assert.ok(langs.includes('common'));
23
+ });
24
+
25
+ test('listAvailable includes files array for each lang', () => {
26
+ const result = listAvailable();
27
+ for (const item of result) {
28
+ assert.ok(Array.isArray(item.files));
29
+ assert.ok(item.files.every(f => f.endsWith('.md')));
30
+ }
31
+ });
32
+
33
+ test('listAvailable sets installedProject false when no .cursor/rules', () => {
34
+ const cwd = tmp();
35
+ const home = tmp();
36
+ const result = listAvailable(cwd, home);
37
+ for (const item of result) {
38
+ assert.equal(item.installedProject, false);
39
+ }
40
+ rmSync(cwd, { recursive: true });
41
+ rmSync(home, { recursive: true });
42
+ });
43
+
44
+ test('listAvailable sets installedProject true when mdc file found', () => {
45
+ const cwd = tmp();
46
+ const home = tmp();
47
+ mkdirSync(join(cwd, '.cursor', 'rules'), { recursive: true });
48
+ writeFileSync(join(cwd, '.cursor', 'rules', 'python-effective-python.mdc'), '# test');
49
+ const result = listAvailable(cwd, home);
50
+ const py = result.find(r => r.lang === 'python');
51
+ assert.ok(py);
52
+ assert.equal(py.installedProject, true);
53
+ rmSync(cwd, { recursive: true });
54
+ rmSync(home, { recursive: true });
55
+ });
56
+
57
+ test('listAvailable sets installedGlobal true when marker found in CLAUDE.md', () => {
58
+ const cwd = tmp();
59
+ const home = tmp();
60
+ mkdirSync(join(home, '.claude'), { recursive: true });
61
+ writeFileSync(
62
+ join(home, '.claude', 'CLAUDE.md'),
63
+ '<!-- booklib-rules-python-start -->\nsome content\n<!-- booklib-rules-python-end -->\n',
64
+ );
65
+ const result = listAvailable(cwd, home);
66
+ const py = result.find(r => r.lang === 'python');
67
+ assert.ok(py);
68
+ assert.equal(py.installedGlobal, true);
69
+ rmSync(cwd, { recursive: true });
70
+ rmSync(home, { recursive: true });
71
+ });
72
+
73
+ // ─── installRule (project) ───────────────────────────────────────────────────
74
+
75
+ test('installRule writes mdc file to .cursor/rules/', () => {
76
+ const cwd = tmp();
77
+ const home = tmp();
78
+ const written = installRule('python', { cwd, home });
79
+ assert.ok(written.length > 0);
80
+ const destFile = join(cwd, '.cursor', 'rules', 'python-effective-python.mdc');
81
+ assert.ok(existsSync(destFile));
82
+ rmSync(cwd, { recursive: true });
83
+ rmSync(home, { recursive: true });
84
+ });
85
+
86
+ test('installRule adds MDC frontmatter when absent', () => {
87
+ const cwd = tmp();
88
+ const home = tmp();
89
+ installRule('python', { cwd, home });
90
+ const destFile = join(cwd, '.cursor', 'rules', 'python-effective-python.mdc');
91
+ const content = readFileSync(destFile, 'utf8');
92
+ assert.ok(content.startsWith('---'));
93
+ assert.ok(content.includes('alwaysApply: false'));
94
+ rmSync(cwd, { recursive: true });
95
+ rmSync(home, { recursive: true });
96
+ });
97
+
98
+ test('installRule preserves existing frontmatter', () => {
99
+ const cwd = tmp();
100
+ const home = tmp();
101
+ installRule('python', { cwd, home });
102
+ const destFile = join(cwd, '.cursor', 'rules', 'python-effective-python.mdc');
103
+ const content = readFileSync(destFile, 'utf8');
104
+ const frontmatterCount = (content.match(/^---/gm) || []).length;
105
+ assert.ok(frontmatterCount <= 2, `Too many frontmatter markers: ${frontmatterCount}`);
106
+ rmSync(cwd, { recursive: true });
107
+ rmSync(home, { recursive: true });
108
+ });
109
+
110
+ test('installRule throws for unknown lang', () => {
111
+ const cwd = tmp();
112
+ const home = tmp();
113
+ assert.throws(
114
+ () => installRule('scala', { cwd, home }),
115
+ /Unknown language/,
116
+ );
117
+ rmSync(cwd, { recursive: true });
118
+ rmSync(home, { recursive: true });
119
+ });
120
+
121
+ // ─── installRule (global) ────────────────────────────────────────────────────
122
+
123
+ test('installRule global appends section to CLAUDE.md', () => {
124
+ const cwd = tmp();
125
+ const home = tmp();
126
+ mkdirSync(join(home, '.claude'), { recursive: true });
127
+ installRule('python', { cwd, home, global: true });
128
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
129
+ assert.ok(content.includes('<!-- booklib-rules-python-start -->'));
130
+ assert.ok(content.includes('<!-- booklib-rules-python-end -->'));
131
+ rmSync(cwd, { recursive: true });
132
+ rmSync(home, { recursive: true });
133
+ });
134
+
135
+ test('installRule global is idempotent — replaces existing section', () => {
136
+ const cwd = tmp();
137
+ const home = tmp();
138
+ mkdirSync(join(home, '.claude'), { recursive: true });
139
+ installRule('python', { cwd, home, global: true });
140
+ installRule('python', { cwd, home, global: true });
141
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
142
+ const startCount = (content.match(/<!-- booklib-rules-python-start -->/g) || []).length;
143
+ assert.equal(startCount, 1);
144
+ rmSync(cwd, { recursive: true });
145
+ rmSync(home, { recursive: true });
146
+ });
147
+
148
+ test('installRule global creates CLAUDE.md if absent', () => {
149
+ const cwd = tmp();
150
+ const home = tmp();
151
+ mkdirSync(join(home, '.claude'), { recursive: true });
152
+ installRule('python', { cwd, home, global: true });
153
+ assert.ok(existsSync(join(home, '.claude', 'CLAUDE.md')));
154
+ rmSync(cwd, { recursive: true });
155
+ rmSync(home, { recursive: true });
156
+ });
157
+
158
+ test('installRule global preserves existing CLAUDE.md content', () => {
159
+ const cwd = tmp();
160
+ const home = tmp();
161
+ mkdirSync(join(home, '.claude'), { recursive: true });
162
+ writeFileSync(join(home, '.claude', 'CLAUDE.md'), '# My existing rules\n\nDo not delete me.\n');
163
+ installRule('python', { cwd, home, global: true });
164
+ const content = readFileSync(join(home, '.claude', 'CLAUDE.md'), 'utf8');
165
+ assert.ok(content.includes('# My existing rules'));
166
+ assert.ok(content.includes('Do not delete me.'));
167
+ assert.ok(content.includes('<!-- booklib-rules-python-start -->'));
168
+ rmSync(cwd, { recursive: true });
169
+ rmSync(home, { recursive: true });
170
+ });
171
+
172
+ // ─── status ──────────────────────────────────────────────────────────────────
173
+
174
+ test('status returns empty when no rules installed', () => {
175
+ const cwd = tmp();
176
+ const home = tmp();
177
+ const result = status(cwd, home);
178
+ assert.deepEqual(result.cursor, []);
179
+ assert.deepEqual(result.global, []);
180
+ assert.equal(result.totalBytes, 0);
181
+ rmSync(cwd, { recursive: true });
182
+ rmSync(home, { recursive: true });
183
+ });
184
+
185
+ test('status lists cursor mdc files with sizes', () => {
186
+ const cwd = tmp();
187
+ const home = tmp();
188
+ mkdirSync(join(cwd, '.cursor', 'rules'), { recursive: true });
189
+ writeFileSync(join(cwd, '.cursor', 'rules', 'python-effective-python.mdc'), 'hello world');
190
+ const result = status(cwd, home);
191
+ assert.equal(result.cursor.length, 1);
192
+ assert.ok(result.cursor[0].path.endsWith('python-effective-python.mdc'));
193
+ assert.ok(result.cursor[0].sizeBytes > 0);
194
+ rmSync(cwd, { recursive: true });
195
+ rmSync(home, { recursive: true });
196
+ });
197
+
198
+ test('status lists global sections with sizes', () => {
199
+ const cwd = tmp();
200
+ const home = tmp();
201
+ mkdirSync(join(home, '.claude'), { recursive: true });
202
+ writeFileSync(
203
+ join(home, '.claude', 'CLAUDE.md'),
204
+ '<!-- booklib-rules-python-start -->\npython rules here\n<!-- booklib-rules-python-end -->\n',
205
+ );
206
+ const result = status(cwd, home);
207
+ assert.equal(result.global.length, 1);
208
+ assert.equal(result.global[0].lang, 'python');
209
+ assert.ok(result.global[0].sizeBytes > 0);
210
+ rmSync(cwd, { recursive: true });
211
+ rmSync(home, { recursive: true });
212
+ });
213
+
214
+ test('status totalBytes is sum of cursor and global', () => {
215
+ const cwd = tmp();
216
+ const home = tmp();
217
+ mkdirSync(join(cwd, '.cursor', 'rules'), { recursive: true });
218
+ writeFileSync(join(cwd, '.cursor', 'rules', 'python-effective-python.mdc'), 'aaa');
219
+ mkdirSync(join(home, '.claude'), { recursive: true });
220
+ writeFileSync(
221
+ join(home, '.claude', 'CLAUDE.md'),
222
+ '<!-- booklib-rules-common-start -->\nbbb\n<!-- booklib-rules-common-end -->\n',
223
+ );
224
+ const result = status(cwd, home);
225
+ const expected = result.cursor.reduce((s, c) => s + c.sizeBytes, 0) +
226
+ result.global.reduce((s, g) => s + g.sizeBytes, 0);
227
+ assert.equal(result.totalBytes, expected);
228
+ rmSync(cwd, { recursive: true });
229
+ rmSync(home, { recursive: true });
230
+ });
@@ -0,0 +1,40 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { WellKnownBuilder } from '../lib/well-known-builder.js';
7
+
8
+ function tmpDir() {
9
+ return mkdtempSync(path.join(tmpdir(), 'booklib-wk-test-'));
10
+ }
11
+
12
+ test('generates skill.md at correct path', async () => {
13
+ const outDir = tmpDir();
14
+ const builder = new WellKnownBuilder({ outDir });
15
+ const outPath = await builder.build();
16
+ assert.ok(existsSync(outPath), `Expected file at ${outPath}`);
17
+ rmSync(outDir, { recursive: true });
18
+ });
19
+
20
+ test('generated skill.md has valid frontmatter', async () => {
21
+ const outDir = tmpDir();
22
+ const builder = new WellKnownBuilder({ outDir });
23
+ const outPath = await builder.build();
24
+ const content = readFileSync(outPath, 'utf8');
25
+ assert.ok(content.startsWith('---'), 'Missing frontmatter opening');
26
+ assert.ok(content.includes('name: booklib-skills'), 'Missing name field');
27
+ assert.ok(content.includes('description:'), 'Missing description field');
28
+ rmSync(outDir, { recursive: true });
29
+ });
30
+
31
+ test('generated skill.md lists all bundled skills', async () => {
32
+ const outDir = tmpDir();
33
+ const builder = new WellKnownBuilder({ outDir });
34
+ const outPath = await builder.build();
35
+ const content = readFileSync(outPath, 'utf8');
36
+ assert.ok(content.includes('effective-kotlin'), 'Missing effective-kotlin entry');
37
+ assert.ok(content.includes('effective-python'), 'Missing effective-python entry');
38
+ assert.ok(content.includes('clean-code-reviewer'), 'Missing clean-code-reviewer entry');
39
+ rmSync(outDir, { recursive: true });
40
+ });
@@ -0,0 +1,31 @@
1
+ // tests/wizard/integration-detector.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { detectIntegrations } from '../../lib/wizard/integration-detector.js';
8
+
9
+ test('detects ruflo from config file in cwd', () => {
10
+ const dir = mkdtempSync(join(tmpdir(), 'booklib-int-'));
11
+ writeFileSync(join(dir, 'ruflo.config.json'), '{}');
12
+ const result = detectIntegrations({ cwd: dir, home: dir });
13
+ assert.equal(result.ruflo, true);
14
+ rmSync(dir, { recursive: true });
15
+ });
16
+
17
+ test('detects superpowers from plugins dir', () => {
18
+ const dir = mkdtempSync(join(tmpdir(), 'booklib-int-'));
19
+ mkdirSync(join(dir, '.claude', 'plugins', 'superpowers'), { recursive: true });
20
+ const result = detectIntegrations({ cwd: dir, home: dir });
21
+ assert.equal(result.superpowers, true);
22
+ rmSync(dir, { recursive: true });
23
+ });
24
+
25
+ test('returns false for empty dir', () => {
26
+ const dir = mkdtempSync(join(tmpdir(), 'booklib-int-'));
27
+ const result = detectIntegrations({ cwd: dir, home: dir });
28
+ assert.equal(result.superpowers, false);
29
+ assert.equal(result.ruflo, false);
30
+ rmSync(dir, { recursive: true });
31
+ });
@@ -0,0 +1,51 @@
1
+ // tests/wizard/project-detector.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { detect } from '../../lib/wizard/project-detector.js';
8
+
9
+ function tmp() { return mkdtempSync(join(tmpdir(), 'booklib-detect-')); }
10
+
11
+ test('detects python from pyproject.toml', () => {
12
+ const dir = tmp();
13
+ writeFileSync(join(dir, 'pyproject.toml'), '[tool.poetry]\nname = "app"');
14
+ const result = detect(dir);
15
+ assert.ok(result.languages.includes('python'));
16
+ rmSync(dir, { recursive: true });
17
+ });
18
+
19
+ test('detects kotlin from .kt file', () => {
20
+ const dir = tmp();
21
+ writeFileSync(join(dir, 'Main.kt'), 'fun main() {}');
22
+ const result = detect(dir);
23
+ assert.ok(result.languages.includes('kotlin'));
24
+ rmSync(dir, { recursive: true });
25
+ });
26
+
27
+ test('detects typescript from package.json dep', () => {
28
+ const dir = tmp();
29
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({
30
+ dependencies: { typescript: '^5' }
31
+ }));
32
+ writeFileSync(join(dir, 'index.ts'), '');
33
+ const result = detect(dir);
34
+ assert.ok(result.languages.includes('typescript'));
35
+ rmSync(dir, { recursive: true });
36
+ });
37
+
38
+ test('detects fastapi framework', () => {
39
+ const dir = tmp();
40
+ writeFileSync(join(dir, 'requirements.txt'), 'fastapi==0.100\nuvicorn');
41
+ const result = detect(dir);
42
+ assert.ok(result.frameworks.includes('FastAPI'));
43
+ rmSync(dir, { recursive: true });
44
+ });
45
+
46
+ test('returns empty for empty dir', () => {
47
+ const dir = tmp();
48
+ const result = detect(dir);
49
+ assert.deepEqual(result.languages, []);
50
+ rmSync(dir, { recursive: true });
51
+ });