@mseep/core 3.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 (312) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/LICENSE +21 -0
  3. package/README.ja.md +14 -0
  4. package/README.ko.md +14 -0
  5. package/README.md +227 -0
  6. package/README.pt-BR.md +14 -0
  7. package/README.skills.md +50 -0
  8. package/README.uk.md +14 -0
  9. package/README.zh-CN.md +14 -0
  10. package/bin/booklib-mcp.js +458 -0
  11. package/bin/booklib.js +2394 -0
  12. package/bin/skills.cjs +1292 -0
  13. package/community/registry.json +1616 -0
  14. package/hooks/hooks.json +52 -0
  15. package/hooks/posttooluse-capture.mjs +67 -0
  16. package/hooks/posttooluse-contradict.mjs +76 -0
  17. package/hooks/posttooluse-imports.mjs +67 -0
  18. package/hooks/pretooluse-inject.mjs +82 -0
  19. package/hooks/suggest.js +153 -0
  20. package/lib/agent-detector.js +96 -0
  21. package/lib/config-loader.js +39 -0
  22. package/lib/conflict-resolver.js +148 -0
  23. package/lib/connectors/context7.js +167 -0
  24. package/lib/connectors/github.js +223 -0
  25. package/lib/connectors/local.js +120 -0
  26. package/lib/connectors/notion.js +436 -0
  27. package/lib/connectors/web.js +134 -0
  28. package/lib/context-builder.js +574 -0
  29. package/lib/discovery-engine.js +298 -0
  30. package/lib/doctor/hook-installer.js +83 -0
  31. package/lib/doctor/usage-tracker.js +87 -0
  32. package/lib/engine/auditor.js +103 -0
  33. package/lib/engine/auto-linker.js +177 -0
  34. package/lib/engine/bm25-index.js +178 -0
  35. package/lib/engine/capture.js +120 -0
  36. package/lib/engine/context-map.js +641 -0
  37. package/lib/engine/corrections.js +194 -0
  38. package/lib/engine/decision-checker.js +203 -0
  39. package/lib/engine/doctor.js +207 -0
  40. package/lib/engine/embedding-provider.js +72 -0
  41. package/lib/engine/gap-detector.js +138 -0
  42. package/lib/engine/gap-resolver.js +135 -0
  43. package/lib/engine/graph-injector.js +137 -0
  44. package/lib/engine/graph-search.js +183 -0
  45. package/lib/engine/graph.js +170 -0
  46. package/lib/engine/handoff.js +411 -0
  47. package/lib/engine/import-checker.js +249 -0
  48. package/lib/engine/import-parser.js +145 -0
  49. package/lib/engine/indexer.js +334 -0
  50. package/lib/engine/lookup-priority.js +15 -0
  51. package/lib/engine/parser.js +257 -0
  52. package/lib/engine/principle-extractor.js +116 -0
  53. package/lib/engine/project-analyzer.js +353 -0
  54. package/lib/engine/query-expander.js +42 -0
  55. package/lib/engine/reasoning-modes.js +353 -0
  56. package/lib/engine/registries.js +524 -0
  57. package/lib/engine/reranker.js +45 -0
  58. package/lib/engine/rrf.js +59 -0
  59. package/lib/engine/scanner.js +151 -0
  60. package/lib/engine/searcher.js +223 -0
  61. package/lib/engine/session-coordinator.js +291 -0
  62. package/lib/engine/session-manager.js +375 -0
  63. package/lib/engine/source-detector.js +240 -0
  64. package/lib/engine/source-manager.js +142 -0
  65. package/lib/engine/structured-response.js +47 -0
  66. package/lib/engine/synthesis-templates.js +364 -0
  67. package/lib/installer.js +70 -0
  68. package/lib/instinct-block.js +21 -0
  69. package/lib/mcp-config-writer.js +107 -0
  70. package/lib/paths.js +62 -0
  71. package/lib/project-initializer.js +856 -0
  72. package/lib/registry/skills.js +102 -0
  73. package/lib/registry-searcher.js +107 -0
  74. package/lib/rules/rules-manager.js +169 -0
  75. package/lib/skill-fetcher.js +333 -0
  76. package/lib/well-known-builder.js +74 -0
  77. package/lib/wizard/index.js +1389 -0
  78. package/lib/wizard/integration-detector.js +41 -0
  79. package/lib/wizard/project-detector.js +146 -0
  80. package/lib/wizard/prompt.js +221 -0
  81. package/lib/wizard/registry-embeddings.js +107 -0
  82. package/lib/wizard/skill-recommender.js +69 -0
  83. package/package.json +70 -0
  84. package/skills/animation-at-work/SKILL.md +270 -0
  85. package/skills/animation-at-work/assets/example_asset.txt +1 -0
  86. package/skills/animation-at-work/evals/evals.json +44 -0
  87. package/skills/animation-at-work/evals/results.json +13 -0
  88. package/skills/animation-at-work/examples/after.md +64 -0
  89. package/skills/animation-at-work/examples/before.md +35 -0
  90. package/skills/animation-at-work/references/api_reference.md +369 -0
  91. package/skills/animation-at-work/references/review-checklist.md +79 -0
  92. package/skills/animation-at-work/scripts/audit_animations.py +295 -0
  93. package/skills/animation-at-work/scripts/example.py +1 -0
  94. package/skills/booklib-mcp-guide/SKILL.md +129 -0
  95. package/skills/booklib-mcp-guide/evals/evals.json +37 -0
  96. package/skills/booklib-mcp-guide/examples/after.md +34 -0
  97. package/skills/booklib-mcp-guide/examples/before.md +27 -0
  98. package/skills/booklib-mcp-guide/references/tool-catalog.md +9 -0
  99. package/skills/clean-code-reviewer/SKILL.md +444 -0
  100. package/skills/clean-code-reviewer/audit.json +35 -0
  101. package/skills/clean-code-reviewer/evals/evals.json +185 -0
  102. package/skills/clean-code-reviewer/evals/results.json +13 -0
  103. package/skills/clean-code-reviewer/examples/after.md +48 -0
  104. package/skills/clean-code-reviewer/examples/before.md +33 -0
  105. package/skills/clean-code-reviewer/references/api_reference.md +158 -0
  106. package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
  107. package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
  108. package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
  109. package/skills/data-intensive-patterns/SKILL.md +267 -0
  110. package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
  111. package/skills/data-intensive-patterns/evals/evals.json +54 -0
  112. package/skills/data-intensive-patterns/evals/results.json +13 -0
  113. package/skills/data-intensive-patterns/examples/after.md +61 -0
  114. package/skills/data-intensive-patterns/examples/before.md +38 -0
  115. package/skills/data-intensive-patterns/references/api_reference.md +34 -0
  116. package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
  117. package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
  118. package/skills/data-intensive-patterns/scripts/adr.py +213 -0
  119. package/skills/data-intensive-patterns/scripts/example.py +1 -0
  120. package/skills/data-pipelines/SKILL.md +259 -0
  121. package/skills/data-pipelines/assets/example_asset.txt +1 -0
  122. package/skills/data-pipelines/evals/evals.json +45 -0
  123. package/skills/data-pipelines/evals/results.json +13 -0
  124. package/skills/data-pipelines/examples/after.md +97 -0
  125. package/skills/data-pipelines/examples/before.md +37 -0
  126. package/skills/data-pipelines/references/api_reference.md +301 -0
  127. package/skills/data-pipelines/references/review-checklist.md +181 -0
  128. package/skills/data-pipelines/scripts/example.py +1 -0
  129. package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
  130. package/skills/design-patterns/SKILL.md +271 -0
  131. package/skills/design-patterns/assets/example_asset.txt +1 -0
  132. package/skills/design-patterns/evals/evals.json +46 -0
  133. package/skills/design-patterns/evals/results.json +13 -0
  134. package/skills/design-patterns/examples/after.md +52 -0
  135. package/skills/design-patterns/examples/before.md +29 -0
  136. package/skills/design-patterns/references/api_reference.md +1 -0
  137. package/skills/design-patterns/references/patterns-catalog.md +726 -0
  138. package/skills/design-patterns/references/review-checklist.md +173 -0
  139. package/skills/design-patterns/scripts/example.py +1 -0
  140. package/skills/design-patterns/scripts/scaffold.py +807 -0
  141. package/skills/domain-driven-design/SKILL.md +142 -0
  142. package/skills/domain-driven-design/assets/example_asset.txt +1 -0
  143. package/skills/domain-driven-design/evals/evals.json +48 -0
  144. package/skills/domain-driven-design/evals/results.json +13 -0
  145. package/skills/domain-driven-design/examples/after.md +80 -0
  146. package/skills/domain-driven-design/examples/before.md +43 -0
  147. package/skills/domain-driven-design/references/api_reference.md +1 -0
  148. package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
  149. package/skills/domain-driven-design/references/review-checklist.md +158 -0
  150. package/skills/domain-driven-design/scripts/example.py +1 -0
  151. package/skills/domain-driven-design/scripts/scaffold.py +421 -0
  152. package/skills/effective-java/SKILL.md +227 -0
  153. package/skills/effective-java/assets/example_asset.txt +1 -0
  154. package/skills/effective-java/evals/evals.json +46 -0
  155. package/skills/effective-java/evals/results.json +13 -0
  156. package/skills/effective-java/examples/after.md +83 -0
  157. package/skills/effective-java/examples/before.md +37 -0
  158. package/skills/effective-java/references/api_reference.md +1 -0
  159. package/skills/effective-java/references/items-catalog.md +955 -0
  160. package/skills/effective-java/references/review-checklist.md +216 -0
  161. package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
  162. package/skills/effective-java/scripts/example.py +1 -0
  163. package/skills/effective-kotlin/SKILL.md +271 -0
  164. package/skills/effective-kotlin/assets/example_asset.txt +1 -0
  165. package/skills/effective-kotlin/audit.json +29 -0
  166. package/skills/effective-kotlin/evals/evals.json +45 -0
  167. package/skills/effective-kotlin/evals/results.json +13 -0
  168. package/skills/effective-kotlin/examples/after.md +36 -0
  169. package/skills/effective-kotlin/examples/before.md +38 -0
  170. package/skills/effective-kotlin/references/api_reference.md +1 -0
  171. package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
  172. package/skills/effective-kotlin/references/review-checklist.md +126 -0
  173. package/skills/effective-kotlin/scripts/example.py +1 -0
  174. package/skills/effective-python/SKILL.md +441 -0
  175. package/skills/effective-python/evals/evals.json +44 -0
  176. package/skills/effective-python/evals/results.json +13 -0
  177. package/skills/effective-python/examples/after.md +56 -0
  178. package/skills/effective-python/examples/before.md +40 -0
  179. package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
  180. package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
  181. package/skills/effective-python/ref-03-functions.md +186 -0
  182. package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
  183. package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
  184. package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
  185. package/skills/effective-python/ref-07-concurrency.md +213 -0
  186. package/skills/effective-python/ref-08-robustness-performance.md +248 -0
  187. package/skills/effective-python/ref-09-testing-debugging.md +253 -0
  188. package/skills/effective-python/ref-10-collaboration.md +175 -0
  189. package/skills/effective-python/references/api_reference.md +218 -0
  190. package/skills/effective-python/references/practices-catalog.md +483 -0
  191. package/skills/effective-python/references/review-checklist.md +190 -0
  192. package/skills/effective-python/scripts/lint.py +173 -0
  193. package/skills/effective-typescript/SKILL.md +262 -0
  194. package/skills/effective-typescript/audit.json +29 -0
  195. package/skills/effective-typescript/evals/evals.json +37 -0
  196. package/skills/effective-typescript/evals/results.json +13 -0
  197. package/skills/effective-typescript/examples/after.md +70 -0
  198. package/skills/effective-typescript/examples/before.md +47 -0
  199. package/skills/effective-typescript/references/api_reference.md +118 -0
  200. package/skills/effective-typescript/references/practices-catalog.md +371 -0
  201. package/skills/effective-typescript/scripts/review.py +169 -0
  202. package/skills/kotlin-in-action/SKILL.md +261 -0
  203. package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
  204. package/skills/kotlin-in-action/evals/evals.json +43 -0
  205. package/skills/kotlin-in-action/evals/results.json +13 -0
  206. package/skills/kotlin-in-action/examples/after.md +53 -0
  207. package/skills/kotlin-in-action/examples/before.md +39 -0
  208. package/skills/kotlin-in-action/references/api_reference.md +1 -0
  209. package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
  210. package/skills/kotlin-in-action/references/review-checklist.md +204 -0
  211. package/skills/kotlin-in-action/scripts/example.py +1 -0
  212. package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
  213. package/skills/lean-startup/SKILL.md +160 -0
  214. package/skills/lean-startup/assets/example_asset.txt +1 -0
  215. package/skills/lean-startup/evals/evals.json +43 -0
  216. package/skills/lean-startup/evals/results.json +13 -0
  217. package/skills/lean-startup/examples/after.md +80 -0
  218. package/skills/lean-startup/examples/before.md +34 -0
  219. package/skills/lean-startup/references/api_reference.md +319 -0
  220. package/skills/lean-startup/references/review-checklist.md +137 -0
  221. package/skills/lean-startup/scripts/example.py +1 -0
  222. package/skills/lean-startup/scripts/new_experiment.py +286 -0
  223. package/skills/microservices-patterns/SKILL.md +384 -0
  224. package/skills/microservices-patterns/evals/evals.json +45 -0
  225. package/skills/microservices-patterns/evals/results.json +13 -0
  226. package/skills/microservices-patterns/examples/after.md +69 -0
  227. package/skills/microservices-patterns/examples/before.md +40 -0
  228. package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
  229. package/skills/microservices-patterns/references/review-checklist.md +169 -0
  230. package/skills/microservices-patterns/scripts/new_service.py +583 -0
  231. package/skills/programming-with-rust/SKILL.md +209 -0
  232. package/skills/programming-with-rust/evals/evals.json +37 -0
  233. package/skills/programming-with-rust/evals/results.json +13 -0
  234. package/skills/programming-with-rust/examples/after.md +107 -0
  235. package/skills/programming-with-rust/examples/before.md +59 -0
  236. package/skills/programming-with-rust/references/api_reference.md +152 -0
  237. package/skills/programming-with-rust/references/practices-catalog.md +335 -0
  238. package/skills/programming-with-rust/scripts/review.py +142 -0
  239. package/skills/refactoring-ui/SKILL.md +362 -0
  240. package/skills/refactoring-ui/assets/example_asset.txt +1 -0
  241. package/skills/refactoring-ui/evals/evals.json +45 -0
  242. package/skills/refactoring-ui/evals/results.json +13 -0
  243. package/skills/refactoring-ui/examples/after.md +85 -0
  244. package/skills/refactoring-ui/examples/before.md +58 -0
  245. package/skills/refactoring-ui/references/api_reference.md +355 -0
  246. package/skills/refactoring-ui/references/review-checklist.md +114 -0
  247. package/skills/refactoring-ui/scripts/audit_css.py +250 -0
  248. package/skills/refactoring-ui/scripts/example.py +1 -0
  249. package/skills/rust-in-action/SKILL.md +350 -0
  250. package/skills/rust-in-action/evals/evals.json +38 -0
  251. package/skills/rust-in-action/evals/results.json +13 -0
  252. package/skills/rust-in-action/examples/after.md +156 -0
  253. package/skills/rust-in-action/examples/before.md +56 -0
  254. package/skills/rust-in-action/references/practices-catalog.md +346 -0
  255. package/skills/rust-in-action/scripts/review.py +147 -0
  256. package/skills/skill-router/SKILL.md +186 -0
  257. package/skills/skill-router/evals/evals.json +38 -0
  258. package/skills/skill-router/evals/results.json +13 -0
  259. package/skills/skill-router/examples/after.md +63 -0
  260. package/skills/skill-router/examples/before.md +39 -0
  261. package/skills/skill-router/references/api_reference.md +24 -0
  262. package/skills/skill-router/references/routing-heuristics.md +89 -0
  263. package/skills/skill-router/references/skill-catalog.md +174 -0
  264. package/skills/skill-router/scripts/route.py +266 -0
  265. package/skills/spring-boot-in-action/SKILL.md +340 -0
  266. package/skills/spring-boot-in-action/evals/evals.json +39 -0
  267. package/skills/spring-boot-in-action/evals/results.json +13 -0
  268. package/skills/spring-boot-in-action/examples/after.md +185 -0
  269. package/skills/spring-boot-in-action/examples/before.md +84 -0
  270. package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
  271. package/skills/spring-boot-in-action/scripts/review.py +184 -0
  272. package/skills/storytelling-with-data/SKILL.md +241 -0
  273. package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
  274. package/skills/storytelling-with-data/evals/evals.json +47 -0
  275. package/skills/storytelling-with-data/evals/results.json +13 -0
  276. package/skills/storytelling-with-data/examples/after.md +50 -0
  277. package/skills/storytelling-with-data/examples/before.md +33 -0
  278. package/skills/storytelling-with-data/references/api_reference.md +379 -0
  279. package/skills/storytelling-with-data/references/review-checklist.md +111 -0
  280. package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
  281. package/skills/storytelling-with-data/scripts/example.py +1 -0
  282. package/skills/system-design-interview/SKILL.md +233 -0
  283. package/skills/system-design-interview/assets/example_asset.txt +1 -0
  284. package/skills/system-design-interview/evals/evals.json +46 -0
  285. package/skills/system-design-interview/evals/results.json +13 -0
  286. package/skills/system-design-interview/examples/after.md +94 -0
  287. package/skills/system-design-interview/examples/before.md +27 -0
  288. package/skills/system-design-interview/references/api_reference.md +582 -0
  289. package/skills/system-design-interview/references/review-checklist.md +201 -0
  290. package/skills/system-design-interview/scripts/example.py +1 -0
  291. package/skills/system-design-interview/scripts/new_design.py +421 -0
  292. package/skills/using-asyncio-python/SKILL.md +290 -0
  293. package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
  294. package/skills/using-asyncio-python/evals/evals.json +43 -0
  295. package/skills/using-asyncio-python/evals/results.json +13 -0
  296. package/skills/using-asyncio-python/examples/after.md +68 -0
  297. package/skills/using-asyncio-python/examples/before.md +39 -0
  298. package/skills/using-asyncio-python/references/api_reference.md +267 -0
  299. package/skills/using-asyncio-python/references/review-checklist.md +149 -0
  300. package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
  301. package/skills/using-asyncio-python/scripts/example.py +1 -0
  302. package/skills/web-scraping-python/SKILL.md +280 -0
  303. package/skills/web-scraping-python/assets/example_asset.txt +1 -0
  304. package/skills/web-scraping-python/evals/evals.json +46 -0
  305. package/skills/web-scraping-python/evals/results.json +13 -0
  306. package/skills/web-scraping-python/examples/after.md +109 -0
  307. package/skills/web-scraping-python/examples/before.md +40 -0
  308. package/skills/web-scraping-python/references/api_reference.md +393 -0
  309. package/skills/web-scraping-python/references/review-checklist.md +163 -0
  310. package/skills/web-scraping-python/scripts/example.py +1 -0
  311. package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
  312. package/skills/writing-plans/audit.json +34 -0
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { scanDependencies, checkPublishDate, CUTOFF_DATE, CONCURRENCY } from './registries.js';
4
+
5
+ /**
6
+ * Detect knowledge gaps in the current project by checking dependency
7
+ * publish dates against the model training cutoff.
8
+ */
9
+ export class GapDetector {
10
+ constructor(opts = {}) {
11
+ this.cutoffDate = opts.cutoffDate ?? CUTOFF_DATE;
12
+ this.cachePath = opts.cachePath ?? path.join(process.cwd(), '.booklib', 'version-cache.json');
13
+ this.cacheTtlMs = opts.cacheTtlMs ?? 24 * 60 * 60 * 1000; // 24h
14
+ }
15
+
16
+ /**
17
+ * Scan project and detect all gaps.
18
+ * @param {string} projectDir
19
+ * @returns {Promise<{postTraining: Array, uncapturedDocs: Array, ecosystems: string[], totalDeps: number, checkedDeps: number}>}
20
+ */
21
+ async detect(projectDir) {
22
+ const deps = scanDependencies(projectDir);
23
+ const ecosystems = [...new Set(deps.map(d => d.ecosystem))];
24
+
25
+ const cache = this._loadCache();
26
+
27
+ // Process deps in batches to avoid blasting registries with hundreds of concurrent requests
28
+ const results = [];
29
+ for (let i = 0; i < deps.length; i += CONCURRENCY) {
30
+ const batch = deps.slice(i, i + CONCURRENCY);
31
+ const batchResults = await Promise.all(batch.map(dep => this._checkDep(dep, cache)));
32
+ results.push(...batchResults);
33
+ // Save cache after each batch so progress survives crashes
34
+ this._saveCache(cache);
35
+ }
36
+
37
+ // Aggregate — no shared mutable state during async work
38
+ const postTraining = results.filter(r => r.postTraining).map(r => r.postTraining);
39
+ const checked = results.map(r => r.dep);
40
+
41
+ const uncapturedDocs = this._scanProjectDocs(projectDir);
42
+
43
+ return {
44
+ postTraining,
45
+ uncapturedDocs,
46
+ ecosystems,
47
+ totalDeps: deps.length,
48
+ checkedDeps: checked.length,
49
+ };
50
+ }
51
+
52
+ /** Check a single dep against cache or registry, returning result without side effects. */
53
+ async _checkDep(dep, cache) {
54
+ const cacheKey = `${dep.ecosystem}:${dep.name}@${dep.version}`;
55
+
56
+ if (cache[cacheKey] && Date.now() - cache[cacheKey].checkedAt < this.cacheTtlMs) {
57
+ if (cache[cacheKey].publishDate) {
58
+ const pubDate = new Date(cache[cacheKey].publishDate);
59
+ if (pubDate > this.cutoffDate) {
60
+ return { dep, postTraining: { ...dep, publishDate: pubDate } };
61
+ }
62
+ }
63
+ return { dep, postTraining: null };
64
+ }
65
+
66
+ const publishDate = await checkPublishDate(dep);
67
+ cache[cacheKey] = {
68
+ publishDate: publishDate?.toISOString() ?? null,
69
+ checkedAt: Date.now(),
70
+ };
71
+
72
+ if (publishDate && publishDate > this.cutoffDate) {
73
+ return { dep, postTraining: { ...dep, publishDate } };
74
+ }
75
+ return { dep, postTraining: null };
76
+ }
77
+
78
+ /**
79
+ * Find project documentation directories and files that are not yet
80
+ * connected to BookLib as knowledge sources.
81
+ * @param {string} projectDir
82
+ * @returns {Array<{path: string, type: string, fileCount: number}>}
83
+ */
84
+ _scanProjectDocs(projectDir) {
85
+ const docPaths = ['docs', 'decisions', 'specs', 'adrs', 'architecture'];
86
+ const docFiles = ['ARCHITECTURE.md', 'CONVENTIONS.md', 'DECISIONS.md', 'ADR.md'];
87
+
88
+ // Check which docs are already connected as BookLib sources
89
+ const connectedNames = new Set();
90
+ try {
91
+ const sourcesPath = path.join(projectDir, '.booklib', 'sources.json');
92
+ if (fs.existsSync(sourcesPath)) {
93
+ const registry = JSON.parse(fs.readFileSync(sourcesPath, 'utf8'));
94
+ for (const s of registry.sources ?? []) {
95
+ connectedNames.add(s.name);
96
+ }
97
+ }
98
+ } catch { /* best effort */ }
99
+
100
+ const found = [];
101
+
102
+ for (const dir of docPaths) {
103
+ if (connectedNames.has(dir)) continue; // already connected
104
+ const full = path.join(projectDir, dir);
105
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
106
+ const fileCount = fs.readdirSync(full).filter(f => /\.(md|mdx|txt)$/i.test(f)).length;
107
+ if (fileCount > 0) {
108
+ found.push({ path: dir, type: 'directory', fileCount });
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const file of docFiles) {
114
+ if (connectedNames.has(file)) continue;
115
+ if (fs.existsSync(path.join(projectDir, file))) {
116
+ found.push({ path: file, type: 'file', fileCount: 1 });
117
+ }
118
+ }
119
+
120
+ return found;
121
+ }
122
+
123
+ _loadCache() {
124
+ try {
125
+ return JSON.parse(fs.readFileSync(this.cachePath, 'utf8'));
126
+ } catch {
127
+ return {};
128
+ }
129
+ }
130
+
131
+ _saveCache(cache) {
132
+ try {
133
+ const dir = path.dirname(this.cachePath);
134
+ fs.mkdirSync(dir, { recursive: true });
135
+ fs.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2));
136
+ } catch { /* best effort */ }
137
+ }
138
+ }
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Resolve knowledge gaps by trying multiple sources in priority order.
6
+ * Context7 (instant) -> GitHub releases -> manual suggestion.
7
+ */
8
+ export class GapResolver {
9
+ constructor(opts = {}) {
10
+ this.outputBase = opts.outputBase ?? path.join(process.cwd(), '.booklib', 'sources');
11
+ }
12
+
13
+ /**
14
+ * Resolve a single dependency gap.
15
+ * @param {{ name: string, version: string, ecosystem: string }} dep
16
+ * @returns {Promise<{ resolved: boolean, source: string, pageCount: number, suggestion?: string }>}
17
+ */
18
+ async resolve(dep) {
19
+ // Try Context7 first — fastest, broadest coverage
20
+ const ctx7Result = await this._tryContext7(dep);
21
+ if (ctx7Result.resolved) return ctx7Result;
22
+
23
+ // Fall back to GitHub releases
24
+ const ghResult = await this._tryGitHub(dep);
25
+ if (ghResult.resolved) return ghResult;
26
+
27
+ // Last resort: manual suggestion with ecosystem-specific URL
28
+ return this._suggestManual(dep);
29
+ }
30
+
31
+ /**
32
+ * Resolve multiple gaps, returning results for each.
33
+ * @param {Array<{ name: string, version: string, ecosystem: string }>} deps
34
+ * @param {Function} [onProgress] - called with { dep, result, index, total }
35
+ * @returns {Promise<Array<{ dep, result }>>}
36
+ */
37
+ async resolveAll(deps, onProgress) {
38
+ const results = [];
39
+ for (let i = 0; i < deps.length; i++) {
40
+ const dep = deps[i];
41
+ const result = await this.resolve(dep);
42
+ results.push({ dep, result });
43
+ onProgress?.({ dep, result, index: i, total: deps.length });
44
+ }
45
+ return results;
46
+ }
47
+
48
+ /** Attempt resolution via Context7 API. */
49
+ async _tryContext7(dep) {
50
+ try {
51
+ const { Context7Connector } = await import('../connectors/context7.js');
52
+ const ctx7 = new Context7Connector();
53
+ if (!ctx7.checkAuth().ok) return { resolved: false, source: 'context7' };
54
+
55
+ const sourceName = `ctx7-${dep.name.replace(/[@/]/g, '_').replace(/^_+/, '')}`;
56
+ const outputDir = path.join(this.outputBase, sourceName);
57
+ const result = await ctx7.resolveAndFetch(
58
+ dep.name,
59
+ outputDir,
60
+ `${dep.name} v${dep.version} API`,
61
+ );
62
+
63
+ if (result.resolved && result.pageCount > 0) {
64
+ return {
65
+ resolved: true,
66
+ source: 'context7',
67
+ pageCount: result.pageCount,
68
+ sourceName,
69
+ outputDir,
70
+ };
71
+ }
72
+ } catch { /* Context7 unavailable */ }
73
+ return { resolved: false, source: 'context7' };
74
+ }
75
+
76
+ /** Attempt resolution via GitHub release notes. npm-only. */
77
+ async _tryGitHub(dep) {
78
+ if (dep.ecosystem !== 'npm') return { resolved: false, source: 'github' };
79
+
80
+ try {
81
+ const { GitHubConnector } = await import('../connectors/github.js');
82
+ const gh = new GitHubConnector();
83
+ if (!gh.checkAuth().ok) return { resolved: false, source: 'github' };
84
+
85
+ // Resolve npm package -> GitHub repo via npm registry
86
+ const res = await fetch(
87
+ `https://registry.npmjs.org/${encodeURIComponent(dep.name)}`,
88
+ {
89
+ headers: { 'Accept': 'application/vnd.npm.install-v1+json' },
90
+ signal: AbortSignal.timeout(5000),
91
+ },
92
+ );
93
+ if (!res.ok) return { resolved: false, source: 'github' };
94
+
95
+ const data = await res.json();
96
+ const repoUrl = data.repository?.url ?? '';
97
+ const match = repoUrl.match(/github\.com[/:]([^/]+\/[^/.]+)/);
98
+ if (!match) return { resolved: false, source: 'github' };
99
+
100
+ const repo = match[1];
101
+ const sourceName = `gh-${dep.name}-releases`;
102
+ const outputDir = path.join(this.outputBase, sourceName);
103
+ const result = await gh.fetchReleases(repo, outputDir, { limit: 5 });
104
+
105
+ if (result.pageCount > 0) {
106
+ return {
107
+ resolved: true,
108
+ source: 'github',
109
+ pageCount: result.pageCount,
110
+ sourceName,
111
+ outputDir,
112
+ };
113
+ }
114
+ } catch { /* GitHub unavailable */ }
115
+ return { resolved: false, source: 'github' };
116
+ }
117
+
118
+ /** Build a manual suggestion with ecosystem-specific URL. */
119
+ _suggestManual(dep) {
120
+ const suggestions = {
121
+ npm: `booklib connect https://www.npmjs.com/package/${dep.name} --type=framework-docs`,
122
+ pypi: `booklib connect https://pypi.org/project/${dep.name}/ --type=framework-docs`,
123
+ crates: `booklib connect https://docs.rs/${dep.name} --type=framework-docs`,
124
+ go: `booklib connect https://pkg.go.dev/${dep.name} --type=framework-docs`,
125
+ rubygems: `booklib connect https://rubygems.org/gems/${dep.name} --type=framework-docs`,
126
+ maven: `booklib connect https://search.maven.org/artifact/${dep.name.replace(':', '/')} --type=framework-docs`,
127
+ };
128
+ return {
129
+ resolved: false,
130
+ source: 'manual',
131
+ pageCount: 0,
132
+ suggestion: suggestions[dep.ecosystem] ?? 'booklib connect <docs-url> --type=framework-docs',
133
+ };
134
+ }
135
+ }
@@ -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,183 @@
1
+ import { listNodes, loadNode, parseNodeFrontmatter, loadEdges, traverseEdges } from './graph.js';
2
+ import { extractKeywords } from './query-expander.js';
3
+
4
+ /**
5
+ * Multi-dimensional graph-activated search.
6
+ * Parses query into concepts, activates graph subregions for each,
7
+ * scores nodes by how many concepts they connect to, merges with text search results.
8
+ *
9
+ * @param {string} query
10
+ * @param {Array} textSearchResults - results from BookLibSearcher.search()
11
+ * @param {object} opts
12
+ * @param {string} [opts.nodesDir] - knowledge nodes directory
13
+ * @param {string} [opts.graphFile] - graph.jsonl path
14
+ * @returns {object} { concepts, graphResults, mergedResults, activated }
15
+ */
16
+ export function graphActivatedSearch(query, textSearchResults = [], opts = {}) {
17
+ const { nodesDir, graphFile } = opts;
18
+
19
+ const concepts = extractConcepts(query);
20
+
21
+ // Single concept or no concepts: skip graph activation — regular search is sufficient
22
+ if (concepts.length < 2) {
23
+ return {
24
+ concepts,
25
+ graphResults: [],
26
+ mergedResults: textSearchResults,
27
+ activated: false,
28
+ };
29
+ }
30
+
31
+ const allNodeIds = listNodes({ nodesDir });
32
+ const edges = loadEdges({ graphFile });
33
+ const subgraphs = new Map();
34
+
35
+ for (const concept of concepts) {
36
+ const activated = activateSubgraph(concept, allNodeIds, edges, { nodesDir });
37
+ subgraphs.set(concept, activated);
38
+ }
39
+
40
+ const nodeScores = scoreIntersections(subgraphs);
41
+
42
+ const graphResults = [];
43
+ for (const [nodeId, { score: intersectionScore, matchedConcepts }] of nodeScores.entries()) {
44
+ if (intersectionScore < 2) continue;
45
+
46
+ const raw = loadNode(nodeId, { nodesDir });
47
+ if (!raw) continue;
48
+ const parsed = parseNodeFrontmatter(raw);
49
+
50
+ graphResults.push({
51
+ principle: parsed.title ?? nodeId,
52
+ context: (parsed.body ?? '').slice(0, 150),
53
+ source: `project ${parsed.type ?? 'knowledge'}: ${nodeId}`,
54
+ section: 'knowledge',
55
+ matchedConcepts,
56
+ intersectionScore,
57
+ });
58
+ }
59
+
60
+ graphResults.sort((a, b) => b.intersectionScore - a.intersectionScore);
61
+
62
+ const textPrinciples = textSearchResults.map(r => ({
63
+ principle: r.text?.slice(0, 150) ?? '',
64
+ context: '',
65
+ source: r.metadata?.name ?? 'unknown',
66
+ section: r.metadata?.type ?? 'content',
67
+ matchedConcepts: [],
68
+ intersectionScore: 0,
69
+ score: r.score,
70
+ }));
71
+
72
+ const mergedResults = [...graphResults, ...textPrinciples];
73
+
74
+ return {
75
+ concepts,
76
+ graphResults,
77
+ mergedResults,
78
+ activated: true,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Extract semantic concepts from a query.
84
+ * Groups consecutive keywords into compound concepts.
85
+ *
86
+ * @param {string} query
87
+ * @returns {string[]}
88
+ */
89
+ export function extractConcepts(query) {
90
+ const keywords = extractKeywords(query);
91
+ if (keywords.length === 0) return [];
92
+
93
+ // Collect runs of consecutive keywords from the original query
94
+ const words = query.toLowerCase().split(/\s+/);
95
+ const runs = [];
96
+ let current = [];
97
+
98
+ for (const word of words) {
99
+ const isKeyword = keywords.includes(word);
100
+ if (isKeyword) {
101
+ current.push(word);
102
+ } else if (current.length > 0) {
103
+ runs.push(current);
104
+ current = [];
105
+ }
106
+ }
107
+ if (current.length > 0) {
108
+ runs.push(current);
109
+ }
110
+
111
+ // Split runs into concepts: compound terms (max 2 words) stay grouped,
112
+ // longer runs are split into individual concepts
113
+ const concepts = [];
114
+ for (const run of runs) {
115
+ if (run.length <= 2) {
116
+ concepts.push(run.join(' '));
117
+ } else {
118
+ for (const word of run) {
119
+ concepts.push(word);
120
+ }
121
+ }
122
+ }
123
+
124
+ return [...new Set(concepts)];
125
+ }
126
+
127
+ /**
128
+ * Activate a subgraph for a single concept.
129
+ * Finds matching nodes + their 1-hop neighbors.
130
+ *
131
+ * @param {string} concept
132
+ * @param {string[]} allNodeIds
133
+ * @param {Array} edges
134
+ * @param {{ nodesDir?: string }} opts
135
+ * @returns {Set<string>} activated node IDs
136
+ */
137
+ function activateSubgraph(concept, allNodeIds, edges, { nodesDir } = {}) {
138
+ const activated = new Set();
139
+ const conceptWords = concept.split(/\s+/);
140
+
141
+ for (const nodeId of allNodeIds) {
142
+ const raw = loadNode(nodeId, { nodesDir });
143
+ if (!raw) continue;
144
+ const parsed = parseNodeFrontmatter(raw);
145
+
146
+ const nodeText = `${parsed.title ?? ''} ${parsed.body ?? ''} ${(parsed.tags ?? []).join(' ')}`.toLowerCase();
147
+
148
+ const matches = conceptWords.some(w => w.length > 2 && nodeText.includes(w));
149
+ if (matches) {
150
+ activated.add(nodeId);
151
+
152
+ const neighbors = traverseEdges(nodeId, edges, 1);
153
+ for (const { id } of neighbors) {
154
+ activated.add(id);
155
+ }
156
+ }
157
+ }
158
+
159
+ return activated;
160
+ }
161
+
162
+ /**
163
+ * Score nodes by how many concept subgraphs they appear in.
164
+ *
165
+ * @param {Map<string, Set<string>>} subgraphs
166
+ * @returns {Map<string, { score: number, matchedConcepts: string[] }>}
167
+ */
168
+ function scoreIntersections(subgraphs) {
169
+ const scores = new Map();
170
+
171
+ for (const [concept, activated] of subgraphs.entries()) {
172
+ for (const nodeId of activated) {
173
+ if (!scores.has(nodeId)) {
174
+ scores.set(nodeId, { score: 0, matchedConcepts: [] });
175
+ }
176
+ const entry = scores.get(nodeId);
177
+ entry.score++;
178
+ entry.matchedConcepts.push(concept);
179
+ }
180
+ }
181
+
182
+ return scores;
183
+ }