@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.
Files changed (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. 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
+ }