@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.
- package/CHANGELOG.md +285 -0
- package/LICENSE +21 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +227 -0
- package/README.pt-BR.md +14 -0
- package/README.skills.md +50 -0
- package/README.uk.md +14 -0
- package/README.zh-CN.md +14 -0
- package/bin/booklib-mcp.js +458 -0
- package/bin/booklib.js +2394 -0
- package/bin/skills.cjs +1292 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +52 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/posttooluse-contradict.mjs +76 -0
- package/hooks/posttooluse-imports.mjs +67 -0
- package/hooks/pretooluse-inject.mjs +82 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/connectors/context7.js +167 -0
- package/lib/connectors/github.js +223 -0
- package/lib/connectors/local.js +120 -0
- package/lib/connectors/notion.js +436 -0
- package/lib/connectors/web.js +134 -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/auditor.js +103 -0
- package/lib/engine/auto-linker.js +177 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/context-map.js +641 -0
- package/lib/engine/corrections.js +194 -0
- package/lib/engine/decision-checker.js +203 -0
- package/lib/engine/doctor.js +207 -0
- package/lib/engine/embedding-provider.js +72 -0
- package/lib/engine/gap-detector.js +138 -0
- package/lib/engine/gap-resolver.js +135 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph-search.js +183 -0
- package/lib/engine/graph.js +170 -0
- package/lib/engine/handoff.js +411 -0
- package/lib/engine/import-checker.js +249 -0
- package/lib/engine/import-parser.js +145 -0
- package/lib/engine/indexer.js +334 -0
- package/lib/engine/lookup-priority.js +15 -0
- package/lib/engine/parser.js +257 -0
- package/lib/engine/principle-extractor.js +116 -0
- package/lib/engine/project-analyzer.js +353 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reasoning-modes.js +353 -0
- package/lib/engine/registries.js +524 -0
- package/lib/engine/reranker.js +45 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +223 -0
- package/lib/engine/session-coordinator.js +291 -0
- package/lib/engine/session-manager.js +375 -0
- package/lib/engine/source-detector.js +240 -0
- package/lib/engine/source-manager.js +142 -0
- package/lib/engine/structured-response.js +47 -0
- package/lib/engine/synthesis-templates.js +364 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +21 -0
- package/lib/mcp-config-writer.js +107 -0
- package/lib/paths.js +62 -0
- package/lib/project-initializer.js +856 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +107 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +74 -0
- package/lib/wizard/index.js +1389 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +146 -0
- package/lib/wizard/prompt.js +221 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/package.json +70 -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/booklib-mcp-guide/SKILL.md +129 -0
- package/skills/booklib-mcp-guide/evals/evals.json +37 -0
- package/skills/booklib-mcp-guide/examples/after.md +34 -0
- package/skills/booklib-mcp-guide/examples/before.md +27 -0
- package/skills/booklib-mcp-guide/references/tool-catalog.md +9 -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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// lib/engine/corrections.js
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { createEmbeddingPipeline } from './embedding-provider.js';
|
|
7
|
+
|
|
8
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const LEVEL_THRESHOLDS = [
|
|
11
|
+
{ level: 4, min: 10 },
|
|
12
|
+
{ level: 3, min: 5 },
|
|
13
|
+
{ level: 2, min: 3 },
|
|
14
|
+
{ level: 1, min: 1 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const DEDUP_THRESHOLD = 0.85;
|
|
18
|
+
const MAX_INJECTED = 20;
|
|
19
|
+
|
|
20
|
+
export const MARKER_START = '<!-- booklib-learned-start -->';
|
|
21
|
+
export const MARKER_END = '<!-- booklib-learned-end -->';
|
|
22
|
+
|
|
23
|
+
// ── Embedding model (lazy-loaded, module-level singleton) ────────────────────
|
|
24
|
+
|
|
25
|
+
async function _getEmbedding(text) {
|
|
26
|
+
const { extractor } = await createEmbeddingPipeline({ quiet: true });
|
|
27
|
+
const output = await extractor(text, { pooling: 'mean', normalize: true });
|
|
28
|
+
return Array.from(output.data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Pure functions ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export function levelFromMentions(n) {
|
|
34
|
+
for (const { level, min } of LEVEL_THRESHOLDS) {
|
|
35
|
+
if (n >= min) return level;
|
|
36
|
+
}
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cosine(a, b) {
|
|
41
|
+
let dot = 0, na = 0, nb = 0;
|
|
42
|
+
for (let i = 0; i < a.length; i++) {
|
|
43
|
+
dot += a[i] * b[i];
|
|
44
|
+
na += a[i] * a[i];
|
|
45
|
+
nb += b[i] * b[i];
|
|
46
|
+
}
|
|
47
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb);
|
|
48
|
+
return denom === 0 ? 0 : dot / denom;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _generateId() {
|
|
52
|
+
return randomUUID().replace(/-/g, '').slice(0, 8);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _escapeRegex(s) {
|
|
56
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── File paths ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function _correctionsPath(home) {
|
|
62
|
+
return path.join(home, '.booklib', 'corrections.jsonl');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _claudeMdPath(home) {
|
|
66
|
+
return path.join(home, '.claude', 'CLAUDE.md');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Storage ───────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function loadCorrections(home = os.homedir()) {
|
|
72
|
+
const p = _correctionsPath(home);
|
|
73
|
+
if (!fs.existsSync(p)) return [];
|
|
74
|
+
return fs.readFileSync(p, 'utf8')
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.reduce((acc, line) => {
|
|
78
|
+
try {
|
|
79
|
+
acc.push(JSON.parse(line));
|
|
80
|
+
} catch {
|
|
81
|
+
process.stderr.write(`Warning: skipping corrupt line in corrections.jsonl: ${line.slice(0, 40)}\n`);
|
|
82
|
+
}
|
|
83
|
+
return acc;
|
|
84
|
+
}, []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _saveCorrections(corrections, home) {
|
|
88
|
+
const p = _correctionsPath(home);
|
|
89
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
90
|
+
const lines = corrections.map(c => JSON.stringify(c)).join('\n');
|
|
91
|
+
fs.writeFileSync(p, corrections.length ? lines + '\n' : '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── CLAUDE.md injection ────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export function rebuildLearnedSection(home = os.homedir()) {
|
|
97
|
+
const corrections = loadCorrections(home);
|
|
98
|
+
const active = corrections
|
|
99
|
+
.filter(c => c.level >= 3)
|
|
100
|
+
.sort((a, b) => b.mentions - a.mentions)
|
|
101
|
+
.slice(0, MAX_INJECTED);
|
|
102
|
+
|
|
103
|
+
const claudeFile = _claudeMdPath(home);
|
|
104
|
+
fs.mkdirSync(path.dirname(claudeFile), { recursive: true });
|
|
105
|
+
|
|
106
|
+
let existing = '';
|
|
107
|
+
try { existing = fs.readFileSync(claudeFile, 'utf8'); } catch (e) {
|
|
108
|
+
if (e.code !== 'ENOENT') throw e;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (active.length === 0) {
|
|
112
|
+
const re = new RegExp(
|
|
113
|
+
`\\n?${_escapeRegex(MARKER_START)}[\\s\\S]*?${_escapeRegex(MARKER_END)}\\n?`
|
|
114
|
+
);
|
|
115
|
+
const updated = existing.replace(re, '').trimEnd();
|
|
116
|
+
fs.writeFileSync(claudeFile, updated ? updated + '\n' : '');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bullets = active.map(c => `- ${c.text.slice(0, 120)}`).join('\n');
|
|
121
|
+
const section = [
|
|
122
|
+
MARKER_START,
|
|
123
|
+
'## Learned Corrections (BookLib)',
|
|
124
|
+
'',
|
|
125
|
+
'> When the user corrects your approach, run: booklib correction add "brief rule"',
|
|
126
|
+
'',
|
|
127
|
+
bullets,
|
|
128
|
+
'',
|
|
129
|
+
MARKER_END,
|
|
130
|
+
].join('\n');
|
|
131
|
+
|
|
132
|
+
const re = new RegExp(
|
|
133
|
+
`${_escapeRegex(MARKER_START)}[\\s\\S]*?${_escapeRegex(MARKER_END)}`
|
|
134
|
+
);
|
|
135
|
+
const updated = existing.includes(MARKER_START)
|
|
136
|
+
? existing.replace(re, section)
|
|
137
|
+
: (existing.trimEnd() ? `${existing.trimEnd()}\n\n${section}\n` : `${section}\n`);
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(claudeFile, updated);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export function listCorrections(home = os.homedir()) {
|
|
145
|
+
return loadCorrections(home).sort((a, b) => b.mentions - a.mentions);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function removeCorrection(id, home = os.homedir()) {
|
|
149
|
+
const corrections = loadCorrections(home);
|
|
150
|
+
const idx = corrections.findIndex(c => c.id === id);
|
|
151
|
+
if (idx === -1) return null;
|
|
152
|
+
const [removed] = corrections.splice(idx, 1);
|
|
153
|
+
_saveCorrections(corrections, home);
|
|
154
|
+
if (removed.level >= 3) rebuildLearnedSection(home);
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function addCorrection(text, home = os.homedir(), embedFn = _getEmbedding) {
|
|
159
|
+
const corrections = loadCorrections(home);
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const newVec = await embedFn(text);
|
|
162
|
+
|
|
163
|
+
for (const c of corrections) {
|
|
164
|
+
const existVec = c.embedding ?? await embedFn(c.text);
|
|
165
|
+
if (!c.embedding) c.embedding = existVec; // backfill on first encounter
|
|
166
|
+
const sim = cosine(newVec, existVec);
|
|
167
|
+
if (sim >= DEDUP_THRESHOLD) {
|
|
168
|
+
const oldLevel = c.level;
|
|
169
|
+
c.mentions += 1;
|
|
170
|
+
c.level = levelFromMentions(c.mentions);
|
|
171
|
+
c.lastSeen = now;
|
|
172
|
+
c.sessions.push(now);
|
|
173
|
+
_saveCorrections(corrections, home);
|
|
174
|
+
if (c.level >= 3 && c.level !== oldLevel) rebuildLearnedSection(home);
|
|
175
|
+
const { embedding: _emb, ...rest } = c;
|
|
176
|
+
return { ...rest, wasExisting: true };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entry = {
|
|
181
|
+
id: _generateId(),
|
|
182
|
+
text,
|
|
183
|
+
mentions: 1,
|
|
184
|
+
level: levelFromMentions(1),
|
|
185
|
+
sessions: [now],
|
|
186
|
+
firstSeen: now,
|
|
187
|
+
lastSeen: now,
|
|
188
|
+
embedding: newVec,
|
|
189
|
+
};
|
|
190
|
+
corrections.push(entry);
|
|
191
|
+
_saveCorrections(corrections, home);
|
|
192
|
+
const { embedding: _emb, ...rest } = entry;
|
|
193
|
+
return { ...rest, wasExisting: false };
|
|
194
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseImports, detectLanguage } from './import-parser.js';
|
|
4
|
+
|
|
5
|
+
const MAX_FILE_SIZE = 1_000_000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prohibition patterns -- regex that captures what's being prohibited.
|
|
9
|
+
* Group 1 should capture the prohibited thing.
|
|
10
|
+
*/
|
|
11
|
+
const PROHIBITION_PATTERNS = [
|
|
12
|
+
// "do not use X", "don't use X", "never use X", "avoid X"
|
|
13
|
+
/(?:do\s+not|don't|never|avoid)\s+(?:use|using)\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
14
|
+
// "deprecated: X", "X is deprecated"
|
|
15
|
+
/deprecated:?\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
16
|
+
/(['"`]?[A-Z][\w.]*['"`]?)\s+is\s+deprecated/gi,
|
|
17
|
+
// "prefer X over Y" -- Y is prohibited
|
|
18
|
+
/prefer\s+\S+\s+over\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
19
|
+
// "replaced X with Y" -- X is prohibited
|
|
20
|
+
/replaced?\s+['"`]?([^'"`.,;\n]+?)['"`]?\s+with\s/gi,
|
|
21
|
+
// "decided against X"
|
|
22
|
+
/decided\s+against\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
23
|
+
// "instead of X" — only after a decision verb to reduce false positives
|
|
24
|
+
/(?:decided|agreed|chose|chosen|use)\s+\S+\s+instead\s+of\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
25
|
+
// "must not X", "should not X"
|
|
26
|
+
/(?:must|should)\s+not\s+(?:use|import|call|reference)\s+['"`]?([^'"`.,;\n]+)/gi,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export class DecisionChecker {
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} [opts]
|
|
32
|
+
* @param {object} [opts.searcher] - BookLibSearcher instance
|
|
33
|
+
* @param {number} [opts.minScore] - minimum search relevance score
|
|
34
|
+
*/
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
this.searcher = opts.searcher ?? null;
|
|
37
|
+
this.minScore = opts.minScore ?? 0.4;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check a file for contradictions against indexed team decisions.
|
|
42
|
+
* @param {string} filePath
|
|
43
|
+
* @returns {Promise<{contradictions: Array<{identifier: string, decision: string, source: string, pattern: string}>, checked: number}>}
|
|
44
|
+
*/
|
|
45
|
+
async checkFile(filePath) {
|
|
46
|
+
if (!this.searcher) return { contradictions: [], checked: 0 };
|
|
47
|
+
|
|
48
|
+
const language = detectLanguage(filePath);
|
|
49
|
+
if (!language) return { contradictions: [], checked: 0 };
|
|
50
|
+
|
|
51
|
+
const resolved = path.resolve(filePath);
|
|
52
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
53
|
+
if (!stat || stat.size > MAX_FILE_SIZE) return { contradictions: [], checked: 0 };
|
|
54
|
+
|
|
55
|
+
const code = fs.readFileSync(resolved, 'utf8');
|
|
56
|
+
const identifiers = this._extractIdentifiers(code, language);
|
|
57
|
+
if (identifiers.length === 0) return { contradictions: [], checked: 0 };
|
|
58
|
+
|
|
59
|
+
return this._findContradictions(identifiers);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract meaningful identifiers from code -- import names, API calls, etc.
|
|
64
|
+
* @param {string} code
|
|
65
|
+
* @param {string} language
|
|
66
|
+
* @returns {string[]} unique identifiers
|
|
67
|
+
*/
|
|
68
|
+
_extractIdentifiers(code, language) {
|
|
69
|
+
const imports = parseImports(code, language).map(i => i.module);
|
|
70
|
+
const apiCalls = [];
|
|
71
|
+
// Match dot-notation API calls: stripe.charges.create(), db.collection.find()
|
|
72
|
+
const dotPattern = /\b([a-zA-Z_$][\w]*(?:\.[a-zA-Z_$]\w*)+)\s*\(/g;
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = dotPattern.exec(code)) !== null) {
|
|
75
|
+
apiCalls.push(match[1]);
|
|
76
|
+
}
|
|
77
|
+
return [...new Set([...imports, ...apiCalls])];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Search the index for each identifier and check for contradictions.
|
|
82
|
+
* @param {string[]} identifiers
|
|
83
|
+
* @returns {Promise<{contradictions: Array, checked: number}>}
|
|
84
|
+
*/
|
|
85
|
+
async _findContradictions(identifiers) {
|
|
86
|
+
const contradictions = [];
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
|
|
89
|
+
for (const id of identifiers) {
|
|
90
|
+
try {
|
|
91
|
+
const results = await this.searcher.search(id, 3, this.minScore);
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
this._checkResult(result, identifiers, seen, contradictions);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Search failure -- skip this identifier
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { contradictions, checked: identifiers.length };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check a single search result for prohibition matches.
|
|
105
|
+
* @param {object} result - search result with text and metadata
|
|
106
|
+
* @param {string[]} identifiers - code identifiers to match against
|
|
107
|
+
* @param {Set} seen - dedup tracker
|
|
108
|
+
* @param {Array} contradictions - output array
|
|
109
|
+
*/
|
|
110
|
+
_checkResult(result, identifiers, seen, contradictions) {
|
|
111
|
+
const text = result.item?.text ?? result.text ?? '';
|
|
112
|
+
const prohibited = this._extractProhibitions(text);
|
|
113
|
+
|
|
114
|
+
for (const { target, pattern } of prohibited) {
|
|
115
|
+
const matchedId = this._matchesIdentifier(target, identifiers);
|
|
116
|
+
const key = `${matchedId}:${text.slice(0, 50)}`;
|
|
117
|
+
if (matchedId && !seen.has(key)) {
|
|
118
|
+
seen.add(key);
|
|
119
|
+
contradictions.push({
|
|
120
|
+
identifier: matchedId,
|
|
121
|
+
decision: text.slice(0, 300),
|
|
122
|
+
source: result.metadata?.sourceName ?? result.metadata?.title ?? 'unknown',
|
|
123
|
+
pattern,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract prohibition targets from a text snippet.
|
|
131
|
+
* @param {string} text
|
|
132
|
+
* @returns {Array<{target: string, pattern: string}>}
|
|
133
|
+
*/
|
|
134
|
+
_extractProhibitions(text) {
|
|
135
|
+
const results = [];
|
|
136
|
+
for (const pattern of PROHIBITION_PATTERNS) {
|
|
137
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = regex.exec(text)) !== null) {
|
|
140
|
+
const target = match[1].trim();
|
|
141
|
+
if (target.length >= 2 && target.length <= 100) {
|
|
142
|
+
results.push({ target, pattern: pattern.source.slice(0, 40) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a prohibition target matches any code identifier.
|
|
151
|
+
* Splits both on word boundaries (dots, spaces) for segment-level matching.
|
|
152
|
+
* "Charges API" matches "stripe.charges" because "charges" overlaps.
|
|
153
|
+
* @param {string} target - the prohibited thing from decision text
|
|
154
|
+
* @param {string[]} identifiers - identifiers from the code
|
|
155
|
+
* @returns {string|null} the matched identifier, or null
|
|
156
|
+
*/
|
|
157
|
+
_matchesIdentifier(target, identifiers) {
|
|
158
|
+
const targetSegments = this._toSegments(target);
|
|
159
|
+
for (const id of identifiers) {
|
|
160
|
+
if (this._segmentsOverlap(targetSegments, this._toSegments(id))) {
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Split a string into lowercase segments by dots, spaces, and camelCase.
|
|
169
|
+
* @param {string} str
|
|
170
|
+
* @returns {string[]}
|
|
171
|
+
*/
|
|
172
|
+
_toSegments(str) {
|
|
173
|
+
return str
|
|
174
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.split(/[\s./:@-]+/)
|
|
177
|
+
.filter(s => s.length >= 2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if any significant segment from A exactly matches one from B.
|
|
182
|
+
* Exact equality only — substring matching produces too many false positives
|
|
183
|
+
* on short common segments like "api", "sql", "log".
|
|
184
|
+
* @param {string[]} segsA
|
|
185
|
+
* @param {string[]} segsB
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
_segmentsOverlap(segsA, segsB) {
|
|
189
|
+
const setB = new Set(segsB);
|
|
190
|
+
// Require exact match on segments of 4+ chars, or at least 2 short-segment matches
|
|
191
|
+
let shortMatches = 0;
|
|
192
|
+
for (const a of segsA) {
|
|
193
|
+
if (setB.has(a)) {
|
|
194
|
+
if (a.length >= 4) return true;
|
|
195
|
+
shortMatches++;
|
|
196
|
+
if (shortMatches >= 2) return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { PROHIBITION_PATTERNS };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { resolveBookLibPaths } from '../paths.js';
|
|
5
|
+
import { SKILL_LIMIT } from '../wizard/skill-recommender.js';
|
|
6
|
+
import { countAllSlots, countInstalledSlots, listInstalledSkillNames } from '../skill-fetcher.js';
|
|
7
|
+
|
|
8
|
+
const MARKER_START = '<!-- booklib-standards-start -->';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Runs all diagnostic checks and returns an array of findings.
|
|
12
|
+
* @param {string} cwd - project root
|
|
13
|
+
* @returns {Array<{ check: string, severity: 'error'|'warning'|'info', message: string, fixable: boolean, data?: object }>}
|
|
14
|
+
*/
|
|
15
|
+
export function runDiagnostics(cwd = process.cwd()) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
|
|
18
|
+
checkSlotOverload(findings);
|
|
19
|
+
checkOversizedConfigs(findings, cwd);
|
|
20
|
+
checkMissingIndex(findings, cwd);
|
|
21
|
+
checkMissingConfigFiles(findings, cwd);
|
|
22
|
+
checkStaleSkills(findings);
|
|
23
|
+
checkOrphanedSkills(findings);
|
|
24
|
+
|
|
25
|
+
return findings;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Check 1: Slot overload */
|
|
29
|
+
function checkSlotOverload(findings) {
|
|
30
|
+
const slotsUsed = countAllSlots();
|
|
31
|
+
if (slotsUsed > SKILL_LIMIT) {
|
|
32
|
+
findings.push({
|
|
33
|
+
check: 'slot-overload',
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
message: `${slotsUsed} skills installed (limit: ${SKILL_LIMIT}). Agent context is overloaded. Run: booklib doctor --usage`,
|
|
36
|
+
fixable: false,
|
|
37
|
+
data: { slotsUsed },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Check 2: Oversized config files */
|
|
43
|
+
function checkOversizedConfigs(findings, cwd) {
|
|
44
|
+
const configFiles = [
|
|
45
|
+
{ file: 'CLAUDE.md', path: path.join(cwd, 'CLAUDE.md') },
|
|
46
|
+
{ file: '.github/copilot-instructions.md', path: path.join(cwd, '.github', 'copilot-instructions.md') },
|
|
47
|
+
{ file: '.gemini/context.md', path: path.join(cwd, '.gemini', 'context.md') },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const cf of configFiles) {
|
|
51
|
+
if (fs.existsSync(cf.path)) {
|
|
52
|
+
const content = fs.readFileSync(cf.path, 'utf8');
|
|
53
|
+
const lines = content.split('\n').length;
|
|
54
|
+
if (lines > 500 && content.includes(MARKER_START)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
check: 'oversized-config',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
message: `${cf.file} is ${lines} lines. Recommended: under 200.`,
|
|
59
|
+
fixable: true,
|
|
60
|
+
data: { file: cf.file, absPath: cf.path, lines },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Check 3: Missing index */
|
|
68
|
+
function checkMissingIndex(findings, cwd) {
|
|
69
|
+
try {
|
|
70
|
+
const { indexPath } = resolveBookLibPaths(cwd);
|
|
71
|
+
if (!fs.existsSync(indexPath)) {
|
|
72
|
+
findings.push({
|
|
73
|
+
check: 'missing-index',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
message: 'No search index found. Search and recommendations won\'t work.',
|
|
76
|
+
fixable: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
findings.push({
|
|
81
|
+
check: 'missing-index',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
message: 'No search index found. Search and recommendations won\'t work.',
|
|
84
|
+
fixable: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Check 4: Missing config files for configured tools */
|
|
90
|
+
function checkMissingConfigFiles(findings, cwd) {
|
|
91
|
+
const configPath = path.join(cwd, 'booklib.config.json');
|
|
92
|
+
if (!fs.existsSync(configPath)) return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
96
|
+
const toolFileMap = {
|
|
97
|
+
claude: 'CLAUDE.md',
|
|
98
|
+
copilot: '.github/copilot-instructions.md',
|
|
99
|
+
gemini: '.gemini/context.md',
|
|
100
|
+
codex: 'AGENTS.md',
|
|
101
|
+
cursor: '.cursor/rules/booklib-standards.mdc',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const tool of (config.tools ?? [])) {
|
|
105
|
+
const expectedFile = toolFileMap[tool];
|
|
106
|
+
if (expectedFile && !fs.existsSync(path.join(cwd, expectedFile))) {
|
|
107
|
+
findings.push({
|
|
108
|
+
check: 'missing-config',
|
|
109
|
+
severity: 'warning',
|
|
110
|
+
message: `${tool} configured but ${expectedFile} not found.`,
|
|
111
|
+
fixable: true,
|
|
112
|
+
data: { tool, file: expectedFile },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* malformed config, skip */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Check 5: Stale skills (installed but never used) */
|
|
120
|
+
function checkStaleSkills(findings) {
|
|
121
|
+
const usagePath = path.join(os.homedir(), '.booklib', 'usage.json');
|
|
122
|
+
if (!fs.existsSync(usagePath)) return;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const usage = JSON.parse(fs.readFileSync(usagePath, 'utf8'));
|
|
126
|
+
const usedNames = new Set(
|
|
127
|
+
usage.map(u => u.skill?.toLowerCase()).filter(Boolean),
|
|
128
|
+
);
|
|
129
|
+
const installed = listInstalledSkillNames();
|
|
130
|
+
const stale = installed.filter(n => !usedNames.has(n.toLowerCase()));
|
|
131
|
+
|
|
132
|
+
if (stale.length > 5) {
|
|
133
|
+
findings.push({
|
|
134
|
+
check: 'stale-skills',
|
|
135
|
+
severity: 'info',
|
|
136
|
+
message: `${stale.length} skills have no recorded usage.`,
|
|
137
|
+
fixable: true,
|
|
138
|
+
data: { staleNames: stale },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch { /* no usage data, skip */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Check 6: Orphaned skills (have .booklib marker but not in any catalog/index) */
|
|
145
|
+
function checkOrphanedSkills(findings) {
|
|
146
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
147
|
+
if (!fs.existsSync(claudeSkillsDir)) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const installed = listInstalledSkillNames();
|
|
151
|
+
// Read the bundled skills directory to find known skill names
|
|
152
|
+
const packageRoot = path.resolve(
|
|
153
|
+
new URL('.', import.meta.url).pathname, '..', '..',
|
|
154
|
+
);
|
|
155
|
+
const bundledSkillsDir = path.join(packageRoot, 'skills');
|
|
156
|
+
const knownNames = new Set();
|
|
157
|
+
if (fs.existsSync(bundledSkillsDir)) {
|
|
158
|
+
for (const entry of fs.readdirSync(bundledSkillsDir, { withFileTypes: true })) {
|
|
159
|
+
if (entry.isDirectory()) knownNames.add(entry.name);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Also load community registry skill names
|
|
164
|
+
const registryPath = path.join(packageRoot, 'community', 'registry.json');
|
|
165
|
+
if (fs.existsSync(registryPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
168
|
+
for (const skill of (registry.skills ?? [])) {
|
|
169
|
+
knownNames.add(skill.name);
|
|
170
|
+
}
|
|
171
|
+
} catch { /* malformed registry, skip */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const orphaned = installed.filter(n => !knownNames.has(n));
|
|
175
|
+
for (const name of orphaned) {
|
|
176
|
+
findings.push({
|
|
177
|
+
check: 'orphaned-skill',
|
|
178
|
+
severity: 'info',
|
|
179
|
+
message: `${name} -- not found in any catalog. May be outdated.`,
|
|
180
|
+
fixable: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch { /* skip orphan check on error */ }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Prints diagnostic results to stdout.
|
|
188
|
+
* @param {Array<{ check: string, severity: string, message: string, fixable: boolean }>} findings
|
|
189
|
+
*/
|
|
190
|
+
export function printDiagnostics(findings) {
|
|
191
|
+
if (findings.length === 0) {
|
|
192
|
+
console.log(' No issues found. BookLib is healthy.\n');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const f of findings) {
|
|
197
|
+
const icon = f.severity === 'error' ? '\u2717' : f.severity === 'warning' ? '\u26A0' : '\u2139';
|
|
198
|
+
console.log(` ${icon} ${f.message}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fixable = findings.filter(f => f.fixable).length;
|
|
202
|
+
if (fixable > 0) {
|
|
203
|
+
console.log(`\n ${fixable} issue(s) fixable. Run: booklib doctor --cure\n`);
|
|
204
|
+
} else {
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// lib/engine/embedding-provider.js
|
|
2
|
+
// Suppress ONNX runtime JS-level warnings
|
|
3
|
+
process.env.ORT_LOG_LEVEL = 'ERROR';
|
|
4
|
+
|
|
5
|
+
import { pipeline } from '@huggingface/transformers';
|
|
6
|
+
|
|
7
|
+
const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
|
|
8
|
+
const BATCH_SIZE = 32;
|
|
9
|
+
|
|
10
|
+
let _cachedPipeline = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns a shared embedding pipeline (singleton per process).
|
|
14
|
+
* First call downloads/loads the model; subsequent calls return the cached instance.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} [opts]
|
|
17
|
+
* @param {boolean} [opts.quiet=false] - Suppress loading message.
|
|
18
|
+
* @returns {Promise<{ extractor: Function }>}
|
|
19
|
+
*/
|
|
20
|
+
export async function createEmbeddingPipeline(opts = {}) {
|
|
21
|
+
if (_cachedPipeline) return _cachedPipeline;
|
|
22
|
+
|
|
23
|
+
const { quiet = false } = opts;
|
|
24
|
+
if (!quiet) {
|
|
25
|
+
console.log('Loading local embedding model...');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Try CoreML (Apple Silicon) with stderr suppressed during load
|
|
29
|
+
// CoreML spams native warnings ("Context leak", "IsInputSupported") — harmless but noisy
|
|
30
|
+
let extractor;
|
|
31
|
+
const isMac = process.platform === 'darwin';
|
|
32
|
+
|
|
33
|
+
// CPU int8 quantization is faster than CoreML for this model (73ms vs 1550ms).
|
|
34
|
+
// CoreML partitions 231/323 nodes to GPU, but the CPU↔GPU transfer overhead
|
|
35
|
+
// makes it slower than pure CPU for MiniLM-L6 (384-dim, small model).
|
|
36
|
+
// CPU q8 also produces zero native warnings.
|
|
37
|
+
extractor = await pipeline('feature-extraction', MODEL_NAME, { dtype: 'q8' });
|
|
38
|
+
|
|
39
|
+
// Warm up the model with a dummy embedding — triggers native CoreML warnings NOW
|
|
40
|
+
// so they don't appear during user-facing operations (search, lookup, index).
|
|
41
|
+
try {
|
|
42
|
+
await extractor('warmup', { pooling: 'mean', normalize: true });
|
|
43
|
+
} catch { /* warmup is best-effort */ }
|
|
44
|
+
|
|
45
|
+
_cachedPipeline = { extractor };
|
|
46
|
+
return _cachedPipeline;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Embed multiple texts in batches.
|
|
51
|
+
* @param {Function} extractor - the pipeline function
|
|
52
|
+
* @param {string[]} texts - array of texts to embed
|
|
53
|
+
* @param {number} [batchSize=32]
|
|
54
|
+
* @param {Function} [onBatch] - called after each batch with { done, total }
|
|
55
|
+
* @returns {Promise<number[][]>} array of embedding vectors
|
|
56
|
+
*/
|
|
57
|
+
export async function batchEmbed(extractor, texts, batchSize = BATCH_SIZE, onBatch) {
|
|
58
|
+
const allVectors = [];
|
|
59
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
60
|
+
const batch = texts.slice(i, i + batchSize);
|
|
61
|
+
const outputs = await extractor(batch, { pooling: 'mean', normalize: true });
|
|
62
|
+
const dims = outputs.dims[outputs.dims.length - 1];
|
|
63
|
+
for (let j = 0; j < batch.length; j++) {
|
|
64
|
+
const start = j * dims;
|
|
65
|
+
allVectors.push(Array.from(outputs.data.slice(start, start + dims)));
|
|
66
|
+
}
|
|
67
|
+
onBatch?.({ done: Math.min(i + batchSize, texts.length), total: texts.length });
|
|
68
|
+
}
|
|
69
|
+
return allVectors;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { BATCH_SIZE, MODEL_NAME };
|