@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,574 @@
1
+ /**
2
+ * ContextBuilder — task-aware cross-skill knowledge synthesizer.
3
+ *
4
+ * Given a task description:
5
+ * 1. Decomposes the task into search sub-queries
6
+ * 2. Searches all indexed skills for each sub-query
7
+ * 3. Groups results by skill — keeps the best chunk per skill
8
+ * 4. Within each matched chunk, extracts the most relevant paragraph
9
+ * and labels it with book title + section (where in the skill it came from)
10
+ * 5. Resolves conflicts using ConflictResolver:
11
+ * - Auto-resolved: shown with full prose rationale, non-blocking
12
+ * - Genuine conflict: user is prompted to choose interactively
13
+ * 6. Compiles a sharp, dense system prompt from all resolved knowledge,
14
+ * each piece cited with its source book and section
15
+ *
16
+ * Usage:
17
+ * const builder = new ContextBuilder();
18
+ * const output = await builder.build('implement a Kotlin payment service with async error handling');
19
+ * // pipe-friendly: builder.build(task, { promptOnly: true })
20
+ */
21
+
22
+ import * as rl from 'node:readline/promises';
23
+ import { stdin as input, stdout as output } from 'node:process';
24
+ import { BookLibSearcher } from './engine/searcher.js';
25
+ import { ConflictResolver } from './conflict-resolver.js';
26
+ import { resolveBookLibPaths } from './paths.js';
27
+ import { buildGraphContext } from './engine/graph-injector.js';
28
+
29
+ // ── Book label map ──────────────────────────────────────────────────────────
30
+ const BOOK_LABELS = {
31
+ 'clean-code-reviewer': 'Clean Code — Robert C. Martin',
32
+ 'effective-kotlin': 'Effective Kotlin — Marcin Moskała',
33
+ 'effective-java': 'Effective Java — Joshua Bloch',
34
+ 'effective-python': 'Effective Python — Brett Slatkin',
35
+ 'effective-typescript': 'Effective TypeScript — Dan Vanderkam',
36
+ 'domain-driven-design': 'Domain-Driven Design — Eric Evans',
37
+ 'microservices-patterns': 'Microservices Patterns — Chris Richardson',
38
+ 'system-design-interview':'System Design Interview — Alex Xu',
39
+ 'data-intensive-patterns':'Designing Data-Intensive Applications — Martin Kleppmann',
40
+ 'data-pipelines': 'Data Pipelines Pocket Reference — James Densmore',
41
+ 'design-patterns': 'Head First Design Patterns — Freeman & Robson',
42
+ 'kotlin-in-action': 'Kotlin in Action — Elizarov & Isakova',
43
+ 'programming-with-rust': 'Programming with Rust — Donis Marshall',
44
+ 'rust-in-action': 'Rust in Action — Tim McNamara',
45
+ 'refactoring-ui': 'Refactoring UI — Wathan & Schoger',
46
+ 'storytelling-with-data': 'Storytelling with Data — Cole Knaflic',
47
+ 'animation-at-work': 'Animation at Work — Rachel Nabors',
48
+ 'spring-boot-in-action': 'Spring Boot in Action — Craig Walls',
49
+ 'lean-startup': 'The Lean Startup — Eric Ries',
50
+ 'using-asyncio-python': 'Using Asyncio in Python — Caleb Hattingh',
51
+ 'web-scraping-python': 'Web Scraping with Python — Ryan Mitchell',
52
+ 'skill-router': 'BookLib skill-router',
53
+ // Non-code community skills
54
+ 'writing-plans': 'Writing Plans — BookLib Community',
55
+ 'writing-skills': 'Writing Skills — BookLib Community',
56
+ 'article-writing': 'Article Writing — BookLib Community',
57
+ 'strategic-compact': 'Strategic Compact — BookLib Community',
58
+ 'product-lens': 'Product Lens — BookLib Community',
59
+ 'brand-guidelines': 'Brand Guidelines — BookLib Community',
60
+ 'web-design-guidelines': 'Web Design Guidelines — BookLib Community',
61
+ };
62
+
63
+ // Maps stored chunk type/tag to a human-readable section name
64
+ const SECTION_LABELS = {
65
+ 'framework': 'core principles',
66
+ 'core_principles': 'core principles',
67
+ 'pitfalls': 'anti-patterns',
68
+ 'anti_patterns':'anti-patterns',
69
+ 'case_studies': 'examples',
70
+ 'examples': 'examples',
71
+ 'summary': 'overview',
72
+ 'content': 'guidance',
73
+ };
74
+
75
+ function bookLabel(skillName) {
76
+ return BOOK_LABELS[skillName] ?? skillName;
77
+ }
78
+
79
+ function sectionLabel(chunk) {
80
+ const raw = chunk.metadata?.originalTag ?? chunk.metadata?.type ?? 'guidance';
81
+ return SECTION_LABELS[raw] ?? raw;
82
+ }
83
+
84
+ /**
85
+ * Split a skill chunk into individual items (principles, rules, anti-patterns).
86
+ *
87
+ * Recognises three item shapes:
88
+ * - Bold-headed bullets: "- **Item Name** — body text"
89
+ * - Numbered items: "1. **Item Name** — body" or "1. Plain body"
90
+ * - Markdown headings: "## § Section Name\nbody paragraphs"
91
+ * - Plain bullets: "- body without a bold header"
92
+ *
93
+ * Returns Array<{ label: string|null, body: string, raw: string }>
94
+ */
95
+ function extractItems(text) {
96
+ if (!text) return [];
97
+
98
+ // Strip YAML frontmatter block (ECC community skills include it in chunk text)
99
+ text = text.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '').trim();
100
+ if (!text) return [];
101
+
102
+ const items = [];
103
+
104
+ // Split into blocks at heading or double-newline boundaries
105
+ const blocks = text
106
+ .split(/\n(?=#{1,3} )|(?<=\n)\n(?=\S)|\n{3,}/)
107
+ .map(b => b.trim())
108
+ .filter(b => b.length > 10);
109
+
110
+ for (const block of blocks) {
111
+ // Case 1: Markdown heading block
112
+ const headingMatch = block.match(/^(#{1,3})\s+(.+)\n([\s\S]*)/);
113
+ if (headingMatch) {
114
+ const label = headingMatch[2].replace(/^\*+|\*+$/g, '').trim();
115
+ const body = headingMatch[3].trim();
116
+ items.push({ label, body: body.slice(0, 300), raw: block });
117
+ continue;
118
+ }
119
+
120
+ // Case 2: Bold-headed bullet list — split each bullet
121
+ if (/^- \*\*/.test(block) || /^\d+\.\s+\*\*/.test(block)) {
122
+ const bulletRe = /^(?:-|\d+\.)\s+\*\*([^*]+)\*\*\s*[—–:-]?\s*([\s\S]*?)(?=\n(?:-|\d+\.)\s+\*\*|\n#{1,3} |$)/gm;
123
+ let m;
124
+ while ((m = bulletRe.exec(block)) !== null) {
125
+ const label = m[1].trim();
126
+ const body = m[2].replace(/\n/g, ' ').trim().slice(0, 250);
127
+ if (label && body) items.push({ label, body, raw: m[0] });
128
+ }
129
+ // If regex matched nothing, fall through to plain bullet handling
130
+ if (items.length > 0) continue;
131
+ }
132
+
133
+ // Case 3: Plain bullets — each bullet becomes its own item (no label)
134
+ if (/^- /.test(block)) {
135
+ const bullets = block
136
+ .split(/\n(?=- )/)
137
+ .map(b => b.replace(/^- /, '').trim())
138
+ .filter(b => b.length > 15);
139
+ for (const b of bullets) {
140
+ items.push({ label: null, body: b.slice(0, 250), raw: b });
141
+ }
142
+ continue;
143
+ }
144
+
145
+ // Case 4: Plain paragraph — skip if it looks like a heading, code fence, or XML
146
+ if (/^#{1,3} /.test(block) || /^```/.test(block) || /^<[a-z]/.test(block)) continue;
147
+
148
+ // Case 4a: Long prose — split into individual sentences so rankItems can score each one.
149
+ // Covers narrative books (strategy, writing, product, legal) that don't use bullet structure.
150
+ if (block.length > 150) {
151
+ const sentences = block
152
+ .split(/(?<=[.!?])\s+(?=[A-Z"'])/)
153
+ .map(s => s.trim())
154
+ .filter(s => s.length > 30 && !/^\[!\[/.test(s) && !/^!\[/.test(s));
155
+ if (sentences.length >= 2) {
156
+ for (const s of sentences) {
157
+ items.push({ label: null, body: s.slice(0, 300), raw: s });
158
+ }
159
+ continue;
160
+ }
161
+ }
162
+ items.push({ label: null, body: block.slice(0, 300), raw: block });
163
+ }
164
+
165
+ // Filter and clean items
166
+ return items
167
+ .map(item => {
168
+ let b = item.body.trim();
169
+ // Strip "You should generate:" prefix from example chunks — keep the list
170
+ b = b.replace(/^You should generate:\s*/i, '');
171
+ // Strip "Actions:" prefix
172
+ b = b.replace(/^Actions:\s*/i, '');
173
+ return { ...item, body: b.trim() };
174
+ })
175
+ .filter(item => {
176
+ const b = item.body.trim();
177
+ if (b.length < 20) return false;
178
+ if (/^#+ /.test(b)) return false; // naked heading
179
+ if (/^<[a-z]/.test(b)) return false; // XML tag
180
+ if (/^```/.test(b)) return false; // code fence
181
+ if (/^[-─═]{3,}/.test(b)) return false; // horizontal rule / YAML separator
182
+ if (/^You are (an? |the )/.test(b)) return false; // skill meta-instruction
183
+ if (/^You help (developers|teams|users)/.test(b)) return false;
184
+ if (/^(name|description|origin|version|tags|author):\s+\S/.test(b)) return false; // YAML frontmatter lines
185
+ if (/^\[!\[/.test(b)) return false; // badge markdown [![img](url)](url)
186
+ if (/^!\[/.test(b)) return false; // inline image ![alt](url)
187
+ return true;
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Score items by keyword overlap with the query, return top N.
193
+ * Items with a label score higher (they're named principles, not noise).
194
+ */
195
+ function rankItems(items, queryWords, topN = 3) {
196
+ if (items.length === 0) return [];
197
+
198
+ const scored = items.map(item => {
199
+ const text = `${item.label ?? ''} ${item.body}`.toLowerCase();
200
+ const hits = queryWords.reduce((n, w) => n + (text.includes(w) ? 1 : 0), 0);
201
+ const labelBonus = item.label ? 0.5 : 0;
202
+ return { item, score: hits + labelBonus };
203
+ });
204
+
205
+ scored.sort((a, b) => b.score - a.score);
206
+ return scored.slice(0, topN).map(s => s.item);
207
+ }
208
+
209
+ function hr(label = '', width = 64) {
210
+ const pad = label ? ` ${label} ` : '';
211
+ const dashes = '─'.repeat(Math.max(2, width - pad.length - 4));
212
+ return `── ${pad}${dashes}`;
213
+ }
214
+
215
+ // ── ContextBuilder ──────────────────────────────────────────────────────────
216
+
217
+ export class ContextBuilder {
218
+ constructor(options = {}) {
219
+ const paths = resolveBookLibPaths(options.projectCwd);
220
+ this._searcher = new BookLibSearcher(paths.indexPath);
221
+ }
222
+
223
+ /**
224
+ * Build context for a task.
225
+ * @param {string} task
226
+ * @param {object} opts
227
+ * @param {boolean} opts.promptOnly - Skip the report, output only the prompt block
228
+ */
229
+ async build(task, { promptOnly = false } = {}) {
230
+ const queries = this._decomposeTask(task);
231
+ const queryWords = task.toLowerCase().split(/\W+/).filter(w => w.length > 3);
232
+
233
+ const bySkill = await this._searchAndGroup(queries);
234
+
235
+ if (bySkill.size === 0) {
236
+ return 'No indexed skills found. Run `booklib index` first.';
237
+ }
238
+
239
+ // Enrich each chunk: extract item-level knowledge + section label
240
+ for (const [, chunk] of bySkill) {
241
+ chunk._section = sectionLabel(chunk);
242
+ chunk._items = rankItems(extractItems(chunk.text), queryWords, 3);
243
+ }
244
+
245
+ // Resolve conflicts
246
+ const chunks = [...bySkill.values()];
247
+ const resolver = new ConflictResolver();
248
+ const { winners, suppressed, conflicts } = resolver.resolveChunks(chunks);
249
+
250
+ // Build suppression lookup: skillName → { rationale, winnerName }
251
+ const suppressionMap = new Map(
252
+ suppressed.map(s => {
253
+ const winnerMatch = (s._rationale ?? '').match(/`([^`]+)`/);
254
+ return [s._skill, { rationale: s._rationale, winner: winnerMatch?.[1] }];
255
+ })
256
+ );
257
+
258
+ // Resolve genuine conflicts interactively or auto
259
+ const decisions = [];
260
+ const extraWinners = [];
261
+
262
+ for (const conflict of conflicts) {
263
+ if (promptOnly || !process.stdin.isTTY) {
264
+ const winner = chunks.find(c => c._skill === conflict.options[0].name);
265
+ if (winner) {
266
+ extraWinners.push({ ...winner, _decision: 'auto-conflict' });
267
+ decisions.push({
268
+ conflict,
269
+ chosen: conflict.options[0].name,
270
+ rejected: conflict.options.slice(1).map(o => o.name),
271
+ auto: true,
272
+ reason: `auto-resolved: highest specificity (${conflict.options[0].specificity})`,
273
+ });
274
+ }
275
+ } else {
276
+ const choice = await this._promptConflict(conflict, chunks, queryWords);
277
+ extraWinners.push({ ...choice.chunk, _decision: 'user' });
278
+ decisions.push({
279
+ conflict,
280
+ chosen: choice.skillName,
281
+ rejected: conflict.options.map(o => o.name).filter(n => n !== choice.skillName),
282
+ auto: false,
283
+ });
284
+ }
285
+ }
286
+
287
+ const allWinners = [...winners, ...extraWinners];
288
+
289
+ if (promptOnly) {
290
+ return this._compilePrompt(task, allWinners, suppressionMap, decisions, queryWords);
291
+ }
292
+
293
+ return (
294
+ this._compileReport(task, allWinners, suppressionMap, bySkill, decisions) +
295
+ '\n\n' + hr('Final prompt') + '\n\n' +
296
+ this._compilePrompt(task, allWinners, suppressionMap, decisions, queryWords)
297
+ );
298
+ }
299
+
300
+ // ── Private ─────────────────────────────────────────────────────────────────
301
+
302
+ _decomposeTask(task) {
303
+ const queries = [task];
304
+
305
+ // Split on connectors (avoid 'in' — too common, breaks phrases like "service in Kotlin")
306
+ const parts = task
307
+ .split(/\s+(?:and|with|using|via|plus|,)\s+/i)
308
+ .map(p => p.trim())
309
+ .filter(p => p.length > 4 && p !== task);
310
+
311
+ // Include all parts (up to 5), not just first 3
312
+ for (const p of parts.slice(0, 5)) queries.push(p);
313
+
314
+ // Also add multi-word noun phrases (2–3 consecutive capitalised/content words)
315
+ // e.g. "domain driven design", "async error handling"
316
+ const nounPhrases = task.match(/(?:[a-z]+ ){1,3}[a-z]+/gi) ?? [];
317
+ for (const phrase of nounPhrases) {
318
+ if (phrase.split(' ').length >= 2 && !queries.includes(phrase)) {
319
+ queries.push(phrase);
320
+ }
321
+ }
322
+
323
+ return [...new Set(queries)].slice(0, 8); // cap at 8 queries to keep search fast
324
+ }
325
+
326
+ async _searchAndGroup(queries) {
327
+ const bySkill = new Map();
328
+ for (const query of queries) {
329
+ let results = [];
330
+ // MiniLM scores for domain-specific technical content cluster at 0.28-0.55.
331
+ // Principled sections (core knowledge) get the lowest floor — they're worth
332
+ // surfacing even at modest similarity. Generic/review sections need stronger match.
333
+ const SCORE_FLOORS = {
334
+ framework: 0.27, core_principles: 0.27, pitfalls: 0.27, anti_patterns: 0.27,
335
+ content: 0.29, guidelines: 0.29,
336
+ summary: 0.38, overview: 0.38, // generic meta-descriptions — need stronger match
337
+ case_studies: 0.40, examples: 0.40, // narrow examples — need strong match
338
+ strengths_to_praise: 0.40, // review-mode content, not design guidance
339
+ };
340
+ const DEFAULT_FLOOR = 0.35;
341
+
342
+ // Fetch 30 candidates — duplicates in the index can fill slots, so we over-fetch then deduplicate
343
+ try { results = await this._searcher.search(query, 30, 0.25); } catch { /* not indexed */ }
344
+ // Deduplicate by (skillName, type, content fingerprint) — index can have duplicate entries
345
+ const seen = new Set();
346
+ results = results.filter(chunk => {
347
+ const key = `${chunk.metadata?.name}|${chunk.metadata?.type}|${(chunk.text ?? '').slice(0, 60)}`;
348
+ if (seen.has(key)) return false;
349
+ seen.add(key);
350
+ return true;
351
+ });
352
+
353
+ for (const chunk of results) {
354
+ // name is in frontmatter for SKILL.md chunks; reference files derive it from filePath
355
+ const skillName = chunk.metadata?.name
356
+ ?? chunk.metadata?.filePath?.split('/')[0]
357
+ ?? null;
358
+ if (!skillName) continue;
359
+
360
+ // Apply per-type score floor
361
+ const chunkType = chunk.metadata?.type ?? '';
362
+ const floor = SCORE_FLOORS[chunkType] ?? DEFAULT_FLOOR;
363
+ if (chunk.score < floor) continue;
364
+
365
+ // Normalise chunk so _skill and metadata.name are always set
366
+ const normChunk = { ...chunk, _skill: skillName, metadata: { ...chunk.metadata, name: skillName } };
367
+ const existing = bySkill.get(skillName);
368
+ if (!existing) {
369
+ bySkill.set(skillName, normChunk);
370
+ continue;
371
+ }
372
+ // Prefer principled sections over overview/examples
373
+ const principled = ['framework', 'core_principles', 'pitfalls', 'anti_patterns', 'content', 'guidelines'];
374
+ const newType = chunkType;
375
+ const existingType = existing.metadata?.type ?? '';
376
+ const newIsPrincipled = principled.includes(newType);
377
+ const existingIsPrincipled = principled.includes(existingType);
378
+ const scoreDelta = chunk.score - existing.score;
379
+ if (
380
+ (newIsPrincipled && !existingIsPrincipled) ||
381
+ (newIsPrincipled === existingIsPrincipled && scoreDelta > 0)
382
+ ) {
383
+ bySkill.set(skillName, normChunk);
384
+ }
385
+ }
386
+ }
387
+ return bySkill;
388
+ }
389
+
390
+ async _promptConflict(conflict, chunks, queryWords) {
391
+ const iface = rl.createInterface({ input, output });
392
+ const options = conflict.options;
393
+
394
+ console.log('');
395
+ console.log(hr(`Conflict — ${conflict.topic}`));
396
+ console.log(` Both skills are equally applicable. Which should guide this decision?\n`);
397
+
398
+ for (let i = 0; i < options.length; i++) {
399
+ const letter = String.fromCharCode(97 + i);
400
+ const chunk = chunks.find(c => c._skill === options[i].name);
401
+ const book = bookLabel(options[i].name);
402
+ const section = chunk?._section ?? 'guidance';
403
+ const items = chunk?._items ?? [];
404
+
405
+ console.log(` [${letter}] ${book} (${section})`);
406
+ for (const item of items.slice(0, 2)) {
407
+ if (item.label) {
408
+ console.log(` • ${item.label}: ${item.body.slice(0, 100)}…`);
409
+ } else {
410
+ console.log(` • ${item.body.slice(0, 110)}…`);
411
+ }
412
+ }
413
+ console.log('');
414
+ }
415
+
416
+ let answer = '';
417
+ while (true) {
418
+ answer = (await iface.question(' → Your choice: ')).trim().toLowerCase();
419
+ const idx = answer.charCodeAt(0) - 97;
420
+ if (idx >= 0 && idx < options.length) {
421
+ iface.close();
422
+ return {
423
+ skillName: options[idx].name,
424
+ chunk: chunks.find(c => c._skill === options[idx].name),
425
+ };
426
+ }
427
+ console.log(` Invalid — enter a–${String.fromCharCode(97 + options.length - 1)}.`);
428
+ }
429
+ }
430
+
431
+ // ── Report (full explanation view) ─────────────────────────────────────────
432
+
433
+ _compileReport(task, winners, suppressionMap, bySkill, decisions) {
434
+ const lines = [
435
+ `Context for: "${task}"`,
436
+ '─'.repeat(64),
437
+ `${bySkill.size} skills matched · ${winners.length} selected · ${suppressionMap.size} suppressed`,
438
+ '',
439
+ ];
440
+
441
+ for (const w of winners) {
442
+ const book = bookLabel(w._skill);
443
+ const section = w._section ?? 'guidance';
444
+ const items = w._items ?? [];
445
+ const score = (w.score ?? 0).toFixed(2);
446
+
447
+ const confidence = w.score >= 0.45 ? '' : w.score >= 0.35 ? ' ⚠ low confidence' : ' ⚠ borderline match';
448
+ lines.push(hr(w._skill));
449
+ lines.push(` 📖 ${book}`);
450
+ lines.push(` Section: ${section} Relevance: ${score}${confidence}`);
451
+ lines.push('');
452
+
453
+ if (items.length > 0) {
454
+ for (const item of items) {
455
+ if (item.label) {
456
+ lines.push(` § ${item.label}`);
457
+ lines.push(` ${item.body}`);
458
+ } else {
459
+ lines.push(` • ${item.body}`);
460
+ }
461
+ lines.push('');
462
+ }
463
+ }
464
+
465
+
466
+ // Show auto-decision rationale (non-blocking, always visible)
467
+ if (w._decision === 'auto' && w._rationale) {
468
+ lines.push(` ✓ Auto-selected`);
469
+ lines.push(` ${w._rationale}`);
470
+
471
+ // Show what was suppressed in favour of this skill and why
472
+ for (const [suppName, { rationale, winner }] of suppressionMap) {
473
+ if (winner === w._skill) {
474
+ lines.push(` ↳ suppressed: ${bookLabel(suppName)}`);
475
+ lines.push(` Reason: ${rationale}`);
476
+ }
477
+ }
478
+ lines.push('');
479
+ }
480
+
481
+ // Show user decision rationale
482
+ const dec = decisions.find(d => d.chosen === w._skill);
483
+ if (dec && w._decision === 'user') {
484
+ const rejected = dec.rejected.map(r => bookLabel(r)).join(', ');
485
+ lines.push(` ✓ Your choice — selected over: ${rejected}`);
486
+ lines.push('');
487
+ }
488
+ if (dec && w._decision === 'auto-conflict') {
489
+ const rejected = dec.rejected.map(r => bookLabel(r)).join(', ');
490
+ lines.push(` ✓ Auto-resolved conflict`);
491
+ lines.push(` ${dec.reason}`);
492
+ lines.push(` ↳ suppressed: ${rejected}`);
493
+ lines.push('');
494
+ }
495
+ }
496
+
497
+ return lines.join('\n');
498
+ }
499
+
500
+ // ── Graph-augmented context ─────────────────────────────────────────────────
501
+
502
+ /**
503
+ * Builds combined context: skill chunks + knowledge graph nodes.
504
+ * @param {string} task - Task description
505
+ * @param {string|null} filePath - Current file path for component matching
506
+ * @returns {Promise<string>} Formatted context string
507
+ */
508
+ async buildWithGraph(task, filePath = null) {
509
+ const skillContext = await this.build(task);
510
+
511
+ const searcher = new BookLibSearcher();
512
+ const graphNodes = await buildGraphContext({ filePath, taskContext: task, searcher });
513
+
514
+ if (graphNodes.length === 0) return skillContext;
515
+
516
+ const nodeSection = graphNodes
517
+ .map(node => `### 📝 ${node.title} [${node.type}]\n${node.body}`)
518
+ .join('\n\n---\n\n');
519
+
520
+ return `${skillContext}\n\n---\n\n## Knowledge Graph Context\n\n${nodeSection}`;
521
+ }
522
+
523
+ // ── Prompt (sharp injectable block) ────────────────────────────────────────
524
+
525
+ _compilePrompt(task, winners, suppressionMap, decisions, queryWords) {
526
+ const lines = [
527
+ `You are working on: ${task}`,
528
+ '',
529
+ 'Apply the following principles from canonical books and sources:',
530
+ '',
531
+ ];
532
+
533
+ for (const w of winners) {
534
+ const book = bookLabel(w._skill);
535
+ const section = w._section ?? 'guidance';
536
+ const items = w._items ?? [];
537
+
538
+ lines.push(`**${book}** — ${section}`);
539
+
540
+ // Show each specific item with its label
541
+ for (const item of items) {
542
+ if (item.label) {
543
+ lines.push(`- **${item.label}**: ${item.body}`);
544
+ } else {
545
+ lines.push(`- ${item.body}`);
546
+ }
547
+ }
548
+
549
+ // Inline decision note
550
+ const dec = decisions.find(d => d.chosen === w._skill);
551
+ if (dec) {
552
+ const how = dec.auto ? 'auto-resolved' : 'your choice';
553
+ const over = dec.rejected.map(r => bookLabel(r)).join(', ');
554
+ lines.push(`*(${how} over: ${over})*`);
555
+ } else if (w._decision === 'auto' && w._rationale) {
556
+ lines.push(`*(auto-selected: ${w._rationale})*`);
557
+ }
558
+
559
+ lines.push('');
560
+ }
561
+
562
+ const suppressed = [...suppressionMap.keys()];
563
+ if (suppressed.length > 0) {
564
+ lines.push('---');
565
+ lines.push('*Additional books matched but were suppressed — more specific guidance above covers their domain:*');
566
+ for (const s of suppressed) {
567
+ const info = suppressionMap.get(s);
568
+ lines.push(`- **${bookLabel(s)}** — ${info.rationale ?? 'lower specificity'}`);
569
+ }
570
+ }
571
+
572
+ return lines.join('\n');
573
+ }
574
+ }