@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,138 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { scanDependencies, checkPublishDate, CUTOFF_DATE, CONCURRENCY } from './registries.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect knowledge gaps in the current project by checking dependency
|
|
7
|
+
* publish dates against the model training cutoff.
|
|
8
|
+
*/
|
|
9
|
+
export class GapDetector {
|
|
10
|
+
constructor(opts = {}) {
|
|
11
|
+
this.cutoffDate = opts.cutoffDate ?? CUTOFF_DATE;
|
|
12
|
+
this.cachePath = opts.cachePath ?? path.join(process.cwd(), '.booklib', 'version-cache.json');
|
|
13
|
+
this.cacheTtlMs = opts.cacheTtlMs ?? 24 * 60 * 60 * 1000; // 24h
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scan project and detect all gaps.
|
|
18
|
+
* @param {string} projectDir
|
|
19
|
+
* @returns {Promise<{postTraining: Array, uncapturedDocs: Array, ecosystems: string[], totalDeps: number, checkedDeps: number}>}
|
|
20
|
+
*/
|
|
21
|
+
async detect(projectDir) {
|
|
22
|
+
const deps = scanDependencies(projectDir);
|
|
23
|
+
const ecosystems = [...new Set(deps.map(d => d.ecosystem))];
|
|
24
|
+
|
|
25
|
+
const cache = this._loadCache();
|
|
26
|
+
|
|
27
|
+
// Process deps in batches to avoid blasting registries with hundreds of concurrent requests
|
|
28
|
+
const results = [];
|
|
29
|
+
for (let i = 0; i < deps.length; i += CONCURRENCY) {
|
|
30
|
+
const batch = deps.slice(i, i + CONCURRENCY);
|
|
31
|
+
const batchResults = await Promise.all(batch.map(dep => this._checkDep(dep, cache)));
|
|
32
|
+
results.push(...batchResults);
|
|
33
|
+
// Save cache after each batch so progress survives crashes
|
|
34
|
+
this._saveCache(cache);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Aggregate — no shared mutable state during async work
|
|
38
|
+
const postTraining = results.filter(r => r.postTraining).map(r => r.postTraining);
|
|
39
|
+
const checked = results.map(r => r.dep);
|
|
40
|
+
|
|
41
|
+
const uncapturedDocs = this._scanProjectDocs(projectDir);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
postTraining,
|
|
45
|
+
uncapturedDocs,
|
|
46
|
+
ecosystems,
|
|
47
|
+
totalDeps: deps.length,
|
|
48
|
+
checkedDeps: checked.length,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Check a single dep against cache or registry, returning result without side effects. */
|
|
53
|
+
async _checkDep(dep, cache) {
|
|
54
|
+
const cacheKey = `${dep.ecosystem}:${dep.name}@${dep.version}`;
|
|
55
|
+
|
|
56
|
+
if (cache[cacheKey] && Date.now() - cache[cacheKey].checkedAt < this.cacheTtlMs) {
|
|
57
|
+
if (cache[cacheKey].publishDate) {
|
|
58
|
+
const pubDate = new Date(cache[cacheKey].publishDate);
|
|
59
|
+
if (pubDate > this.cutoffDate) {
|
|
60
|
+
return { dep, postTraining: { ...dep, publishDate: pubDate } };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { dep, postTraining: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const publishDate = await checkPublishDate(dep);
|
|
67
|
+
cache[cacheKey] = {
|
|
68
|
+
publishDate: publishDate?.toISOString() ?? null,
|
|
69
|
+
checkedAt: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (publishDate && publishDate > this.cutoffDate) {
|
|
73
|
+
return { dep, postTraining: { ...dep, publishDate } };
|
|
74
|
+
}
|
|
75
|
+
return { dep, postTraining: null };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find project documentation directories and files that are not yet
|
|
80
|
+
* connected to BookLib as knowledge sources.
|
|
81
|
+
* @param {string} projectDir
|
|
82
|
+
* @returns {Array<{path: string, type: string, fileCount: number}>}
|
|
83
|
+
*/
|
|
84
|
+
_scanProjectDocs(projectDir) {
|
|
85
|
+
const docPaths = ['docs', 'decisions', 'specs', 'adrs', 'architecture'];
|
|
86
|
+
const docFiles = ['ARCHITECTURE.md', 'CONVENTIONS.md', 'DECISIONS.md', 'ADR.md'];
|
|
87
|
+
|
|
88
|
+
// Check which docs are already connected as BookLib sources
|
|
89
|
+
const connectedNames = new Set();
|
|
90
|
+
try {
|
|
91
|
+
const sourcesPath = path.join(projectDir, '.booklib', 'sources.json');
|
|
92
|
+
if (fs.existsSync(sourcesPath)) {
|
|
93
|
+
const registry = JSON.parse(fs.readFileSync(sourcesPath, 'utf8'));
|
|
94
|
+
for (const s of registry.sources ?? []) {
|
|
95
|
+
connectedNames.add(s.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch { /* best effort */ }
|
|
99
|
+
|
|
100
|
+
const found = [];
|
|
101
|
+
|
|
102
|
+
for (const dir of docPaths) {
|
|
103
|
+
if (connectedNames.has(dir)) continue; // already connected
|
|
104
|
+
const full = path.join(projectDir, dir);
|
|
105
|
+
if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
|
|
106
|
+
const fileCount = fs.readdirSync(full).filter(f => /\.(md|mdx|txt)$/i.test(f)).length;
|
|
107
|
+
if (fileCount > 0) {
|
|
108
|
+
found.push({ path: dir, type: 'directory', fileCount });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const file of docFiles) {
|
|
114
|
+
if (connectedNames.has(file)) continue;
|
|
115
|
+
if (fs.existsSync(path.join(projectDir, file))) {
|
|
116
|
+
found.push({ path: file, type: 'file', fileCount: 1 });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_loadCache() {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(fs.readFileSync(this.cachePath, 'utf8'));
|
|
126
|
+
} catch {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_saveCache(cache) {
|
|
132
|
+
try {
|
|
133
|
+
const dir = path.dirname(this.cachePath);
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
fs.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2));
|
|
136
|
+
} catch { /* best effort */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve knowledge gaps by trying multiple sources in priority order.
|
|
6
|
+
* Context7 (instant) -> GitHub releases -> manual suggestion.
|
|
7
|
+
*/
|
|
8
|
+
export class GapResolver {
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.outputBase = opts.outputBase ?? path.join(process.cwd(), '.booklib', 'sources');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a single dependency gap.
|
|
15
|
+
* @param {{ name: string, version: string, ecosystem: string }} dep
|
|
16
|
+
* @returns {Promise<{ resolved: boolean, source: string, pageCount: number, suggestion?: string }>}
|
|
17
|
+
*/
|
|
18
|
+
async resolve(dep) {
|
|
19
|
+
// Try Context7 first — fastest, broadest coverage
|
|
20
|
+
const ctx7Result = await this._tryContext7(dep);
|
|
21
|
+
if (ctx7Result.resolved) return ctx7Result;
|
|
22
|
+
|
|
23
|
+
// Fall back to GitHub releases
|
|
24
|
+
const ghResult = await this._tryGitHub(dep);
|
|
25
|
+
if (ghResult.resolved) return ghResult;
|
|
26
|
+
|
|
27
|
+
// Last resort: manual suggestion with ecosystem-specific URL
|
|
28
|
+
return this._suggestManual(dep);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve multiple gaps, returning results for each.
|
|
33
|
+
* @param {Array<{ name: string, version: string, ecosystem: string }>} deps
|
|
34
|
+
* @param {Function} [onProgress] - called with { dep, result, index, total }
|
|
35
|
+
* @returns {Promise<Array<{ dep, result }>>}
|
|
36
|
+
*/
|
|
37
|
+
async resolveAll(deps, onProgress) {
|
|
38
|
+
const results = [];
|
|
39
|
+
for (let i = 0; i < deps.length; i++) {
|
|
40
|
+
const dep = deps[i];
|
|
41
|
+
const result = await this.resolve(dep);
|
|
42
|
+
results.push({ dep, result });
|
|
43
|
+
onProgress?.({ dep, result, index: i, total: deps.length });
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Attempt resolution via Context7 API. */
|
|
49
|
+
async _tryContext7(dep) {
|
|
50
|
+
try {
|
|
51
|
+
const { Context7Connector } = await import('../connectors/context7.js');
|
|
52
|
+
const ctx7 = new Context7Connector();
|
|
53
|
+
if (!ctx7.checkAuth().ok) return { resolved: false, source: 'context7' };
|
|
54
|
+
|
|
55
|
+
const sourceName = `ctx7-${dep.name.replace(/[@/]/g, '_').replace(/^_+/, '')}`;
|
|
56
|
+
const outputDir = path.join(this.outputBase, sourceName);
|
|
57
|
+
const result = await ctx7.resolveAndFetch(
|
|
58
|
+
dep.name,
|
|
59
|
+
outputDir,
|
|
60
|
+
`${dep.name} v${dep.version} API`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (result.resolved && result.pageCount > 0) {
|
|
64
|
+
return {
|
|
65
|
+
resolved: true,
|
|
66
|
+
source: 'context7',
|
|
67
|
+
pageCount: result.pageCount,
|
|
68
|
+
sourceName,
|
|
69
|
+
outputDir,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
} catch { /* Context7 unavailable */ }
|
|
73
|
+
return { resolved: false, source: 'context7' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Attempt resolution via GitHub release notes. npm-only. */
|
|
77
|
+
async _tryGitHub(dep) {
|
|
78
|
+
if (dep.ecosystem !== 'npm') return { resolved: false, source: 'github' };
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { GitHubConnector } = await import('../connectors/github.js');
|
|
82
|
+
const gh = new GitHubConnector();
|
|
83
|
+
if (!gh.checkAuth().ok) return { resolved: false, source: 'github' };
|
|
84
|
+
|
|
85
|
+
// Resolve npm package -> GitHub repo via npm registry
|
|
86
|
+
const res = await fetch(
|
|
87
|
+
`https://registry.npmjs.org/${encodeURIComponent(dep.name)}`,
|
|
88
|
+
{
|
|
89
|
+
headers: { 'Accept': 'application/vnd.npm.install-v1+json' },
|
|
90
|
+
signal: AbortSignal.timeout(5000),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
if (!res.ok) return { resolved: false, source: 'github' };
|
|
94
|
+
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
const repoUrl = data.repository?.url ?? '';
|
|
97
|
+
const match = repoUrl.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
98
|
+
if (!match) return { resolved: false, source: 'github' };
|
|
99
|
+
|
|
100
|
+
const repo = match[1];
|
|
101
|
+
const sourceName = `gh-${dep.name}-releases`;
|
|
102
|
+
const outputDir = path.join(this.outputBase, sourceName);
|
|
103
|
+
const result = await gh.fetchReleases(repo, outputDir, { limit: 5 });
|
|
104
|
+
|
|
105
|
+
if (result.pageCount > 0) {
|
|
106
|
+
return {
|
|
107
|
+
resolved: true,
|
|
108
|
+
source: 'github',
|
|
109
|
+
pageCount: result.pageCount,
|
|
110
|
+
sourceName,
|
|
111
|
+
outputDir,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
} catch { /* GitHub unavailable */ }
|
|
115
|
+
return { resolved: false, source: 'github' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Build a manual suggestion with ecosystem-specific URL. */
|
|
119
|
+
_suggestManual(dep) {
|
|
120
|
+
const suggestions = {
|
|
121
|
+
npm: `booklib connect https://www.npmjs.com/package/${dep.name} --type=framework-docs`,
|
|
122
|
+
pypi: `booklib connect https://pypi.org/project/${dep.name}/ --type=framework-docs`,
|
|
123
|
+
crates: `booklib connect https://docs.rs/${dep.name} --type=framework-docs`,
|
|
124
|
+
go: `booklib connect https://pkg.go.dev/${dep.name} --type=framework-docs`,
|
|
125
|
+
rubygems: `booklib connect https://rubygems.org/gems/${dep.name} --type=framework-docs`,
|
|
126
|
+
maven: `booklib connect https://search.maven.org/artifact/${dep.name.replace(':', '/')} --type=framework-docs`,
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
resolved: false,
|
|
130
|
+
source: 'manual',
|
|
131
|
+
pageCount: 0,
|
|
132
|
+
suggestion: suggestions[dep.ecosystem] ?? 'booklib connect <docs-url> --type=framework-docs',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// lib/engine/graph-injector.js
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import {
|
|
4
|
+
listNodes, loadNode, loadEdges, traverseEdges,
|
|
5
|
+
parseNodeFrontmatter, resolveKnowledgePaths,
|
|
6
|
+
} from './graph.js';
|
|
7
|
+
|
|
8
|
+
// ── Component matching ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns component nodes whose path globs match filePath.
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
* @param {Array<{id: string, paths: string[], title: string}>} components
|
|
14
|
+
*/
|
|
15
|
+
export function findOwningComponents(filePath, components) {
|
|
16
|
+
return components.filter(comp =>
|
|
17
|
+
(comp.paths ?? []).some(glob => minimatch(filePath, glob, { matchBase: true }))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Ranking ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Deduplicates nodes by id (keeping highest score) and sorts descending by score.
|
|
25
|
+
* @param {Array<{id: string, score: number, text: string, hop: number}>} nodes
|
|
26
|
+
*/
|
|
27
|
+
export function scoreAndRankNodes(nodes) {
|
|
28
|
+
const best = new Map();
|
|
29
|
+
for (const node of nodes) {
|
|
30
|
+
const existing = best.get(node.id);
|
|
31
|
+
if (!existing || node.score > existing.score) {
|
|
32
|
+
best.set(node.id, node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...best.values()].sort((a, b) => b.score - a.score);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Main injection pipeline ───────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds a ranked list of relevant knowledge nodes for the given context.
|
|
42
|
+
*
|
|
43
|
+
* Pipeline:
|
|
44
|
+
* 1. Find component nodes that own the current file (path matching)
|
|
45
|
+
* 2. Semantic search for nodes matching the task context
|
|
46
|
+
* 3. BFS graph traversal from all start nodes (components + semantic hits)
|
|
47
|
+
* 4. Deduplicate and rank by score
|
|
48
|
+
* 5. Return top N with full content loaded
|
|
49
|
+
*
|
|
50
|
+
* @param {object} opts
|
|
51
|
+
* @param {string|null} opts.filePath - File being edited (used for component matching)
|
|
52
|
+
* @param {string} opts.taskContext - Task description for semantic search
|
|
53
|
+
* @param {object} opts.searcher - BookLibSearcher instance
|
|
54
|
+
* @param {number} [opts.limit=8] - Max nodes to return
|
|
55
|
+
* @param {number} [opts.minScore=0.35] - Minimum semantic similarity score
|
|
56
|
+
* @returns {Promise<Array<{id: string, title: string, type: string, body: string, score: number}>>}
|
|
57
|
+
*/
|
|
58
|
+
export async function buildGraphContext({ filePath, taskContext, searcher, limit = 8, minScore = 0.35 }) {
|
|
59
|
+
const { nodesDir } = resolveKnowledgePaths();
|
|
60
|
+
const allNodeIds = listNodes({ nodesDir });
|
|
61
|
+
if (allNodeIds.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
const edges = loadEdges();
|
|
64
|
+
|
|
65
|
+
// Load all component nodes for path matching
|
|
66
|
+
const componentNodes = allNodeIds
|
|
67
|
+
.map(id => {
|
|
68
|
+
const raw = loadNode(id, { nodesDir });
|
|
69
|
+
return raw ? parseNodeFrontmatter(raw) : null;
|
|
70
|
+
})
|
|
71
|
+
.filter(n => n?.type === 'component');
|
|
72
|
+
|
|
73
|
+
// 1. Component nodes that own the current file
|
|
74
|
+
const owningComponents = filePath
|
|
75
|
+
? findOwningComponents(filePath, componentNodes)
|
|
76
|
+
: [];
|
|
77
|
+
|
|
78
|
+
// 2. Semantic search — only knowledge nodes (nodeKind: 'knowledge')
|
|
79
|
+
let semanticResults = [];
|
|
80
|
+
if (taskContext) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = await searcher.search(taskContext, 20, minScore);
|
|
83
|
+
semanticResults = raw
|
|
84
|
+
.filter(r => r.metadata?.nodeKind === 'knowledge' && r.metadata?.id)
|
|
85
|
+
.map(r => ({
|
|
86
|
+
id: r.metadata.id,
|
|
87
|
+
title: r.metadata.title,
|
|
88
|
+
type: r.metadata.type,
|
|
89
|
+
text: r.text,
|
|
90
|
+
score: r.score,
|
|
91
|
+
hop: 0,
|
|
92
|
+
}));
|
|
93
|
+
} catch {
|
|
94
|
+
// Index may not exist yet — skip semantic step gracefully
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. BFS traversal from all start nodes
|
|
99
|
+
const startIds = new Set([
|
|
100
|
+
...owningComponents.map(c => c.id),
|
|
101
|
+
...semanticResults.map(r => r.id),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const traversalHits = [];
|
|
105
|
+
for (const startId of startIds) {
|
|
106
|
+
const hops = traverseEdges(startId, edges, 2);
|
|
107
|
+
for (const { id, hop } of hops) {
|
|
108
|
+
const raw = loadNode(id, { nodesDir });
|
|
109
|
+
if (!raw) continue;
|
|
110
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
111
|
+
traversalHits.push({
|
|
112
|
+
id,
|
|
113
|
+
title: parsed.title,
|
|
114
|
+
type: parsed.type,
|
|
115
|
+
text: parsed.body ?? '',
|
|
116
|
+
score: 0.5 / hop, // distance penalty: hop 1 → 0.5, hop 2 → 0.25
|
|
117
|
+
hop,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 4. Merge, deduplicate, rank
|
|
123
|
+
const ranked = scoreAndRankNodes([...semanticResults, ...traversalHits]);
|
|
124
|
+
|
|
125
|
+
// 5. Load full content for top results
|
|
126
|
+
return ranked.slice(0, limit).map(node => {
|
|
127
|
+
const raw = loadNode(node.id, { nodesDir });
|
|
128
|
+
const parsed = raw ? parseNodeFrontmatter(raw) : {};
|
|
129
|
+
return {
|
|
130
|
+
id: node.id,
|
|
131
|
+
title: parsed.title ?? node.title,
|
|
132
|
+
type: parsed.type ?? node.type,
|
|
133
|
+
body: parsed.body ?? node.text,
|
|
134
|
+
score: node.score,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { listNodes, loadNode, parseNodeFrontmatter, loadEdges, traverseEdges } from './graph.js';
|
|
2
|
+
import { extractKeywords } from './query-expander.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-dimensional graph-activated search.
|
|
6
|
+
* Parses query into concepts, activates graph subregions for each,
|
|
7
|
+
* scores nodes by how many concepts they connect to, merges with text search results.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} query
|
|
10
|
+
* @param {Array} textSearchResults - results from BookLibSearcher.search()
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {string} [opts.nodesDir] - knowledge nodes directory
|
|
13
|
+
* @param {string} [opts.graphFile] - graph.jsonl path
|
|
14
|
+
* @returns {object} { concepts, graphResults, mergedResults, activated }
|
|
15
|
+
*/
|
|
16
|
+
export function graphActivatedSearch(query, textSearchResults = [], opts = {}) {
|
|
17
|
+
const { nodesDir, graphFile } = opts;
|
|
18
|
+
|
|
19
|
+
const concepts = extractConcepts(query);
|
|
20
|
+
|
|
21
|
+
// Single concept or no concepts: skip graph activation — regular search is sufficient
|
|
22
|
+
if (concepts.length < 2) {
|
|
23
|
+
return {
|
|
24
|
+
concepts,
|
|
25
|
+
graphResults: [],
|
|
26
|
+
mergedResults: textSearchResults,
|
|
27
|
+
activated: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const allNodeIds = listNodes({ nodesDir });
|
|
32
|
+
const edges = loadEdges({ graphFile });
|
|
33
|
+
const subgraphs = new Map();
|
|
34
|
+
|
|
35
|
+
for (const concept of concepts) {
|
|
36
|
+
const activated = activateSubgraph(concept, allNodeIds, edges, { nodesDir });
|
|
37
|
+
subgraphs.set(concept, activated);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nodeScores = scoreIntersections(subgraphs);
|
|
41
|
+
|
|
42
|
+
const graphResults = [];
|
|
43
|
+
for (const [nodeId, { score: intersectionScore, matchedConcepts }] of nodeScores.entries()) {
|
|
44
|
+
if (intersectionScore < 2) continue;
|
|
45
|
+
|
|
46
|
+
const raw = loadNode(nodeId, { nodesDir });
|
|
47
|
+
if (!raw) continue;
|
|
48
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
49
|
+
|
|
50
|
+
graphResults.push({
|
|
51
|
+
principle: parsed.title ?? nodeId,
|
|
52
|
+
context: (parsed.body ?? '').slice(0, 150),
|
|
53
|
+
source: `project ${parsed.type ?? 'knowledge'}: ${nodeId}`,
|
|
54
|
+
section: 'knowledge',
|
|
55
|
+
matchedConcepts,
|
|
56
|
+
intersectionScore,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
graphResults.sort((a, b) => b.intersectionScore - a.intersectionScore);
|
|
61
|
+
|
|
62
|
+
const textPrinciples = textSearchResults.map(r => ({
|
|
63
|
+
principle: r.text?.slice(0, 150) ?? '',
|
|
64
|
+
context: '',
|
|
65
|
+
source: r.metadata?.name ?? 'unknown',
|
|
66
|
+
section: r.metadata?.type ?? 'content',
|
|
67
|
+
matchedConcepts: [],
|
|
68
|
+
intersectionScore: 0,
|
|
69
|
+
score: r.score,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const mergedResults = [...graphResults, ...textPrinciples];
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
concepts,
|
|
76
|
+
graphResults,
|
|
77
|
+
mergedResults,
|
|
78
|
+
activated: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract semantic concepts from a query.
|
|
84
|
+
* Groups consecutive keywords into compound concepts.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} query
|
|
87
|
+
* @returns {string[]}
|
|
88
|
+
*/
|
|
89
|
+
export function extractConcepts(query) {
|
|
90
|
+
const keywords = extractKeywords(query);
|
|
91
|
+
if (keywords.length === 0) return [];
|
|
92
|
+
|
|
93
|
+
// Collect runs of consecutive keywords from the original query
|
|
94
|
+
const words = query.toLowerCase().split(/\s+/);
|
|
95
|
+
const runs = [];
|
|
96
|
+
let current = [];
|
|
97
|
+
|
|
98
|
+
for (const word of words) {
|
|
99
|
+
const isKeyword = keywords.includes(word);
|
|
100
|
+
if (isKeyword) {
|
|
101
|
+
current.push(word);
|
|
102
|
+
} else if (current.length > 0) {
|
|
103
|
+
runs.push(current);
|
|
104
|
+
current = [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (current.length > 0) {
|
|
108
|
+
runs.push(current);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Split runs into concepts: compound terms (max 2 words) stay grouped,
|
|
112
|
+
// longer runs are split into individual concepts
|
|
113
|
+
const concepts = [];
|
|
114
|
+
for (const run of runs) {
|
|
115
|
+
if (run.length <= 2) {
|
|
116
|
+
concepts.push(run.join(' '));
|
|
117
|
+
} else {
|
|
118
|
+
for (const word of run) {
|
|
119
|
+
concepts.push(word);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [...new Set(concepts)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Activate a subgraph for a single concept.
|
|
129
|
+
* Finds matching nodes + their 1-hop neighbors.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} concept
|
|
132
|
+
* @param {string[]} allNodeIds
|
|
133
|
+
* @param {Array} edges
|
|
134
|
+
* @param {{ nodesDir?: string }} opts
|
|
135
|
+
* @returns {Set<string>} activated node IDs
|
|
136
|
+
*/
|
|
137
|
+
function activateSubgraph(concept, allNodeIds, edges, { nodesDir } = {}) {
|
|
138
|
+
const activated = new Set();
|
|
139
|
+
const conceptWords = concept.split(/\s+/);
|
|
140
|
+
|
|
141
|
+
for (const nodeId of allNodeIds) {
|
|
142
|
+
const raw = loadNode(nodeId, { nodesDir });
|
|
143
|
+
if (!raw) continue;
|
|
144
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
145
|
+
|
|
146
|
+
const nodeText = `${parsed.title ?? ''} ${parsed.body ?? ''} ${(parsed.tags ?? []).join(' ')}`.toLowerCase();
|
|
147
|
+
|
|
148
|
+
const matches = conceptWords.some(w => w.length > 2 && nodeText.includes(w));
|
|
149
|
+
if (matches) {
|
|
150
|
+
activated.add(nodeId);
|
|
151
|
+
|
|
152
|
+
const neighbors = traverseEdges(nodeId, edges, 1);
|
|
153
|
+
for (const { id } of neighbors) {
|
|
154
|
+
activated.add(id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return activated;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Score nodes by how many concept subgraphs they appear in.
|
|
164
|
+
*
|
|
165
|
+
* @param {Map<string, Set<string>>} subgraphs
|
|
166
|
+
* @returns {Map<string, { score: number, matchedConcepts: string[] }>}
|
|
167
|
+
*/
|
|
168
|
+
function scoreIntersections(subgraphs) {
|
|
169
|
+
const scores = new Map();
|
|
170
|
+
|
|
171
|
+
for (const [concept, activated] of subgraphs.entries()) {
|
|
172
|
+
for (const nodeId of activated) {
|
|
173
|
+
if (!scores.has(nodeId)) {
|
|
174
|
+
scores.set(nodeId, { score: 0, matchedConcepts: [] });
|
|
175
|
+
}
|
|
176
|
+
const entry = scores.get(nodeId);
|
|
177
|
+
entry.score++;
|
|
178
|
+
entry.matchedConcepts.push(concept);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return scores;
|
|
183
|
+
}
|