@graphmemory/server 1.1.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/LICENSE +15 -0
- package/README.md +216 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/embed.js +47 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +71 -0
- package/dist/api/rest/index.js +371 -0
- package/dist/api/rest/knowledge.js +239 -0
- package/dist/api/rest/skills.js +285 -0
- package/dist/api/rest/tasks.js +273 -0
- package/dist/api/rest/tools.js +157 -0
- package/dist/api/rest/validation.js +196 -0
- package/dist/api/rest/websocket.js +71 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +26 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +25 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +58 -0
- package/dist/cli/index.js +617 -0
- package/dist/cli/indexer.js +275 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +204 -0
- package/dist/graphs/docs.js +231 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +768 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1016 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +972 -0
- package/dist/lib/access.js +67 -0
- package/dist/lib/embedder.js +235 -0
- package/dist/lib/events-log.js +401 -0
- package/dist/lib/file-import.js +328 -0
- package/dist/lib/file-mirror.js +461 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/jwt.js +146 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +393 -0
- package/dist/lib/parsers/code.js +214 -0
- package/dist/lib/parsers/codeblock.js +33 -0
- package/dist/lib/parsers/docs.js +199 -0
- package/dist/lib/parsers/languages/index.js +15 -0
- package/dist/lib/parsers/languages/registry.js +68 -0
- package/dist/lib/parsers/languages/types.js +2 -0
- package/dist/lib/parsers/languages/typescript.js +306 -0
- package/dist/lib/project-manager.js +458 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +106 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/team.js +89 -0
- package/dist/lib/watcher.js +67 -0
- package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
- package/dist/ui/assets/index-kKd4mVrh.css +1 -0
- package/dist/ui/favicon.svg +1 -0
- package/dist/ui/icons.svg +24 -0
- package/dist/ui/index.html +14 -0
- package/package.json +89 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchCode = searchCode;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
const bm25_1 = require("../../lib/search/bm25");
|
|
6
|
+
/**
|
|
7
|
+
* Semantic search over the code graph.
|
|
8
|
+
*
|
|
9
|
+
* 1. Score every node by cosine similarity to the query embedding.
|
|
10
|
+
* 2. Filter seeds below `minScore`, take top `topK`.
|
|
11
|
+
* 3. BFS expansion via graph edges up to `bfsDepth` hops with score decay.
|
|
12
|
+
* 4. De-duplicate, re-filter, sort, cap at `maxResults`.
|
|
13
|
+
*/
|
|
14
|
+
function searchCode(graph, queryEmbedding, options = {}) {
|
|
15
|
+
const { topK = 5, bfsDepth = 1, maxResults = 20, minScore = 0.5, bfsDecay = 0.8, queryText, bm25Index, searchMode = 'hybrid', rrfK = 60 } = options;
|
|
16
|
+
const useVector = searchMode !== 'keyword';
|
|
17
|
+
const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
|
|
18
|
+
// --- 1. Score all nodes ---
|
|
19
|
+
const scored = [];
|
|
20
|
+
if (useVector) {
|
|
21
|
+
graph.forEachNode((id, attrs) => {
|
|
22
|
+
if (attrs.embedding.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (useBm25) {
|
|
28
|
+
const bm25Scores = bm25Index.score(queryText);
|
|
29
|
+
if (useVector && scored.length > 0) {
|
|
30
|
+
// RRF fusion of vector and BM25 — include all vector results (not just positive)
|
|
31
|
+
const vectorMap = new Map(scored.map(s => [s.id, s.score]));
|
|
32
|
+
const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
|
|
33
|
+
scored.length = 0;
|
|
34
|
+
for (const [id, score] of fused)
|
|
35
|
+
scored.push({ id, score });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
scored.length = 0;
|
|
39
|
+
for (const [id, score] of bm25Scores)
|
|
40
|
+
scored.push({ id, score });
|
|
41
|
+
}
|
|
42
|
+
// Normalize scores to 0–1 so minScore threshold works uniformly
|
|
43
|
+
const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
|
|
44
|
+
if (maxScore > 0) {
|
|
45
|
+
for (const s of scored)
|
|
46
|
+
s.score /= maxScore;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (scored.length === 0)
|
|
50
|
+
return [];
|
|
51
|
+
scored.sort((a, b) => b.score - a.score);
|
|
52
|
+
// --- 2. Filter seeds ---
|
|
53
|
+
const minS = minScore;
|
|
54
|
+
const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
|
|
55
|
+
if (seeds.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
// --- 3. BFS expansion ---
|
|
58
|
+
const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
|
|
59
|
+
function bfs(startId, seedScore) {
|
|
60
|
+
const queue = [
|
|
61
|
+
{ id: startId, depth: 0, score: seedScore },
|
|
62
|
+
];
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
while (queue.length > 0) {
|
|
65
|
+
const item = queue.shift();
|
|
66
|
+
if (visited.has(item.id))
|
|
67
|
+
continue;
|
|
68
|
+
visited.add(item.id);
|
|
69
|
+
const prev = scoreMap.get(item.id) ?? -Infinity;
|
|
70
|
+
if (item.score > prev)
|
|
71
|
+
scoreMap.set(item.id, item.score);
|
|
72
|
+
if (item.depth >= bfsDepth)
|
|
73
|
+
continue;
|
|
74
|
+
if (item.score * bfsDecay < minS)
|
|
75
|
+
continue;
|
|
76
|
+
const nextScore = item.score * bfsDecay;
|
|
77
|
+
graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
78
|
+
graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const seed of seeds) {
|
|
82
|
+
bfs(seed.id, seed.score);
|
|
83
|
+
}
|
|
84
|
+
// --- 4. Build results ---
|
|
85
|
+
return [...scoreMap.entries()]
|
|
86
|
+
.filter(([, score]) => score >= minS)
|
|
87
|
+
.map(([id, score]) => {
|
|
88
|
+
const attrs = graph.getNodeAttributes(id);
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
fileId: attrs.fileId,
|
|
92
|
+
kind: attrs.kind,
|
|
93
|
+
name: attrs.name,
|
|
94
|
+
signature: attrs.signature,
|
|
95
|
+
docComment: attrs.docComment,
|
|
96
|
+
startLine: attrs.startLine,
|
|
97
|
+
endLine: attrs.endLine,
|
|
98
|
+
score,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.sort((a, b) => b.score - a.score)
|
|
102
|
+
.slice(0, maxResults);
|
|
103
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.search = search;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
const bm25_1 = require("../../lib/search/bm25");
|
|
6
|
+
/**
|
|
7
|
+
* Semantic search over the graph.
|
|
8
|
+
*
|
|
9
|
+
* 1. Score every node by cosine similarity to the query embedding.
|
|
10
|
+
* 2. Discard seeds below `minScore` (default 0 = keep all).
|
|
11
|
+
* 3. Take the top `topK` remaining seeds.
|
|
12
|
+
* 4. BFS from each seed up to `bfsDepth` hops; BFS nodes inherit the seed's
|
|
13
|
+
* score multiplied by `bfsDecay` per hop (default 0.8), so deeper nodes
|
|
14
|
+
* rank lower and are filtered by `minScore` too.
|
|
15
|
+
* 5. De-duplicate and return results sorted by score, capped at `maxResults`.
|
|
16
|
+
*/
|
|
17
|
+
function search(graph, queryEmbedding, options = {}) {
|
|
18
|
+
const { topK = 5, bfsDepth = 1, maxResults = 20, minScore = 0.5, bfsDecay = 0.8, queryText, bm25Index, searchMode = 'hybrid', rrfK = 60 } = options;
|
|
19
|
+
const useVector = searchMode !== 'keyword';
|
|
20
|
+
const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
|
|
21
|
+
// --- 1. Score all nodes ---
|
|
22
|
+
const scored = [];
|
|
23
|
+
if (useVector) {
|
|
24
|
+
graph.forEachNode((id, attrs) => {
|
|
25
|
+
if (attrs.embedding.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (useBm25) {
|
|
31
|
+
const bm25Scores = bm25Index.score(queryText);
|
|
32
|
+
if (useVector && scored.length > 0) {
|
|
33
|
+
// RRF fusion of vector and BM25 — include all vector results (not just positive)
|
|
34
|
+
const vectorMap = new Map(scored.map(s => [s.id, s.score]));
|
|
35
|
+
const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
|
|
36
|
+
scored.length = 0;
|
|
37
|
+
for (const [id, score] of fused)
|
|
38
|
+
scored.push({ id, score });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// BM25-only or vector returned nothing — use BM25 as fallback
|
|
42
|
+
scored.length = 0;
|
|
43
|
+
for (const [id, score] of bm25Scores)
|
|
44
|
+
scored.push({ id, score });
|
|
45
|
+
}
|
|
46
|
+
// Normalize scores to 0–1 so minScore threshold works uniformly
|
|
47
|
+
const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
|
|
48
|
+
if (maxScore > 0) {
|
|
49
|
+
for (const s of scored)
|
|
50
|
+
s.score /= maxScore;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (scored.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
scored.sort((a, b) => b.score - a.score);
|
|
56
|
+
// --- 2. Filter seeds by minScore, then take topK ---
|
|
57
|
+
const minS = minScore;
|
|
58
|
+
const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
|
|
59
|
+
if (seeds.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
// --- 3. BFS expansion with score decay ---
|
|
62
|
+
// scoreMap holds the best score seen for each node
|
|
63
|
+
const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
|
|
64
|
+
function bfs(startId, seedScore) {
|
|
65
|
+
const queue = [
|
|
66
|
+
{ id: startId, depth: 0, score: seedScore },
|
|
67
|
+
];
|
|
68
|
+
const localVisited = new Set();
|
|
69
|
+
while (queue.length > 0) {
|
|
70
|
+
const item = queue.shift();
|
|
71
|
+
if (localVisited.has(item.id))
|
|
72
|
+
continue;
|
|
73
|
+
localVisited.add(item.id);
|
|
74
|
+
// Keep the best score this node has received across all BFS runs
|
|
75
|
+
const prev = scoreMap.get(item.id) ?? -Infinity;
|
|
76
|
+
if (item.score > prev)
|
|
77
|
+
scoreMap.set(item.id, item.score);
|
|
78
|
+
if (item.depth >= bfsDepth)
|
|
79
|
+
continue;
|
|
80
|
+
if (item.score * bfsDecay < minS)
|
|
81
|
+
continue; // prune: deeper hops won't pass threshold
|
|
82
|
+
const nextScore = item.score * bfsDecay;
|
|
83
|
+
graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
84
|
+
graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const seed of seeds) {
|
|
88
|
+
bfs(seed.id, seed.score);
|
|
89
|
+
}
|
|
90
|
+
// --- 4. Build results from scoreMap, apply minScore filter, sort, cap ---
|
|
91
|
+
return [...scoreMap.entries()]
|
|
92
|
+
.filter(([, score]) => score >= minS)
|
|
93
|
+
.map(([id, score]) => {
|
|
94
|
+
const attrs = graph.getNodeAttributes(id);
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
fileId: attrs.fileId,
|
|
98
|
+
title: attrs.title,
|
|
99
|
+
content: attrs.content,
|
|
100
|
+
level: attrs.level,
|
|
101
|
+
score,
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => b.score - a.score)
|
|
105
|
+
.slice(0, maxResults);
|
|
106
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchFileIndex = searchFileIndex;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
/**
|
|
6
|
+
* Semantic search over file nodes by path embedding.
|
|
7
|
+
* Only searches file nodes (directories have empty embeddings).
|
|
8
|
+
* Pure cosine similarity, no BFS expansion.
|
|
9
|
+
*/
|
|
10
|
+
function searchFileIndex(graph, queryEmbedding, options = {}) {
|
|
11
|
+
const { topK = 10, minScore = 0.3 } = options;
|
|
12
|
+
const scored = [];
|
|
13
|
+
graph.forEachNode((_, attrs) => {
|
|
14
|
+
if (attrs.kind !== 'file' || attrs.embedding.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
const score = (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding);
|
|
17
|
+
if (score >= minScore) {
|
|
18
|
+
scored.push({
|
|
19
|
+
filePath: attrs.filePath,
|
|
20
|
+
fileName: attrs.fileName,
|
|
21
|
+
extension: attrs.extension,
|
|
22
|
+
language: attrs.language,
|
|
23
|
+
size: attrs.size,
|
|
24
|
+
score,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return scored
|
|
29
|
+
.sort((a, b) => b.score - a.score)
|
|
30
|
+
.slice(0, topK);
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchDocFiles = searchDocFiles;
|
|
4
|
+
exports.searchCodeFiles = searchCodeFiles;
|
|
5
|
+
const embedder_1 = require("../../lib/embedder");
|
|
6
|
+
function searchDocFiles(graph, queryEmbedding, options = {}) {
|
|
7
|
+
const { topK = 10, minScore = 0.3 } = options;
|
|
8
|
+
// Collect root chunks (level=1) that have a fileEmbedding
|
|
9
|
+
const scored = [];
|
|
10
|
+
graph.forEachNode((_, attrs) => {
|
|
11
|
+
if (attrs.level !== 1 || attrs.fileEmbedding.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
scored.push({
|
|
14
|
+
fileId: attrs.fileId,
|
|
15
|
+
title: attrs.title,
|
|
16
|
+
score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.fileEmbedding),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
// Count chunks per file
|
|
20
|
+
const chunkCounts = new Map();
|
|
21
|
+
graph.forEachNode((_, attrs) => {
|
|
22
|
+
chunkCounts.set(attrs.fileId, (chunkCounts.get(attrs.fileId) ?? 0) + 1);
|
|
23
|
+
});
|
|
24
|
+
return scored
|
|
25
|
+
.filter(s => s.score >= minScore)
|
|
26
|
+
.sort((a, b) => b.score - a.score)
|
|
27
|
+
.slice(0, topK)
|
|
28
|
+
.map(s => ({
|
|
29
|
+
fileId: s.fileId,
|
|
30
|
+
title: s.title,
|
|
31
|
+
chunks: chunkCounts.get(s.fileId) ?? 0,
|
|
32
|
+
score: s.score,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
function searchCodeFiles(graph, queryEmbedding, options = {}) {
|
|
36
|
+
const { topK = 10, minScore = 0.3 } = options;
|
|
37
|
+
// Collect file nodes that have a fileEmbedding
|
|
38
|
+
const scored = [];
|
|
39
|
+
graph.forEachNode((_, attrs) => {
|
|
40
|
+
if (attrs.kind !== 'file' || attrs.fileEmbedding.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
scored.push({
|
|
43
|
+
fileId: attrs.fileId,
|
|
44
|
+
score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.fileEmbedding),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
// Count symbols per file
|
|
48
|
+
const symbolCounts = new Map();
|
|
49
|
+
graph.forEachNode((_, attrs) => {
|
|
50
|
+
symbolCounts.set(attrs.fileId, (symbolCounts.get(attrs.fileId) ?? 0) + 1);
|
|
51
|
+
});
|
|
52
|
+
return scored
|
|
53
|
+
.filter(s => s.score >= minScore)
|
|
54
|
+
.sort((a, b) => b.score - a.score)
|
|
55
|
+
.slice(0, topK)
|
|
56
|
+
.map(s => ({
|
|
57
|
+
fileId: s.fileId,
|
|
58
|
+
symbolCount: symbolCounts.get(s.fileId) ?? 0,
|
|
59
|
+
score: s.score,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchKnowledge = searchKnowledge;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
const bm25_1 = require("../../lib/search/bm25");
|
|
6
|
+
/**
|
|
7
|
+
* Semantic search over the knowledge graph.
|
|
8
|
+
*
|
|
9
|
+
* 1. Score every node by cosine similarity to the query embedding.
|
|
10
|
+
* 2. Filter seeds below `minScore`, take top `topK`.
|
|
11
|
+
* 3. BFS expansion via relation edges up to `bfsDepth` hops with score decay.
|
|
12
|
+
* 4. De-duplicate, re-filter, sort, cap at `maxResults`.
|
|
13
|
+
*/
|
|
14
|
+
function searchKnowledge(graph, queryEmbedding, options = {}) {
|
|
15
|
+
const { topK = 5, bfsDepth = 1, maxResults = 20, minScore = 0.5, bfsDecay = 0.8, queryText, bm25Index, searchMode = 'hybrid', rrfK = 60 } = options;
|
|
16
|
+
const useVector = searchMode !== 'keyword';
|
|
17
|
+
const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
|
|
18
|
+
// --- 1. Score all nodes (skip proxy nodes) ---
|
|
19
|
+
const scored = [];
|
|
20
|
+
if (useVector) {
|
|
21
|
+
graph.forEachNode((id, attrs) => {
|
|
22
|
+
if (attrs.proxyFor)
|
|
23
|
+
return;
|
|
24
|
+
if (attrs.embedding.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (useBm25) {
|
|
30
|
+
const bm25Scores = bm25Index.score(queryText);
|
|
31
|
+
if (useVector && scored.length > 0) {
|
|
32
|
+
// RRF fusion of vector and BM25 — include all vector results (not just positive)
|
|
33
|
+
const vectorMap = new Map(scored.map(s => [s.id, s.score]));
|
|
34
|
+
const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
|
|
35
|
+
scored.length = 0;
|
|
36
|
+
for (const [id, score] of fused)
|
|
37
|
+
scored.push({ id, score });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
scored.length = 0;
|
|
41
|
+
for (const [id, score] of bm25Scores)
|
|
42
|
+
scored.push({ id, score });
|
|
43
|
+
}
|
|
44
|
+
// Normalize scores to 0–1 so minScore threshold works uniformly
|
|
45
|
+
const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
|
|
46
|
+
if (maxScore > 0) {
|
|
47
|
+
for (const s of scored)
|
|
48
|
+
s.score /= maxScore;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (scored.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
scored.sort((a, b) => b.score - a.score);
|
|
54
|
+
// --- 2. Filter seeds ---
|
|
55
|
+
const minS = minScore;
|
|
56
|
+
const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
|
|
57
|
+
if (seeds.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
// --- 3. BFS expansion ---
|
|
60
|
+
const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
|
|
61
|
+
function bfs(startId, seedScore) {
|
|
62
|
+
const queue = [
|
|
63
|
+
{ id: startId, depth: 0, score: seedScore },
|
|
64
|
+
];
|
|
65
|
+
const visited = new Set();
|
|
66
|
+
while (queue.length > 0) {
|
|
67
|
+
const item = queue.shift();
|
|
68
|
+
if (visited.has(item.id))
|
|
69
|
+
continue;
|
|
70
|
+
visited.add(item.id);
|
|
71
|
+
const prev = scoreMap.get(item.id) ?? -Infinity;
|
|
72
|
+
if (item.score > prev)
|
|
73
|
+
scoreMap.set(item.id, item.score);
|
|
74
|
+
if (item.depth >= bfsDepth)
|
|
75
|
+
continue;
|
|
76
|
+
if (item.score * bfsDecay < minS)
|
|
77
|
+
continue;
|
|
78
|
+
const nextScore = item.score * bfsDecay;
|
|
79
|
+
graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
80
|
+
graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const seed of seeds) {
|
|
84
|
+
bfs(seed.id, seed.score);
|
|
85
|
+
}
|
|
86
|
+
// --- 4. Build results (exclude proxy nodes) ---
|
|
87
|
+
return [...scoreMap.entries()]
|
|
88
|
+
.filter(([id, score]) => score >= minS && !graph.getNodeAttribute(id, 'proxyFor'))
|
|
89
|
+
.map(([id, score]) => {
|
|
90
|
+
const attrs = graph.getNodeAttributes(id);
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
title: attrs.title,
|
|
94
|
+
content: attrs.content,
|
|
95
|
+
tags: attrs.tags,
|
|
96
|
+
score,
|
|
97
|
+
};
|
|
98
|
+
})
|
|
99
|
+
.sort((a, b) => b.score - a.score)
|
|
100
|
+
.slice(0, maxResults);
|
|
101
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchSkills = searchSkills;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
const bm25_1 = require("../../lib/search/bm25");
|
|
6
|
+
/**
|
|
7
|
+
* Semantic search over the skill graph.
|
|
8
|
+
*
|
|
9
|
+
* 1. Score every node by cosine similarity to the query embedding.
|
|
10
|
+
* 2. Filter seeds below `minScore`, take top `topK`.
|
|
11
|
+
* 3. BFS expansion via relation edges up to `bfsDepth` hops with score decay.
|
|
12
|
+
* 4. De-duplicate, re-filter, sort, cap at `maxResults`.
|
|
13
|
+
*/
|
|
14
|
+
function searchSkills(graph, queryEmbedding, options = {}) {
|
|
15
|
+
const { topK = 5, bfsDepth = 1, maxResults = 20, minScore = 0.5, bfsDecay = 0.8, queryText, bm25Index, searchMode = 'hybrid', rrfK = 60 } = options;
|
|
16
|
+
const useVector = searchMode !== 'keyword';
|
|
17
|
+
const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
|
|
18
|
+
// --- 1. Score all nodes (skip proxy nodes) ---
|
|
19
|
+
const scored = [];
|
|
20
|
+
if (useVector) {
|
|
21
|
+
graph.forEachNode((id, attrs) => {
|
|
22
|
+
if (attrs.proxyFor)
|
|
23
|
+
return;
|
|
24
|
+
if (attrs.embedding.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (useBm25) {
|
|
30
|
+
const bm25Scores = bm25Index.score(queryText);
|
|
31
|
+
if (useVector && scored.length > 0) {
|
|
32
|
+
// RRF fusion of vector and BM25 — include all vector results (not just positive)
|
|
33
|
+
const vectorMap = new Map(scored.map(s => [s.id, s.score]));
|
|
34
|
+
const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
|
|
35
|
+
scored.length = 0;
|
|
36
|
+
for (const [id, score] of fused)
|
|
37
|
+
scored.push({ id, score });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
scored.length = 0;
|
|
41
|
+
for (const [id, score] of bm25Scores)
|
|
42
|
+
scored.push({ id, score });
|
|
43
|
+
}
|
|
44
|
+
// Normalize scores to 0–1 so minScore threshold works uniformly
|
|
45
|
+
const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
|
|
46
|
+
if (maxScore > 0) {
|
|
47
|
+
for (const s of scored)
|
|
48
|
+
s.score /= maxScore;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (scored.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
scored.sort((a, b) => b.score - a.score);
|
|
54
|
+
// --- 2. Filter seeds ---
|
|
55
|
+
const minS = minScore;
|
|
56
|
+
const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
|
|
57
|
+
if (seeds.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
// --- 3. BFS expansion ---
|
|
60
|
+
const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
|
|
61
|
+
function bfs(startId, seedScore) {
|
|
62
|
+
const queue = [
|
|
63
|
+
{ id: startId, depth: 0, score: seedScore },
|
|
64
|
+
];
|
|
65
|
+
const visited = new Set();
|
|
66
|
+
while (queue.length > 0) {
|
|
67
|
+
const item = queue.shift();
|
|
68
|
+
if (visited.has(item.id))
|
|
69
|
+
continue;
|
|
70
|
+
visited.add(item.id);
|
|
71
|
+
const prev = scoreMap.get(item.id) ?? -Infinity;
|
|
72
|
+
if (item.score > prev)
|
|
73
|
+
scoreMap.set(item.id, item.score);
|
|
74
|
+
if (item.depth >= bfsDepth)
|
|
75
|
+
continue;
|
|
76
|
+
if (item.score * bfsDecay < minS)
|
|
77
|
+
continue;
|
|
78
|
+
const nextScore = item.score * bfsDecay;
|
|
79
|
+
graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
80
|
+
graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const seed of seeds) {
|
|
84
|
+
bfs(seed.id, seed.score);
|
|
85
|
+
}
|
|
86
|
+
// --- 4. Build results (exclude proxy nodes) ---
|
|
87
|
+
return [...scoreMap.entries()]
|
|
88
|
+
.filter(([id, score]) => score >= minS && !graph.getNodeAttribute(id, 'proxyFor'))
|
|
89
|
+
.map(([id, score]) => {
|
|
90
|
+
const attrs = graph.getNodeAttributes(id);
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
title: attrs.title,
|
|
94
|
+
description: attrs.description,
|
|
95
|
+
source: attrs.source,
|
|
96
|
+
confidence: attrs.confidence,
|
|
97
|
+
usageCount: attrs.usageCount,
|
|
98
|
+
tags: attrs.tags,
|
|
99
|
+
score,
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, maxResults);
|
|
104
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchTasks = searchTasks;
|
|
4
|
+
const embedder_1 = require("../../lib/embedder");
|
|
5
|
+
const bm25_1 = require("../../lib/search/bm25");
|
|
6
|
+
/**
|
|
7
|
+
* Semantic search over the task graph.
|
|
8
|
+
*
|
|
9
|
+
* 1. Score every node by cosine similarity to the query embedding.
|
|
10
|
+
* 2. Filter seeds below `minScore`, take top `topK`.
|
|
11
|
+
* 3. BFS expansion via relation edges up to `bfsDepth` hops with score decay.
|
|
12
|
+
* 4. De-duplicate, re-filter, sort, cap at `maxResults`.
|
|
13
|
+
*/
|
|
14
|
+
function searchTasks(graph, queryEmbedding, options = {}) {
|
|
15
|
+
const { topK = 5, bfsDepth = 1, maxResults = 20, minScore = 0.5, bfsDecay = 0.8, queryText, bm25Index, searchMode = 'hybrid', rrfK = 60 } = options;
|
|
16
|
+
const useVector = searchMode !== 'keyword';
|
|
17
|
+
const useBm25 = searchMode !== 'vector' && !!queryText && !!bm25Index;
|
|
18
|
+
// --- 1. Score all nodes (skip proxy nodes) ---
|
|
19
|
+
const scored = [];
|
|
20
|
+
if (useVector) {
|
|
21
|
+
graph.forEachNode((id, attrs) => {
|
|
22
|
+
if (attrs.proxyFor)
|
|
23
|
+
return;
|
|
24
|
+
if (attrs.embedding.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
scored.push({ id, score: (0, embedder_1.cosineSimilarity)(queryEmbedding, attrs.embedding) });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (useBm25) {
|
|
30
|
+
const bm25Scores = bm25Index.score(queryText);
|
|
31
|
+
if (useVector && scored.length > 0) {
|
|
32
|
+
// RRF fusion of vector and BM25 — include all vector results (not just positive)
|
|
33
|
+
const vectorMap = new Map(scored.map(s => [s.id, s.score]));
|
|
34
|
+
const fused = (0, bm25_1.rrfFuse)(vectorMap, bm25Scores, rrfK);
|
|
35
|
+
scored.length = 0;
|
|
36
|
+
for (const [id, score] of fused)
|
|
37
|
+
scored.push({ id, score });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
scored.length = 0;
|
|
41
|
+
for (const [id, score] of bm25Scores)
|
|
42
|
+
scored.push({ id, score });
|
|
43
|
+
}
|
|
44
|
+
// Normalize scores to 0–1 so minScore threshold works uniformly
|
|
45
|
+
const maxScore = scored.reduce((m, s) => Math.max(m, s.score), 0);
|
|
46
|
+
if (maxScore > 0) {
|
|
47
|
+
for (const s of scored)
|
|
48
|
+
s.score /= maxScore;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (scored.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
scored.sort((a, b) => b.score - a.score);
|
|
54
|
+
// --- 2. Filter seeds ---
|
|
55
|
+
const minS = minScore;
|
|
56
|
+
const seeds = scored.filter(s => s.score >= minS).slice(0, topK);
|
|
57
|
+
if (seeds.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
// --- 3. BFS expansion ---
|
|
60
|
+
const scoreMap = new Map(seeds.map(s => [s.id, s.score]));
|
|
61
|
+
function bfs(startId, seedScore) {
|
|
62
|
+
const queue = [
|
|
63
|
+
{ id: startId, depth: 0, score: seedScore },
|
|
64
|
+
];
|
|
65
|
+
const visited = new Set();
|
|
66
|
+
while (queue.length > 0) {
|
|
67
|
+
const item = queue.shift();
|
|
68
|
+
if (visited.has(item.id))
|
|
69
|
+
continue;
|
|
70
|
+
visited.add(item.id);
|
|
71
|
+
const prev = scoreMap.get(item.id) ?? -Infinity;
|
|
72
|
+
if (item.score > prev)
|
|
73
|
+
scoreMap.set(item.id, item.score);
|
|
74
|
+
if (item.depth >= bfsDepth)
|
|
75
|
+
continue;
|
|
76
|
+
if (item.score * bfsDecay < minS)
|
|
77
|
+
continue;
|
|
78
|
+
const nextScore = item.score * bfsDecay;
|
|
79
|
+
graph.outNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
80
|
+
graph.inNeighbors(item.id).forEach(n => queue.push({ id: n, depth: item.depth + 1, score: nextScore }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const seed of seeds) {
|
|
84
|
+
bfs(seed.id, seed.score);
|
|
85
|
+
}
|
|
86
|
+
// --- 4. Build results (exclude proxy nodes) ---
|
|
87
|
+
return [...scoreMap.entries()]
|
|
88
|
+
.filter(([id, score]) => score >= minS && !graph.getNodeAttribute(id, 'proxyFor'))
|
|
89
|
+
.map(([id, score]) => {
|
|
90
|
+
const attrs = graph.getNodeAttributes(id);
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
title: attrs.title,
|
|
94
|
+
description: attrs.description,
|
|
95
|
+
status: attrs.status,
|
|
96
|
+
priority: attrs.priority,
|
|
97
|
+
tags: attrs.tags,
|
|
98
|
+
score,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.sort((a, b) => b.score - a.score)
|
|
102
|
+
.slice(0, maxResults);
|
|
103
|
+
}
|