@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,198 @@
1
+ // lib/engine/corrections.js
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { pipeline } from '@huggingface/transformers';
7
+
8
+ // ── Constants ────────────────────────────────────────────────────────────────
9
+
10
+ export const LEVEL_THRESHOLDS = [
11
+ { level: 4, min: 10 },
12
+ { level: 3, min: 5 },
13
+ { level: 2, min: 3 },
14
+ { level: 1, min: 1 },
15
+ ];
16
+
17
+ const DEDUP_THRESHOLD = 0.85;
18
+ const MAX_INJECTED = 20;
19
+
20
+ export const MARKER_START = '<!-- booklib-learned-start -->';
21
+ export const MARKER_END = '<!-- booklib-learned-end -->';
22
+
23
+ // ── Embedding model (lazy-loaded, module-level singleton) ────────────────────
24
+
25
+ let _extractor = null;
26
+
27
+ async function _getEmbedding(text) {
28
+ if (!_extractor) {
29
+ _extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
30
+ }
31
+ const output = await _extractor(text, { pooling: 'mean', normalize: true });
32
+ return Array.from(output.data);
33
+ }
34
+
35
+ // ── Pure functions ────────────────────────────────────────────────────────────
36
+
37
+ export function levelFromMentions(n) {
38
+ for (const { level, min } of LEVEL_THRESHOLDS) {
39
+ if (n >= min) return level;
40
+ }
41
+ return 1;
42
+ }
43
+
44
+ export function cosine(a, b) {
45
+ let dot = 0, na = 0, nb = 0;
46
+ for (let i = 0; i < a.length; i++) {
47
+ dot += a[i] * b[i];
48
+ na += a[i] * a[i];
49
+ nb += b[i] * b[i];
50
+ }
51
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
52
+ return denom === 0 ? 0 : dot / denom;
53
+ }
54
+
55
+ function _generateId() {
56
+ return randomUUID().replace(/-/g, '').slice(0, 8);
57
+ }
58
+
59
+ function _escapeRegex(s) {
60
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
+ }
62
+
63
+ // ── File paths ────────────────────────────────────────────────────────────────
64
+
65
+ function _correctionsPath(home) {
66
+ return path.join(home, '.booklib', 'corrections.jsonl');
67
+ }
68
+
69
+ function _claudeMdPath(home) {
70
+ return path.join(home, '.claude', 'CLAUDE.md');
71
+ }
72
+
73
+ // ── Storage ───────────────────────────────────────────────────────────────────
74
+
75
+ export function loadCorrections(home = os.homedir()) {
76
+ const p = _correctionsPath(home);
77
+ if (!fs.existsSync(p)) return [];
78
+ return fs.readFileSync(p, 'utf8')
79
+ .split('\n')
80
+ .filter(Boolean)
81
+ .reduce((acc, line) => {
82
+ try {
83
+ acc.push(JSON.parse(line));
84
+ } catch {
85
+ process.stderr.write(`Warning: skipping corrupt line in corrections.jsonl: ${line.slice(0, 40)}\n`);
86
+ }
87
+ return acc;
88
+ }, []);
89
+ }
90
+
91
+ function _saveCorrections(corrections, home) {
92
+ const p = _correctionsPath(home);
93
+ fs.mkdirSync(path.dirname(p), { recursive: true });
94
+ const lines = corrections.map(c => JSON.stringify(c)).join('\n');
95
+ fs.writeFileSync(p, corrections.length ? lines + '\n' : '');
96
+ }
97
+
98
+ // ── CLAUDE.md injection ────────────────────────────────────────────────────────
99
+
100
+ export function rebuildLearnedSection(home = os.homedir()) {
101
+ const corrections = loadCorrections(home);
102
+ const active = corrections
103
+ .filter(c => c.level >= 3)
104
+ .sort((a, b) => b.mentions - a.mentions)
105
+ .slice(0, MAX_INJECTED);
106
+
107
+ const claudeFile = _claudeMdPath(home);
108
+ fs.mkdirSync(path.dirname(claudeFile), { recursive: true });
109
+
110
+ let existing = '';
111
+ try { existing = fs.readFileSync(claudeFile, 'utf8'); } catch (e) {
112
+ if (e.code !== 'ENOENT') throw e;
113
+ }
114
+
115
+ if (active.length === 0) {
116
+ const re = new RegExp(
117
+ `\\n?${_escapeRegex(MARKER_START)}[\\s\\S]*?${_escapeRegex(MARKER_END)}\\n?`
118
+ );
119
+ const updated = existing.replace(re, '').trimEnd();
120
+ fs.writeFileSync(claudeFile, updated ? updated + '\n' : '');
121
+ return;
122
+ }
123
+
124
+ const bullets = active.map(c => `- ${c.text.slice(0, 120)}`).join('\n');
125
+ const section = [
126
+ MARKER_START,
127
+ '## Learned Corrections (BookLib)',
128
+ '',
129
+ '> When the user corrects your approach, run: booklib correction add "brief rule"',
130
+ '',
131
+ bullets,
132
+ '',
133
+ MARKER_END,
134
+ ].join('\n');
135
+
136
+ const re = new RegExp(
137
+ `${_escapeRegex(MARKER_START)}[\\s\\S]*?${_escapeRegex(MARKER_END)}`
138
+ );
139
+ const updated = existing.includes(MARKER_START)
140
+ ? existing.replace(re, section)
141
+ : (existing.trimEnd() ? `${existing.trimEnd()}\n\n${section}\n` : `${section}\n`);
142
+
143
+ fs.writeFileSync(claudeFile, updated);
144
+ }
145
+
146
+ // ── Public API ────────────────────────────────────────────────────────────────
147
+
148
+ export function listCorrections(home = os.homedir()) {
149
+ return loadCorrections(home).sort((a, b) => b.mentions - a.mentions);
150
+ }
151
+
152
+ export function removeCorrection(id, home = os.homedir()) {
153
+ const corrections = loadCorrections(home);
154
+ const idx = corrections.findIndex(c => c.id === id);
155
+ if (idx === -1) return null;
156
+ const [removed] = corrections.splice(idx, 1);
157
+ _saveCorrections(corrections, home);
158
+ if (removed.level >= 3) rebuildLearnedSection(home);
159
+ return removed;
160
+ }
161
+
162
+ export async function addCorrection(text, home = os.homedir(), embedFn = _getEmbedding) {
163
+ const corrections = loadCorrections(home);
164
+ const now = new Date().toISOString();
165
+ const newVec = await embedFn(text);
166
+
167
+ for (const c of corrections) {
168
+ const existVec = c.embedding ?? await embedFn(c.text);
169
+ if (!c.embedding) c.embedding = existVec; // backfill on first encounter
170
+ const sim = cosine(newVec, existVec);
171
+ if (sim >= DEDUP_THRESHOLD) {
172
+ const oldLevel = c.level;
173
+ c.mentions += 1;
174
+ c.level = levelFromMentions(c.mentions);
175
+ c.lastSeen = now;
176
+ c.sessions.push(now);
177
+ _saveCorrections(corrections, home);
178
+ if (c.level >= 3 && c.level !== oldLevel) rebuildLearnedSection(home);
179
+ const { embedding: _emb, ...rest } = c;
180
+ return { ...rest, wasExisting: true };
181
+ }
182
+ }
183
+
184
+ const entry = {
185
+ id: _generateId(),
186
+ text,
187
+ mentions: 1,
188
+ level: levelFromMentions(1),
189
+ sessions: [now],
190
+ firstSeen: now,
191
+ lastSeen: now,
192
+ embedding: newVec,
193
+ };
194
+ corrections.push(entry);
195
+ _saveCorrections(corrections, home);
196
+ const { embedding: _emb, ...rest } = entry;
197
+ return { ...rest, wasExisting: false };
198
+ }
@@ -0,0 +1,195 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { resolveBookLibPaths } from '../paths.js';
5
+ import { SKILL_LIMIT } from '../wizard/skill-recommender.js';
6
+ import { countInstalledSlots, listInstalledSkillNames } from '../skill-fetcher.js';
7
+
8
+ const MARKER_START = '<!-- booklib-standards-start -->';
9
+
10
+ /**
11
+ * Runs all diagnostic checks and returns an array of findings.
12
+ * @param {string} cwd - project root
13
+ * @returns {Array<{ check: string, severity: 'error'|'warning'|'info', message: string, fixable: boolean, data?: object }>}
14
+ */
15
+ export function runDiagnostics(cwd = process.cwd()) {
16
+ const findings = [];
17
+
18
+ checkSlotOverload(findings);
19
+ checkOversizedConfigs(findings, cwd);
20
+ checkMissingIndex(findings, cwd);
21
+ checkMissingConfigFiles(findings, cwd);
22
+ checkStaleSkills(findings);
23
+ checkOrphanedSkills(findings);
24
+
25
+ return findings;
26
+ }
27
+
28
+ /** Check 1: Slot overload */
29
+ function checkSlotOverload(findings) {
30
+ const slotsUsed = countInstalledSlots();
31
+ if (slotsUsed > SKILL_LIMIT) {
32
+ findings.push({
33
+ check: 'slot-overload',
34
+ severity: 'warning',
35
+ message: `${slotsUsed} skills installed (limit: ${SKILL_LIMIT}). Agent context is overloaded.`,
36
+ fixable: true,
37
+ });
38
+ }
39
+ }
40
+
41
+ /** Check 2: Oversized config files */
42
+ function checkOversizedConfigs(findings, cwd) {
43
+ const configFiles = [
44
+ { file: 'CLAUDE.md', path: path.join(cwd, 'CLAUDE.md') },
45
+ { file: '.github/copilot-instructions.md', path: path.join(cwd, '.github', 'copilot-instructions.md') },
46
+ { file: '.gemini/context.md', path: path.join(cwd, '.gemini', 'context.md') },
47
+ ];
48
+
49
+ for (const cf of configFiles) {
50
+ if (fs.existsSync(cf.path)) {
51
+ const content = fs.readFileSync(cf.path, 'utf8');
52
+ const lines = content.split('\n').length;
53
+ if (lines > 500 && content.includes(MARKER_START)) {
54
+ findings.push({
55
+ check: 'oversized-config',
56
+ severity: 'warning',
57
+ message: `${cf.file} is ${lines} lines. Recommended: under 200.`,
58
+ fixable: true,
59
+ data: { file: cf.file, absPath: cf.path, lines },
60
+ });
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ /** Check 3: Missing index */
67
+ function checkMissingIndex(findings, cwd) {
68
+ try {
69
+ const { indexPath } = resolveBookLibPaths(cwd);
70
+ if (!fs.existsSync(indexPath)) {
71
+ findings.push({
72
+ check: 'missing-index',
73
+ severity: 'error',
74
+ message: 'No search index found. Search and recommendations won\'t work.',
75
+ fixable: true,
76
+ });
77
+ }
78
+ } catch {
79
+ findings.push({
80
+ check: 'missing-index',
81
+ severity: 'error',
82
+ message: 'No search index found. Search and recommendations won\'t work.',
83
+ fixable: true,
84
+ });
85
+ }
86
+ }
87
+
88
+ /** Check 4: Missing config files for configured tools */
89
+ function checkMissingConfigFiles(findings, cwd) {
90
+ const configPath = path.join(cwd, 'booklib.config.json');
91
+ if (!fs.existsSync(configPath)) return;
92
+
93
+ try {
94
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
95
+ const toolFileMap = {
96
+ claude: 'CLAUDE.md',
97
+ copilot: '.github/copilot-instructions.md',
98
+ gemini: '.gemini/context.md',
99
+ codex: 'AGENTS.md',
100
+ cursor: '.cursor/rules/booklib-standards.mdc',
101
+ };
102
+
103
+ for (const tool of (config.tools ?? [])) {
104
+ const expectedFile = toolFileMap[tool];
105
+ if (expectedFile && !fs.existsSync(path.join(cwd, expectedFile))) {
106
+ findings.push({
107
+ check: 'missing-config',
108
+ severity: 'warning',
109
+ message: `${tool} configured but ${expectedFile} not found.`,
110
+ fixable: true,
111
+ data: { tool, file: expectedFile },
112
+ });
113
+ }
114
+ }
115
+ } catch { /* malformed config, skip */ }
116
+ }
117
+
118
+ /** Check 5: Stale skills (installed but never used) */
119
+ function checkStaleSkills(findings) {
120
+ const usagePath = path.join(os.homedir(), '.booklib', 'usage.json');
121
+ if (!fs.existsSync(usagePath)) return;
122
+
123
+ try {
124
+ const usage = JSON.parse(fs.readFileSync(usagePath, 'utf8'));
125
+ const usedNames = new Set(
126
+ usage.map(u => u.skill?.toLowerCase()).filter(Boolean),
127
+ );
128
+ const installed = listInstalledSkillNames();
129
+ const stale = installed.filter(n => !usedNames.has(n.toLowerCase()));
130
+
131
+ if (stale.length > 5) {
132
+ findings.push({
133
+ check: 'stale-skills',
134
+ severity: 'info',
135
+ message: `${stale.length} skills have no recorded usage.`,
136
+ fixable: true,
137
+ data: { staleNames: stale },
138
+ });
139
+ }
140
+ } catch { /* no usage data, skip */ }
141
+ }
142
+
143
+ /** Check 6: Orphaned skills (have .booklib marker but not in any catalog/index) */
144
+ function checkOrphanedSkills(findings) {
145
+ const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
146
+ if (!fs.existsSync(claudeSkillsDir)) return;
147
+
148
+ try {
149
+ const installed = listInstalledSkillNames();
150
+ // Read the bundled skills directory to find known skill names
151
+ const packageRoot = path.resolve(
152
+ new URL('.', import.meta.url).pathname, '..', '..',
153
+ );
154
+ const bundledSkillsDir = path.join(packageRoot, 'skills');
155
+ const bundledNames = new Set();
156
+ if (fs.existsSync(bundledSkillsDir)) {
157
+ for (const entry of fs.readdirSync(bundledSkillsDir, { withFileTypes: true })) {
158
+ if (entry.isDirectory()) bundledNames.add(entry.name);
159
+ }
160
+ }
161
+
162
+ const orphaned = installed.filter(n => !bundledNames.has(n));
163
+ for (const name of orphaned) {
164
+ findings.push({
165
+ check: 'orphaned-skill',
166
+ severity: 'info',
167
+ message: `${name} -- not found in any catalog. May be outdated.`,
168
+ fixable: false,
169
+ });
170
+ }
171
+ } catch { /* skip orphan check on error */ }
172
+ }
173
+
174
+ /**
175
+ * Prints diagnostic results to stdout.
176
+ * @param {Array<{ check: string, severity: string, message: string, fixable: boolean }>} findings
177
+ */
178
+ export function printDiagnostics(findings) {
179
+ if (findings.length === 0) {
180
+ console.log(' No issues found. BookLib is healthy.\n');
181
+ return;
182
+ }
183
+
184
+ for (const f of findings) {
185
+ const icon = f.severity === 'error' ? '\u2717' : f.severity === 'warning' ? '\u26A0' : '\u2139';
186
+ console.log(` ${icon} ${f.message}`);
187
+ }
188
+
189
+ const fixable = findings.filter(f => f.fixable).length;
190
+ if (fixable > 0) {
191
+ console.log(`\n ${fixable} issue(s) fixable. Run: booklib doctor --cure\n`);
192
+ } else {
193
+ console.log('');
194
+ }
195
+ }
@@ -0,0 +1,137 @@
1
+ // lib/engine/graph-injector.js
2
+ import { minimatch } from 'minimatch';
3
+ import {
4
+ listNodes, loadNode, loadEdges, traverseEdges,
5
+ parseNodeFrontmatter, resolveKnowledgePaths,
6
+ } from './graph.js';
7
+
8
+ // ── Component matching ────────────────────────────────────────────────────────
9
+
10
+ /**
11
+ * Returns component nodes whose path globs match filePath.
12
+ * @param {string} filePath
13
+ * @param {Array<{id: string, paths: string[], title: string}>} components
14
+ */
15
+ export function findOwningComponents(filePath, components) {
16
+ return components.filter(comp =>
17
+ (comp.paths ?? []).some(glob => minimatch(filePath, glob, { matchBase: true }))
18
+ );
19
+ }
20
+
21
+ // ── Ranking ───────────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Deduplicates nodes by id (keeping highest score) and sorts descending by score.
25
+ * @param {Array<{id: string, score: number, text: string, hop: number}>} nodes
26
+ */
27
+ export function scoreAndRankNodes(nodes) {
28
+ const best = new Map();
29
+ for (const node of nodes) {
30
+ const existing = best.get(node.id);
31
+ if (!existing || node.score > existing.score) {
32
+ best.set(node.id, node);
33
+ }
34
+ }
35
+ return [...best.values()].sort((a, b) => b.score - a.score);
36
+ }
37
+
38
+ // ── Main injection pipeline ───────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Builds a ranked list of relevant knowledge nodes for the given context.
42
+ *
43
+ * Pipeline:
44
+ * 1. Find component nodes that own the current file (path matching)
45
+ * 2. Semantic search for nodes matching the task context
46
+ * 3. BFS graph traversal from all start nodes (components + semantic hits)
47
+ * 4. Deduplicate and rank by score
48
+ * 5. Return top N with full content loaded
49
+ *
50
+ * @param {object} opts
51
+ * @param {string|null} opts.filePath - File being edited (used for component matching)
52
+ * @param {string} opts.taskContext - Task description for semantic search
53
+ * @param {object} opts.searcher - BookLibSearcher instance
54
+ * @param {number} [opts.limit=8] - Max nodes to return
55
+ * @param {number} [opts.minScore=0.35] - Minimum semantic similarity score
56
+ * @returns {Promise<Array<{id: string, title: string, type: string, body: string, score: number}>>}
57
+ */
58
+ export async function buildGraphContext({ filePath, taskContext, searcher, limit = 8, minScore = 0.35 }) {
59
+ const { nodesDir } = resolveKnowledgePaths();
60
+ const allNodeIds = listNodes({ nodesDir });
61
+ if (allNodeIds.length === 0) return [];
62
+
63
+ const edges = loadEdges();
64
+
65
+ // Load all component nodes for path matching
66
+ const componentNodes = allNodeIds
67
+ .map(id => {
68
+ const raw = loadNode(id, { nodesDir });
69
+ return raw ? parseNodeFrontmatter(raw) : null;
70
+ })
71
+ .filter(n => n?.type === 'component');
72
+
73
+ // 1. Component nodes that own the current file
74
+ const owningComponents = filePath
75
+ ? findOwningComponents(filePath, componentNodes)
76
+ : [];
77
+
78
+ // 2. Semantic search — only knowledge nodes (nodeKind: 'knowledge')
79
+ let semanticResults = [];
80
+ if (taskContext) {
81
+ try {
82
+ const raw = await searcher.search(taskContext, 20, minScore);
83
+ semanticResults = raw
84
+ .filter(r => r.metadata?.nodeKind === 'knowledge' && r.metadata?.id)
85
+ .map(r => ({
86
+ id: r.metadata.id,
87
+ title: r.metadata.title,
88
+ type: r.metadata.type,
89
+ text: r.text,
90
+ score: r.score,
91
+ hop: 0,
92
+ }));
93
+ } catch {
94
+ // Index may not exist yet — skip semantic step gracefully
95
+ }
96
+ }
97
+
98
+ // 3. BFS traversal from all start nodes
99
+ const startIds = new Set([
100
+ ...owningComponents.map(c => c.id),
101
+ ...semanticResults.map(r => r.id),
102
+ ]);
103
+
104
+ const traversalHits = [];
105
+ for (const startId of startIds) {
106
+ const hops = traverseEdges(startId, edges, 2);
107
+ for (const { id, hop } of hops) {
108
+ const raw = loadNode(id, { nodesDir });
109
+ if (!raw) continue;
110
+ const parsed = parseNodeFrontmatter(raw);
111
+ traversalHits.push({
112
+ id,
113
+ title: parsed.title,
114
+ type: parsed.type,
115
+ text: parsed.body ?? '',
116
+ score: 0.5 / hop, // distance penalty: hop 1 → 0.5, hop 2 → 0.25
117
+ hop,
118
+ });
119
+ }
120
+ }
121
+
122
+ // 4. Merge, deduplicate, rank
123
+ const ranked = scoreAndRankNodes([...semanticResults, ...traversalHits]);
124
+
125
+ // 5. Load full content for top results
126
+ return ranked.slice(0, limit).map(node => {
127
+ const raw = loadNode(node.id, { nodesDir });
128
+ const parsed = raw ? parseNodeFrontmatter(raw) : {};
129
+ return {
130
+ id: node.id,
131
+ title: parsed.title ?? node.title,
132
+ type: parsed.type ?? node.type,
133
+ body: parsed.body ?? node.text,
134
+ score: node.score,
135
+ };
136
+ });
137
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ import matter from 'gray-matter';
5
+ import { resolveBookLibPaths } from '../paths.js';
6
+
7
+ export function resolveKnowledgePaths() {
8
+ const { indexPath } = resolveBookLibPaths();
9
+ const bookLibDir = path.dirname(indexPath);
10
+ return {
11
+ nodesDir: path.join(bookLibDir, 'knowledge', 'nodes'),
12
+ graphFile: path.join(bookLibDir, 'knowledge', 'graph.jsonl'),
13
+ };
14
+ }
15
+
16
+ export const EDGE_TYPES = Object.freeze([
17
+ 'implements', 'contradicts', 'extends', 'applies-to',
18
+ 'see-also', 'inspired-by', 'supersedes', 'depends-on',
19
+ ]);
20
+
21
+ export function generateNodeId(prefix = 'node') {
22
+ return `${prefix}_${randomBytes(4).toString('hex')}`;
23
+ }
24
+
25
+ /** Serializes a knowledge node to a gray-matter markdown string. */
26
+ export function serializeNode({
27
+ id, type, title, content = '',
28
+ sources = [], tags = [], area = null,
29
+ confidence = 'high', nodePaths = [],
30
+ raw = null,
31
+ }) {
32
+ const data = { id, type, title, created: new Date().toISOString().split('T')[0] };
33
+ if (sources.length) data.sources = sources;
34
+ if (tags.length) data.tags = tags;
35
+ if (area) data.area = area;
36
+ if (confidence !== 'high') data.confidence = confidence;
37
+ if (nodePaths.length) data.paths = nodePaths;
38
+ if (raw) data.raw = raw;
39
+ return matter.stringify(content.trim(), data);
40
+ }
41
+
42
+ export function saveNode(nodeContent, id, { nodesDir } = {}) {
43
+ const dir = nodesDir ?? resolveKnowledgePaths().nodesDir;
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ const filePath = path.join(dir, `${id}.md`);
46
+ fs.writeFileSync(filePath, nodeContent, 'utf8');
47
+ return filePath;
48
+ }
49
+
50
+ export function loadNode(id, { nodesDir } = {}) {
51
+ const dir = nodesDir ?? resolveKnowledgePaths().nodesDir;
52
+ const filePath = path.join(dir, `${id}.md`);
53
+ if (!fs.existsSync(filePath)) return null;
54
+ return fs.readFileSync(filePath, 'utf8');
55
+ }
56
+
57
+ /** Parses a node markdown file, returning frontmatter fields plus a `body` property. */
58
+ export function parseNodeFrontmatter(content) {
59
+ const { data, content: body } = matter(content);
60
+ return { ...data, body: body.trim() };
61
+ }
62
+
63
+ export function listNodes({ nodesDir } = {}) {
64
+ const { nodesDir: defaultDir } = resolveKnowledgePaths();
65
+ const dir = nodesDir ?? defaultDir;
66
+ if (!fs.existsSync(dir)) return [];
67
+ return fs.readdirSync(dir)
68
+ .filter(f => f.endsWith('.md'))
69
+ .map(f => f.replace(/\.md$/, ''));
70
+ }
71
+
72
+ // ── Edge primitives ──────────────────────────────────────────────────────────
73
+
74
+ export function appendEdge(edge, { graphFile } = {}) {
75
+ const file = graphFile ?? resolveKnowledgePaths().graphFile;
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.appendFileSync(file, JSON.stringify(edge) + '\n', 'utf8');
78
+ }
79
+
80
+ export function loadEdges({ graphFile } = {}) {
81
+ const file = graphFile ?? resolveKnowledgePaths().graphFile;
82
+ if (!fs.existsSync(file)) return [];
83
+ return fs.readFileSync(file, 'utf8')
84
+ .split('\n')
85
+ .filter(Boolean)
86
+ .map(line => JSON.parse(line));
87
+ }
88
+
89
+ /**
90
+ * Resolves a node reference (exact ID or partial case-insensitive title) to a node ID.
91
+ * Returns the matched node ID, or throws an Error with a helpful message.
92
+ * @param {string} ref - Exact node ID or partial title string.
93
+ * @param {{ nodesDir?: string }} [opts]
94
+ */
95
+ export function resolveNodeRef(ref, { nodesDir } = {}) {
96
+ const dir = nodesDir ?? resolveKnowledgePaths().nodesDir;
97
+ const allIds = listNodes({ nodesDir: dir });
98
+
99
+ // Exact ID match
100
+ if (allIds.includes(ref)) return ref;
101
+
102
+ // Title match (case-insensitive, partial)
103
+ const matches = allIds
104
+ .map(id => {
105
+ const raw = loadNode(id, { nodesDir: dir });
106
+ if (!raw) return null;
107
+ const parsed = parseNodeFrontmatter(raw);
108
+ return { id, title: parsed.title ?? '', type: parsed.type ?? '' };
109
+ })
110
+ .filter(n => n && n.title.toLowerCase().includes(ref.toLowerCase()));
111
+
112
+ if (matches.length === 1) return matches[0].id;
113
+
114
+ if (matches.length === 0) {
115
+ throw new Error(`No node found matching "${ref}". Run: booklib nodes list`);
116
+ }
117
+
118
+ const list = matches.map(m => ` ${m.id} [${m.type}] ${m.title}`).join('\n');
119
+ throw new Error(`Multiple nodes match "${ref}" — use the exact ID:\n${list}`);
120
+ }
121
+
122
+ // ── Graph traversal (BFS, max 2 hops by default) ────────────────────────────
123
+
124
+ export function traverseEdges(startId, edges, maxHops = 2) {
125
+ const results = [];
126
+ const visited = new Set([startId]);
127
+ const queue = [{ id: startId, hop: 0 }];
128
+
129
+ while (queue.length > 0) {
130
+ const { id, hop } = queue.shift();
131
+ if (hop >= maxHops) continue;
132
+
133
+ const connected = edges.filter(e => e.from === id || e.to === id);
134
+ for (const edge of connected) {
135
+ const neighborId = edge.from === id ? edge.to : edge.from;
136
+ if (!visited.has(neighborId)) {
137
+ visited.add(neighborId);
138
+ results.push({ id: neighborId, edge, hop: hop + 1 });
139
+ queue.push({ id: neighborId, hop: hop + 1 });
140
+ }
141
+ }
142
+ }
143
+ return results;
144
+ }
145
+
146
+ /**
147
+ * Parses a links argument string like "effective-kotlin:applies-to,design-patterns:see-also"
148
+ * into an array of { to, type } pairs. Skips entries without a colon separator.
149
+ * @param {string} linksArg
150
+ * @returns {{ to: string, type: string }[]}
151
+ */
152
+ export function parseCaptureLinkArgs(linksArg) {
153
+ if (!linksArg) return [];
154
+ return linksArg.split(',')
155
+ .map(pair => {
156
+ const colonIdx = pair.lastIndexOf(':');
157
+ if (colonIdx === -1) return null;
158
+ return { to: pair.slice(0, colonIdx).trim(), type: pair.slice(colonIdx + 1).trim() };
159
+ })
160
+ .filter(Boolean);
161
+ }