@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.
- package/.cursor/rules/booklib-standards.mdc +40 -0
- package/.gemini/context.md +372 -0
- package/AGENTS.md +166 -0
- package/CHANGELOG.md +226 -0
- package/CLAUDE.md +81 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +304 -0
- package/LICENSE +21 -0
- package/PLAN.md +28 -0
- package/README.ja.md +198 -0
- package/README.ko.md +198 -0
- package/README.md +503 -0
- package/README.pt-BR.md +198 -0
- package/README.uk.md +241 -0
- package/README.zh-CN.md +198 -0
- package/SECURITY.md +9 -0
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/assets/logo.svg +36 -0
- package/bin/booklib-mcp.js +304 -0
- package/bin/booklib.js +1705 -0
- package/bin/skills.cjs +1292 -0
- package/booklib-router.mdc +36 -0
- package/booklib.config.json +19 -0
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +23 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-behaviors.js +40 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/ai-features.js +253 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/corrections.js +198 -0
- package/lib/engine/doctor.js +195 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph.js +161 -0
- package/lib/engine/handoff.js +405 -0
- package/lib/engine/indexer.js +242 -0
- package/lib/engine/parser.js +53 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reranker.js +40 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +139 -0
- package/lib/engine/session-coordinator.js +306 -0
- package/lib/engine/session-manager.js +429 -0
- package/lib/engine/synthesizer.js +70 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +33 -0
- package/lib/mcp-config-writer.js +88 -0
- package/lib/paths.js +57 -0
- package/lib/profiles/design.md +19 -0
- package/lib/profiles/general.md +16 -0
- package/lib/profiles/research-analysis.md +22 -0
- package/lib/profiles/software-development.md +23 -0
- package/lib/profiles/writing-content.md +19 -0
- package/lib/project-initializer.js +916 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +99 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +70 -0
- package/lib/wizard/index.js +404 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +100 -0
- package/lib/wizard/prompt.js +156 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/llms-full.txt +254 -0
- package/llms.txt +70 -0
- package/package.json +45 -0
- package/research-reports/2026-04-01-current-architecture.md +160 -0
- package/research-reports/IDEAS.md +93 -0
- package/rules/common/clean-code.md +42 -0
- package/rules/java/effective-java.md +42 -0
- package/rules/kotlin/effective-kotlin.md +37 -0
- package/rules/python/effective-python.md +38 -0
- package/rules/rust/rust.md +37 -0
- package/rules/typescript/effective-typescript.md +42 -0
- package/scripts/gen-llms-full.mjs +36 -0
- package/scripts/gen-og.mjs +142 -0
- package/scripts/validate-frontmatter.js +25 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
- package/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
- package/tests/agent-detector.test.js +83 -0
- package/tests/corrections.test.js +245 -0
- package/tests/doctor/hook-installer.test.js +72 -0
- package/tests/doctor/usage-tracker.test.js +140 -0
- package/tests/engine/benchmark-eval.test.js +31 -0
- package/tests/engine/bm25-index.test.js +85 -0
- package/tests/engine/capture-command.test.js +35 -0
- package/tests/engine/capture.test.js +17 -0
- package/tests/engine/graph-augmented-search.test.js +107 -0
- package/tests/engine/graph-injector.test.js +44 -0
- package/tests/engine/graph.test.js +216 -0
- package/tests/engine/hybrid-searcher.test.js +74 -0
- package/tests/engine/indexer-bm25.test.js +37 -0
- package/tests/engine/mcp-tools.test.js +73 -0
- package/tests/engine/project-initializer-mcp.test.js +99 -0
- package/tests/engine/query-expander.test.js +36 -0
- package/tests/engine/reranker.test.js +51 -0
- package/tests/engine/rrf.test.js +49 -0
- package/tests/engine/srag-prefix.test.js +47 -0
- package/tests/instinct-block.test.js +23 -0
- package/tests/mcp-config-writer.test.js +60 -0
- package/tests/project-initializer-new-agents.test.js +48 -0
- package/tests/rules/rules-manager.test.js +230 -0
- package/tests/well-known-builder.test.js +40 -0
- package/tests/wizard/integration-detector.test.js +31 -0
- package/tests/wizard/project-detector.test.js +51 -0
- package/tests/wizard/prompt-session.test.js +61 -0
- package/tests/wizard/prompt.test.js +16 -0
- package/tests/wizard/registry-embeddings.test.js +35 -0
- package/tests/wizard/skill-recommender.test.js +34 -0
- package/tests/wizard/slot-count.test.js +25 -0
- package/vercel.json +21 -0
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"UserPromptSubmit": [
|
|
3
|
+
{
|
|
4
|
+
"hooks": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
7
|
+
"command": "node \"$HOME/.claude/booklib-suggest.js\""
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"PostToolUse": [
|
|
13
|
+
{
|
|
14
|
+
"matcher": "WebFetch|WebSearch",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \"${BOOKLIB_ROOT}/hooks/posttooluse-capture.mjs\""
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook — suggests saving knowledge after WebFetch or WebSearch.
|
|
3
|
+
// Reads tool info from stdin (JSON), writes a hint to stdout when relevant.
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
process.exitCode = 0;
|
|
8
|
+
|
|
9
|
+
let input = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
12
|
+
process.stdin.on('end', () => {
|
|
13
|
+
let toolName = '';
|
|
14
|
+
let toolInput = {};
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(input);
|
|
18
|
+
toolName = parsed.tool_name ?? parsed.toolName ?? '';
|
|
19
|
+
toolInput = parsed.tool_input ?? parsed.toolInput ?? {};
|
|
20
|
+
} catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const captureTools = ['WebFetch', 'WebSearch', 'web_fetch', 'web_search'];
|
|
25
|
+
if (!captureTools.includes(toolName)) process.exit(0);
|
|
26
|
+
|
|
27
|
+
const isSearch = ['WebSearch', 'web_search'].includes(toolName);
|
|
28
|
+
const url = toolInput.url ?? '';
|
|
29
|
+
const query = toolInput.query ?? toolInput.input ?? '';
|
|
30
|
+
|
|
31
|
+
let suggestedTitle;
|
|
32
|
+
let sourceDesc;
|
|
33
|
+
|
|
34
|
+
if (isSearch && query) {
|
|
35
|
+
suggestedTitle = query.slice(0, 60);
|
|
36
|
+
sourceDesc = `search: "${query}"`;
|
|
37
|
+
} else if (url) {
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(url);
|
|
40
|
+
const lastSegment = u.pathname.split('/').filter(Boolean).pop() ?? '';
|
|
41
|
+
const readable = lastSegment.replace(/[-_]/g, ' ').replace(/\.\w+$/, '').trim();
|
|
42
|
+
suggestedTitle = (readable ? `${readable} (${u.hostname})` : u.hostname).slice(0, 60);
|
|
43
|
+
} catch {
|
|
44
|
+
suggestedTitle = url.slice(0, 60);
|
|
45
|
+
}
|
|
46
|
+
sourceDesc = url;
|
|
47
|
+
} else {
|
|
48
|
+
process.stdout.write('\n[booklib] To save what you found: booklib note "<title>"\n');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Strip shell metacharacters so the displayed command is always safe to copy-paste
|
|
53
|
+
const safeTitle = suggestedTitle.replace(/["$`\\]/g, '');
|
|
54
|
+
|
|
55
|
+
const hint = [
|
|
56
|
+
'',
|
|
57
|
+
`[booklib] Knowledge capture — ${sourceDesc}`,
|
|
58
|
+
` Save what you found:`,
|
|
59
|
+
` echo "paste key findings here" | booklib note "${safeTitle}"`,
|
|
60
|
+
` Or create a research template:`,
|
|
61
|
+
` booklib research "${safeTitle}"`,
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
process.stdout.write(hint);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
package/hooks/suggest.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* booklib-suggest.js
|
|
4
|
+
* Claude Code UserPromptSubmit hook — suggests a relevant booklib skill
|
|
5
|
+
* when the prompt contains a review intent AND a language/domain signal.
|
|
6
|
+
*
|
|
7
|
+
* Install: copy (or symlink) this file to ~/.claude/booklib-suggest.js
|
|
8
|
+
* Hook config (hooks.json):
|
|
9
|
+
* { "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/booklib-suggest.js\"" }] }] }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
process.exitCode = 0;
|
|
15
|
+
|
|
16
|
+
const REVIEW_KEYWORDS = [
|
|
17
|
+
"review", "check", "improve", "refactor", "fix", "audit",
|
|
18
|
+
"analyse", "analyze", "critique", "lint",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const LANGUAGE_SIGNALS = {
|
|
22
|
+
python_async: [".py", "asyncio", "async def", "await "],
|
|
23
|
+
python_scraping: [".py", "python", "beautifulsoup", "scrapy", "requests.get", "web scraping"],
|
|
24
|
+
python: [".py", "python", "def ", "async def", "import ", "asyncio", "beautifulsoup", "scrapy"],
|
|
25
|
+
typescript: [".ts", ".tsx", ".js", "typescript", "interface ", "type ", "const ", "function "],
|
|
26
|
+
java: [".java", "java", "class ", "@override", "public static"],
|
|
27
|
+
kotlin: [".kt", "kotlin", "fun ", "val ", "var ", "data class"],
|
|
28
|
+
rust: [".rs", "rust", "fn ", "impl ", "struct ", "enum ", "let mut"],
|
|
29
|
+
ui_animation: [".css", ".scss", "animation", "transition", "@keyframes", "styled", "tailwind"],
|
|
30
|
+
ui: [".css", ".scss", "animation", "transition", "@keyframes", "styled", "tailwind"],
|
|
31
|
+
data: ["pipeline", "etl", "dataframe", "schema", "migration", "replication"],
|
|
32
|
+
architecture: ["microservice", "aggregate", "bounded context", "saga", "event sourcing"],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SKILL_MAP = {
|
|
36
|
+
python_async: { skill: "/using-asyncio-python", reason: "Async patterns and asyncio best practices in Python" },
|
|
37
|
+
python_scraping: { skill: "/web-scraping-python", reason: "Web scraping techniques and patterns with Python" },
|
|
38
|
+
python: { skill: "/effective-python", reason: "Pythonic idioms and best practices" },
|
|
39
|
+
typescript: { skill: "/effective-typescript", reason: "TypeScript type safety and idiomatic patterns" },
|
|
40
|
+
java: { skill: "/effective-java", reason: "Effective Java patterns and API design" },
|
|
41
|
+
kotlin: { skill: "/effective-kotlin", reason: "Idiomatic Kotlin and best practices" },
|
|
42
|
+
rust: { skill: "/programming-with-rust", reason: "Rust ownership, safety, and idiomatic patterns" },
|
|
43
|
+
ui_animation: { skill: "/animation-at-work", reason: "Web animation principles and best practices" },
|
|
44
|
+
ui: { skill: "/refactoring-ui", reason: "UI design principles and visual hierarchy" },
|
|
45
|
+
data: { skill: "/data-pipelines", reason: "Data pipeline design and ETL best practices" },
|
|
46
|
+
architecture: { skill: "/skill-router", reason: "Routes to the right skill for architecture concerns" },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function extractPrompt(raw) {
|
|
50
|
+
if (!raw || raw.trim() === "") return "";
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
// Claude Code may send { prompt: "..." } or { message: "..." }
|
|
54
|
+
if (parsed && typeof parsed === "object") {
|
|
55
|
+
return String(parsed.prompt || parsed.message || parsed.text || "");
|
|
56
|
+
}
|
|
57
|
+
} catch (_) {
|
|
58
|
+
// Not JSON — treat as raw prompt text
|
|
59
|
+
}
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasReviewIntent(text) {
|
|
64
|
+
const lower = text.toLowerCase();
|
|
65
|
+
return REVIEW_KEYWORDS.some((kw) => lower.includes(kw));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the most specific language key that matches, or null.
|
|
70
|
+
* Order matters: more specific keys (python_async, python_scraping) are checked first.
|
|
71
|
+
*/
|
|
72
|
+
function detectLanguage(text) {
|
|
73
|
+
const lower = text.toLowerCase();
|
|
74
|
+
|
|
75
|
+
const orderedKeys = [
|
|
76
|
+
"python_async",
|
|
77
|
+
"python_scraping",
|
|
78
|
+
"python",
|
|
79
|
+
"typescript",
|
|
80
|
+
"java",
|
|
81
|
+
"kotlin",
|
|
82
|
+
"rust",
|
|
83
|
+
"ui_animation",
|
|
84
|
+
"ui",
|
|
85
|
+
"data",
|
|
86
|
+
"architecture",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const key of orderedKeys) {
|
|
90
|
+
const signals = LANGUAGE_SIGNALS[key];
|
|
91
|
+
// For compound keys, require at least 2 signals to avoid false positives
|
|
92
|
+
// (e.g. python_async needs both a python marker AND an async marker)
|
|
93
|
+
if (key === "python_async") {
|
|
94
|
+
const hasPython = [".py", "python", "def ", "import "].some((s) => lower.includes(s));
|
|
95
|
+
const hasAsync = ["asyncio", "async def", "await "].some((s) => lower.includes(s));
|
|
96
|
+
if (hasPython && hasAsync) return key;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (key === "python_scraping") {
|
|
100
|
+
const hasPython = [".py", "python", "def ", "import "].some((s) => lower.includes(s));
|
|
101
|
+
const hasScraping = ["beautifulsoup", "scrapy", "web scraping", "requests.get"].some((s) => lower.includes(s));
|
|
102
|
+
if (hasPython && hasScraping) return key;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (signals.some((s) => lower.includes(s.toLowerCase()))) {
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main() {
|
|
114
|
+
let raw = "";
|
|
115
|
+
try {
|
|
116
|
+
// Read all of stdin synchronously
|
|
117
|
+
const fd = require("fs").openSync("/dev/stdin", "r");
|
|
118
|
+
const chunks = [];
|
|
119
|
+
const buf = Buffer.alloc(4096);
|
|
120
|
+
let bytesRead;
|
|
121
|
+
while ((bytesRead = require("fs").readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
122
|
+
chunks.push(buf.slice(0, bytesRead));
|
|
123
|
+
}
|
|
124
|
+
require("fs").closeSync(fd);
|
|
125
|
+
raw = Buffer.concat(chunks).toString("utf8");
|
|
126
|
+
} catch (_) {
|
|
127
|
+
// stdin unavailable or empty — exit silently
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const prompt = extractPrompt(raw);
|
|
132
|
+
if (!prompt) process.exit(0);
|
|
133
|
+
|
|
134
|
+
if (!hasReviewIntent(prompt)) process.exit(0);
|
|
135
|
+
|
|
136
|
+
const langKey = detectLanguage(prompt);
|
|
137
|
+
|
|
138
|
+
if (!langKey) {
|
|
139
|
+
// Review intent but no specific language — suggest clean-code-reviewer
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
"💡 booklib: try /clean-code-reviewer — General clean code review principles\n"
|
|
142
|
+
);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const match = SKILL_MAP[langKey];
|
|
147
|
+
if (!match) process.exit(0);
|
|
148
|
+
|
|
149
|
+
process.stdout.write(`💡 booklib: try ${match.skill} — ${match.reason}\n`);
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the agent behaviors markdown block for a given tool target.
|
|
3
|
+
* @param {string} target - Tool target (claude, cursor, copilot, etc.)
|
|
4
|
+
* @returns {string} Markdown block
|
|
5
|
+
*/
|
|
6
|
+
export function renderBehaviors(target) {
|
|
7
|
+
const canRunCLI = !['cursor', 'copilot'].includes(target);
|
|
8
|
+
|
|
9
|
+
if (canRunCLI) {
|
|
10
|
+
return `### Code Review
|
|
11
|
+
When reviewing code or suggesting changes, query BookLib for relevant principles:
|
|
12
|
+
\`booklib search "topic relevant to the code"\`
|
|
13
|
+
Cite the skill and principle in your review.
|
|
14
|
+
|
|
15
|
+
### Knowledge Capture
|
|
16
|
+
When the user discovers a useful pattern or asks to remember something:
|
|
17
|
+
\`booklib capture --title "insight title" --type insight --tags relevant,tags\`
|
|
18
|
+
|
|
19
|
+
### Search Before Answering
|
|
20
|
+
When asked about best practices, patterns, or "how should I do X":
|
|
21
|
+
First run: \`booklib search "the question"\`
|
|
22
|
+
Use results to ground your answer in the project's active skills.
|
|
23
|
+
|
|
24
|
+
### Deep Audit
|
|
25
|
+
When asked to thoroughly review a file against a specific skill:
|
|
26
|
+
\`booklib audit <skill-name> <file-path>\`
|
|
27
|
+
|
|
28
|
+
### Health Check
|
|
29
|
+
When code quality issues accumulate or skills seem irrelevant:
|
|
30
|
+
\`booklib doctor\``;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `### Code Quality Guidance
|
|
34
|
+
When reviewing or generating code, apply the principles from the active skills listed below.
|
|
35
|
+
Focus on the patterns and anti-patterns described in each skill's documentation.
|
|
36
|
+
|
|
37
|
+
### What These Skills Cover
|
|
38
|
+
Each skill represents distilled knowledge from a specific book or guide.
|
|
39
|
+
When making design decisions, consider which skill's principles are most relevant.`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects which AI coding agents are present in the project or on $PATH.
|
|
8
|
+
* Always returns at least ['claude'].
|
|
9
|
+
*/
|
|
10
|
+
export class AgentDetector {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} [opts.cwd] - project root to check for config dirs/files
|
|
14
|
+
* @param {boolean} [opts.checkPath] - whether to check $PATH (default true)
|
|
15
|
+
*/
|
|
16
|
+
constructor({ cwd = process.cwd(), checkPath = true, home } = {}) {
|
|
17
|
+
this.cwd = cwd;
|
|
18
|
+
this.checkPath = checkPath;
|
|
19
|
+
this.home = home ?? os.homedir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
detect() {
|
|
23
|
+
const found = new Set(['claude']); // always present
|
|
24
|
+
|
|
25
|
+
const DIR_SIGNALS = {
|
|
26
|
+
cursor: ['.cursor'],
|
|
27
|
+
'roo-code': ['.roo'],
|
|
28
|
+
openhands: ['.openhands'],
|
|
29
|
+
junie: ['.junie'],
|
|
30
|
+
goose: ['.goose'],
|
|
31
|
+
letta: ['.letta'],
|
|
32
|
+
windsurf: ['.windsurf'],
|
|
33
|
+
gemini: ['.gemini'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FILE_SIGNALS = {
|
|
37
|
+
opencode: ['opencode.toml'],
|
|
38
|
+
copilot: ['.github/copilot-instructions.md'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const EXTENSION_SIGNALS = {
|
|
42
|
+
copilot: ['github.copilot', 'github.copilot-chat'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PATH_SIGNALS = {
|
|
46
|
+
cursor: 'cursor',
|
|
47
|
+
codex: 'codex',
|
|
48
|
+
windsurf: 'windsurf',
|
|
49
|
+
gemini: 'gemini',
|
|
50
|
+
goose: 'goose',
|
|
51
|
+
opencode: 'opencode',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const [agent, dirs] of Object.entries(DIR_SIGNALS)) {
|
|
55
|
+
if (dirs.some(dir => fs.existsSync(path.join(this.cwd, dir)))) {
|
|
56
|
+
found.add(agent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [agent, files] of Object.entries(FILE_SIGNALS)) {
|
|
61
|
+
if (files.some(file => fs.existsSync(path.join(this.cwd, file)))) {
|
|
62
|
+
found.add(agent);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// VS Code extension detection
|
|
67
|
+
const extDir = path.join(this.home, '.vscode', 'extensions');
|
|
68
|
+
if (fs.existsSync(extDir)) {
|
|
69
|
+
let entries;
|
|
70
|
+
try { entries = fs.readdirSync(extDir); } catch { entries = []; }
|
|
71
|
+
for (const [agent, prefixes] of Object.entries(EXTENSION_SIGNALS)) {
|
|
72
|
+
if (prefixes.some(p => entries.some(e => e.startsWith(p)))) {
|
|
73
|
+
found.add(agent);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.checkPath) {
|
|
79
|
+
for (const [agent, bin] of Object.entries(PATH_SIGNALS)) {
|
|
80
|
+
if (this._inPath(bin)) found.add(agent);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...found];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_inPath(bin) {
|
|
88
|
+
try {
|
|
89
|
+
const cmd = process.platform === 'win32' ? `where ${bin}` : `which ${bin}`;
|
|
90
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { resolveBookLibPaths } from './paths.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
sources: [
|
|
6
|
+
{ type: 'registry', trusted: true }
|
|
7
|
+
],
|
|
8
|
+
discovery: {
|
|
9
|
+
ttlHours: 24
|
|
10
|
+
},
|
|
11
|
+
search: {
|
|
12
|
+
minScore: 0.3,
|
|
13
|
+
registryFallbackThreshold: 0.4
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Loads booklib.config.json from project-local or global user dir.
|
|
19
|
+
* Returns DEFAULT_CONFIG if no config file is found.
|
|
20
|
+
* User config is shallow-merged with defaults so missing keys always have values.
|
|
21
|
+
*/
|
|
22
|
+
export function loadConfig(projectCwd) {
|
|
23
|
+
const { configPath } = resolveBookLibPaths(projectCwd);
|
|
24
|
+
|
|
25
|
+
let userConfig = {};
|
|
26
|
+
if (fs.existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
// malformed config — fall back to defaults silently
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
sources: userConfig.sources ?? DEFAULT_CONFIG.sources,
|
|
36
|
+
discovery: { ...DEFAULT_CONFIG.discovery, ...(userConfig.discovery ?? {}) },
|
|
37
|
+
search: { ...DEFAULT_CONFIG.search, ...(userConfig.search ?? {}) },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves conflicts between skill suggestions or indexed chunks.
|
|
3
|
+
*
|
|
4
|
+
* Resolution rules (applied per topic group):
|
|
5
|
+
* specificity delta >= 2 OR score delta >= 0.2 → auto-resolve (higher wins, rationale attached)
|
|
6
|
+
* otherwise → escalate (add to conflicts list)
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const resolver = new ConflictResolver(registryArray);
|
|
10
|
+
* const { winners, suppressed, conflicts } = resolver.resolveSkills(suggestions);
|
|
11
|
+
* const { winners, suppressed, conflicts } = resolver.resolveChunks(searchResults);
|
|
12
|
+
*/
|
|
13
|
+
export class ConflictResolver {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Array<{name: string, specificity?: number, topic?: string, stars?: number}>} registry
|
|
16
|
+
*/
|
|
17
|
+
constructor(registry = []) {
|
|
18
|
+
this._byName = new Map(registry.map(s => [s.name, s]));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves conflicts between registry-level skill suggestions.
|
|
23
|
+
* @param {object[]} skills — each must have .name
|
|
24
|
+
*/
|
|
25
|
+
resolveSkills(skills) {
|
|
26
|
+
return this._resolve(
|
|
27
|
+
skills,
|
|
28
|
+
s => s.name,
|
|
29
|
+
s => this._meta(s.name, s)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves conflicts between semantic-search chunks.
|
|
35
|
+
* @param {Array<{score: number, text: string, metadata: object}>} chunks
|
|
36
|
+
*/
|
|
37
|
+
resolveChunks(chunks) {
|
|
38
|
+
// Deduplicate by skill name — keep highest-scored chunk per skill
|
|
39
|
+
const bySkill = new Map();
|
|
40
|
+
for (const c of chunks) {
|
|
41
|
+
const raw = c.metadata?.name ?? c.metadata?.filePath ?? 'unknown';
|
|
42
|
+
// Normalise to skill name: take first path segment (handles reference file paths like
|
|
43
|
+
// "clean-code-reviewer/references/review-checklist.md" → "clean-code-reviewer")
|
|
44
|
+
const skillName = raw.includes('/') ? raw.split('/')[0] : raw;
|
|
45
|
+
const existing = bySkill.get(skillName);
|
|
46
|
+
if (!existing || (c.score ?? 0) > (existing.score ?? 0)) {
|
|
47
|
+
bySkill.set(skillName, c);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const deduped = [...bySkill.values()];
|
|
51
|
+
return this._resolve(
|
|
52
|
+
deduped,
|
|
53
|
+
c => c.metadata?.name ?? c.metadata?.filePath ?? 'unknown',
|
|
54
|
+
c => this._meta(c.metadata?.name, c)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Look up registry metadata, falling back to inline values or defaults. */
|
|
61
|
+
_meta(name, item = {}) {
|
|
62
|
+
const reg = this._byName.get(name);
|
|
63
|
+
return {
|
|
64
|
+
specificity: reg?.specificity ?? item?.specificity ?? 5,
|
|
65
|
+
topic: reg?.topic ?? item?.topic ?? name ?? 'unknown',
|
|
66
|
+
stars: reg?.stars ?? item?.stars ?? 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_resolve(items, getName, getMeta) {
|
|
71
|
+
// Group by topic
|
|
72
|
+
const byTopic = new Map();
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
const { topic } = getMeta(item);
|
|
75
|
+
if (!byTopic.has(topic)) byTopic.set(topic, []);
|
|
76
|
+
byTopic.get(topic).push(item);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const winners = [];
|
|
80
|
+
const suppressed = [];
|
|
81
|
+
const conflicts = [];
|
|
82
|
+
|
|
83
|
+
for (const [, candidates] of byTopic) {
|
|
84
|
+
if (candidates.length === 1) {
|
|
85
|
+
winners.push({ ...candidates[0], _decision: 'auto', _rationale: null });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sort: specificity desc → score desc → stars desc
|
|
90
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
91
|
+
const ma = getMeta(a), mb = getMeta(b);
|
|
92
|
+
if (mb.specificity !== ma.specificity) return mb.specificity - ma.specificity;
|
|
93
|
+
if ((b.score ?? 0) !== (a.score ?? 0)) return (b.score ?? 0) - (a.score ?? 0);
|
|
94
|
+
return (mb.stars ?? 0) - (ma.stars ?? 0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const best = sorted[0];
|
|
98
|
+
const bestMeta = getMeta(best);
|
|
99
|
+
let bestAdded = false;
|
|
100
|
+
|
|
101
|
+
for (const runner of sorted.slice(1)) {
|
|
102
|
+
const runnerMeta = getMeta(runner);
|
|
103
|
+
const dSpec = bestMeta.specificity - runnerMeta.specificity;
|
|
104
|
+
const dScore = (best.score ?? 0) - (runner.score ?? 0);
|
|
105
|
+
|
|
106
|
+
if (dSpec >= 2 || dScore >= 0.2) {
|
|
107
|
+
// Clear winner — auto-resolve silently
|
|
108
|
+
const reason = dSpec >= 2
|
|
109
|
+
? `more specific (${bestMeta.specificity} vs ${runnerMeta.specificity})`
|
|
110
|
+
: `higher relevance (${(best.score ?? 0).toFixed(2)} vs ${(runner.score ?? 0).toFixed(2)})`;
|
|
111
|
+
|
|
112
|
+
if (!bestAdded) {
|
|
113
|
+
winners.push({
|
|
114
|
+
...best,
|
|
115
|
+
_decision: 'auto',
|
|
116
|
+
_rationale: `chosen over \`${getName(runner)}\` — ${reason}`,
|
|
117
|
+
});
|
|
118
|
+
bestAdded = true;
|
|
119
|
+
}
|
|
120
|
+
suppressed.push({ ...runner, _decision: 'suppressed', _rationale: `\`${getName(best)}\` preferred — ${reason}` });
|
|
121
|
+
} else {
|
|
122
|
+
// Genuine conflict — escalate to human
|
|
123
|
+
if (!conflicts.find(c => c.options.some(o => o.name === getName(best)))) {
|
|
124
|
+
conflicts.push({
|
|
125
|
+
topic: bestMeta.topic,
|
|
126
|
+
options: sorted.map(s => ({
|
|
127
|
+
name: getName(s),
|
|
128
|
+
specificity: getMeta(s).specificity,
|
|
129
|
+
score: s.score ?? null,
|
|
130
|
+
})),
|
|
131
|
+
message:
|
|
132
|
+
`\`${getName(best)}\` vs \`${getName(runner)}\` — both equally applicable` +
|
|
133
|
+
` (specificity ${bestMeta.specificity} vs ${runnerMeta.specificity}).` +
|
|
134
|
+
` Which should guide this decision?`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Add best as winner if no conflict was raised for it
|
|
141
|
+
if (!bestAdded && !conflicts.some(c => c.options.some(o => o.name === getName(best)))) {
|
|
142
|
+
winners.push({ ...best, _decision: 'auto', _rationale: null });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { winners, suppressed, conflicts };
|
|
147
|
+
}
|
|
148
|
+
}
|