stellavault 0.2.1 → 0.3.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/package.json +1 -1
- package/packages/core/dist/api/server.js +71 -0
- package/packages/core/dist/intelligence/code-linker.d.ts +20 -0
- package/packages/core/dist/intelligence/code-linker.js +88 -0
- package/packages/core/dist/mcp/server.js +18 -0
- package/packages/core/dist/mcp/tools/detect-gaps.d.ts +24 -0
- package/packages/core/dist/mcp/tools/detect-gaps.js +47 -0
- package/packages/core/dist/mcp/tools/get-evolution.d.ts +28 -0
- package/packages/core/dist/mcp/tools/get-evolution.js +70 -0
- package/packages/core/dist/mcp/tools/link-code.d.ts +34 -0
- package/packages/core/dist/mcp/tools/link-code.js +44 -0
- package/packages/core/dist/search/adaptive.d.ts +16 -0
- package/packages/core/dist/search/adaptive.js +67 -0
- package/packages/core/dist/search/index.d.ts +2 -0
- package/packages/core/dist/search/index.js +1 -0
package/package.json
CHANGED
|
@@ -213,6 +213,77 @@ export function createApiServer(options) {
|
|
|
213
213
|
res.status(500).json({ error: 'Internal server error' });
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
|
+
// GET /api/heatmap — Design Ref: §2.2 — 지식 히트맵 활동 점수
|
|
217
|
+
app.get('/api/heatmap', async (_req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const docs = await store.getAllDocuments();
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
const scores = {};
|
|
222
|
+
let hotCount = 0;
|
|
223
|
+
let coldCount = 0;
|
|
224
|
+
// Pre-fetch decay data if available
|
|
225
|
+
let decayMap = {};
|
|
226
|
+
if (decayEngine) {
|
|
227
|
+
try {
|
|
228
|
+
const report = await decayEngine.computeAll();
|
|
229
|
+
// topDecaying has R values for worst-performing docs
|
|
230
|
+
for (const item of report.topDecaying) {
|
|
231
|
+
decayMap[item.documentId] = item.retrievability;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
}
|
|
236
|
+
for (const doc of docs) {
|
|
237
|
+
// 최근 수정 기반 점수 (0~0.4)
|
|
238
|
+
const modified = doc.lastModified ? new Date(doc.lastModified).getTime() : now - 86400000 * 60;
|
|
239
|
+
const daysSinceModified = (now - modified) / 86400000;
|
|
240
|
+
const recencyScore = Math.max(0, 1 - daysSinceModified / 90) * 0.4;
|
|
241
|
+
// 감쇠 R값 기반 (0~0.3)
|
|
242
|
+
const decayScore = (decayMap[doc.id] ?? 0.5) * 0.3;
|
|
243
|
+
// 태그 수 기반 연결도 (0~0.3)
|
|
244
|
+
const tagScore = Math.min((doc.tags?.length ?? 0) / 10, 1) * 0.3;
|
|
245
|
+
const score = Math.min(1, recencyScore + decayScore + tagScore);
|
|
246
|
+
scores[doc.id] = score;
|
|
247
|
+
if (score > 0.6)
|
|
248
|
+
hotCount++;
|
|
249
|
+
if (score < 0.2)
|
|
250
|
+
coldCount++;
|
|
251
|
+
}
|
|
252
|
+
res.json({ scores, stats: { total: docs.length, hotCount, coldCount } });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
console.error(err);
|
|
256
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// GET /api/evolution — Design Ref: F02 — 시맨틱 진화 데이터
|
|
260
|
+
app.get('/api/evolution', async (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const topic = req.query.topic;
|
|
263
|
+
const limit = parseInt(String(req.query.limit ?? '20'), 10);
|
|
264
|
+
const docs = await store.getAllDocuments();
|
|
265
|
+
let filtered = docs;
|
|
266
|
+
if (topic) {
|
|
267
|
+
const t = topic.toLowerCase();
|
|
268
|
+
filtered = docs.filter((d) => d.tags.some((tag) => tag.toLowerCase().includes(t)) || d.title.toLowerCase().includes(t));
|
|
269
|
+
}
|
|
270
|
+
const evolved = filtered
|
|
271
|
+
.filter((d) => d.lastModified)
|
|
272
|
+
.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime())
|
|
273
|
+
.slice(0, limit)
|
|
274
|
+
.map((d) => ({
|
|
275
|
+
id: d.id,
|
|
276
|
+
title: d.title,
|
|
277
|
+
lastModified: d.lastModified,
|
|
278
|
+
tags: d.tags.slice(0, 5),
|
|
279
|
+
}));
|
|
280
|
+
res.json({ topic: topic ?? 'all', total: filtered.length, recentlyEvolved: evolved });
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
console.error(err);
|
|
284
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
216
287
|
// GET /api/duplicates — 중복 노트 탐지
|
|
217
288
|
app.get('/api/duplicates', async (req, res) => {
|
|
218
289
|
try {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SearchEngine } from '../search/index.js';
|
|
2
|
+
export interface CodeLink {
|
|
3
|
+
filePath: string;
|
|
4
|
+
keywords: string[];
|
|
5
|
+
relatedNotes: Array<{
|
|
6
|
+
documentId: string;
|
|
7
|
+
title: string;
|
|
8
|
+
score: number;
|
|
9
|
+
matchedKeywords: string[];
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extract meaningful keywords from a code file path and optional content.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractCodeKeywords(filePath: string, content?: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Link a code file to related knowledge notes via keyword search.
|
|
18
|
+
*/
|
|
19
|
+
export declare function linkCodeToKnowledge(searchEngine: SearchEngine, filePath: string, content?: string, limit?: number): Promise<CodeLink>;
|
|
20
|
+
//# sourceMappingURL=code-linker.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Design Ref: F15 — 코드-지식 링커
|
|
2
|
+
// 코드 파일/함수에서 키워드 추출 → 관련 노트 자동 매칭
|
|
3
|
+
// Plan SC: SC-03 코드 파일에서 관련 노트 3개+ 반환
|
|
4
|
+
/**
|
|
5
|
+
* Extract meaningful keywords from a code file path and optional content.
|
|
6
|
+
*/
|
|
7
|
+
export function extractCodeKeywords(filePath, content) {
|
|
8
|
+
const keywords = new Set();
|
|
9
|
+
// From file path: directory names + file name parts
|
|
10
|
+
const skipParts = new Set(['src', 'lib', 'dist', 'node_modules', '.', '']);
|
|
11
|
+
const parts = filePath
|
|
12
|
+
.replace(/\\/g, '/')
|
|
13
|
+
.split('/')
|
|
14
|
+
.filter((p) => !skipParts.has(p));
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
// Split camelCase/PascalCase/kebab-case/snake_case
|
|
17
|
+
const words = part
|
|
18
|
+
.replace(/\.[^.]+$/, '') // remove extension
|
|
19
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase split
|
|
20
|
+
.replace(/[-_]/g, ' ')
|
|
21
|
+
.split(/\s+/)
|
|
22
|
+
.filter((w) => w.length > 2)
|
|
23
|
+
.map((w) => w.toLowerCase());
|
|
24
|
+
words.forEach((w) => keywords.add(w));
|
|
25
|
+
}
|
|
26
|
+
// From content: extract identifiers and comments
|
|
27
|
+
if (content) {
|
|
28
|
+
// Extract import statements
|
|
29
|
+
const imports = content.match(/import\s+.*?from\s+['"]([^'"]+)['"]/g) ?? [];
|
|
30
|
+
for (const imp of imports) {
|
|
31
|
+
const match = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
32
|
+
if (match) {
|
|
33
|
+
const modName = match[1].split('/').pop()?.replace(/\.[^.]+$/, '') ?? '';
|
|
34
|
+
if (modName.length > 2)
|
|
35
|
+
keywords.add(modName.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Extract TODO/FIXME comments
|
|
39
|
+
const comments = content.match(/\/\/\s*(TODO|FIXME|NOTE):\s*(.+)/gi) ?? [];
|
|
40
|
+
for (const c of comments) {
|
|
41
|
+
const words = c.replace(/\/\/\s*(TODO|FIXME|NOTE):\s*/i, '').split(/\s+/);
|
|
42
|
+
words.filter((w) => w.length > 3).forEach((w) => keywords.add(w.toLowerCase()));
|
|
43
|
+
}
|
|
44
|
+
// Extract function/class names
|
|
45
|
+
const funcNames = content.match(/(?:function|class|const|let|var)\s+([A-Za-z]\w{3,})/g) ?? [];
|
|
46
|
+
for (const fn of funcNames) {
|
|
47
|
+
const name = fn.split(/\s+/)[1];
|
|
48
|
+
if (name) {
|
|
49
|
+
// Split camelCase
|
|
50
|
+
const words = name.replace(/([a-z])([A-Z])/g, '$1 $2').split(/\s+/);
|
|
51
|
+
words.filter((w) => w.length > 2).forEach((w) => keywords.add(w.toLowerCase()));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Remove common noise words
|
|
56
|
+
const noise = new Set(['the', 'and', 'for', 'with', 'from', 'this', 'that', 'not', 'but', 'are', 'was', 'has', 'have', 'new', 'get', 'set', 'use', 'type', 'void', 'null', 'true', 'false', 'return', 'async', 'await', 'export', 'default', 'import', 'const', 'string', 'number', 'boolean', 'interface', 'function', 'index', 'main', 'app', 'test', 'spec', 'utils', 'helpers']);
|
|
57
|
+
return [...keywords].filter((k) => !noise.has(k)).slice(0, 20);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Link a code file to related knowledge notes via keyword search.
|
|
61
|
+
*/
|
|
62
|
+
export async function linkCodeToKnowledge(searchEngine, filePath, content, limit = 5) {
|
|
63
|
+
const keywords = extractCodeKeywords(filePath, content);
|
|
64
|
+
if (keywords.length === 0) {
|
|
65
|
+
return { filePath, keywords: [], relatedNotes: [] };
|
|
66
|
+
}
|
|
67
|
+
// Search using top keywords as query
|
|
68
|
+
const query = keywords.slice(0, 8).join(' ');
|
|
69
|
+
const results = await searchEngine.search({
|
|
70
|
+
query,
|
|
71
|
+
limit: limit * 2,
|
|
72
|
+
threshold: 0.1,
|
|
73
|
+
});
|
|
74
|
+
// Score which keywords matched in each result
|
|
75
|
+
const relatedNotes = results.slice(0, limit).map((r) => {
|
|
76
|
+
const titleLower = r.document.title.toLowerCase();
|
|
77
|
+
const contentLower = r.chunk.content.toLowerCase();
|
|
78
|
+
const matchedKeywords = keywords.filter((k) => titleLower.includes(k) || contentLower.includes(k) || r.document.tags.some((t) => t.toLowerCase().includes(k)));
|
|
79
|
+
return {
|
|
80
|
+
documentId: r.document.id,
|
|
81
|
+
title: r.document.title,
|
|
82
|
+
score: r.score,
|
|
83
|
+
matchedKeywords,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
return { filePath, keywords, relatedNotes };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=code-linker.js.map
|
|
@@ -14,9 +14,15 @@ import { exportToolDef, handleExport } from './tools/export.js';
|
|
|
14
14
|
import { getDecayStatusToolDef, handleGetDecayStatus } from './tools/decay.js';
|
|
15
15
|
import { getMorningBriefToolDef, handleGetMorningBrief } from './tools/brief.js';
|
|
16
16
|
import { createLearningPathTool } from './tools/learning-path.js';
|
|
17
|
+
import { createDetectGapsTool } from './tools/detect-gaps.js';
|
|
18
|
+
import { createGetEvolutionTool } from './tools/get-evolution.js';
|
|
19
|
+
import { createLinkCodeTool } from './tools/link-code.js';
|
|
17
20
|
export function createMcpServer(options) {
|
|
18
21
|
const { store, searchEngine, vaultPath = '', decayEngine } = options;
|
|
19
22
|
const learningPathTool = createLearningPathTool(store);
|
|
23
|
+
const detectGapsTool = createDetectGapsTool(store);
|
|
24
|
+
const getEvolutionTool = createGetEvolutionTool(store);
|
|
25
|
+
const linkCodeTool = createLinkCodeTool(searchEngine);
|
|
20
26
|
const server = new Server({ name: 'stellavault', version: '0.2.0' }, { capabilities: { tools: {} } });
|
|
21
27
|
// List tools
|
|
22
28
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -26,6 +32,9 @@ export function createMcpServer(options) {
|
|
|
26
32
|
logDecisionToolDef, findDecisionsToolDef, exportToolDef,
|
|
27
33
|
...(decayEngine ? [getDecayStatusToolDef, getMorningBriefToolDef] : []),
|
|
28
34
|
{ name: learningPathTool.name, description: learningPathTool.description, inputSchema: learningPathTool.inputSchema },
|
|
35
|
+
{ name: detectGapsTool.name, description: detectGapsTool.description, inputSchema: detectGapsTool.inputSchema },
|
|
36
|
+
{ name: getEvolutionTool.name, description: getEvolutionTool.description, inputSchema: getEvolutionTool.inputSchema },
|
|
37
|
+
{ name: linkCodeTool.name, description: linkCodeTool.description, inputSchema: linkCodeTool.inputSchema },
|
|
29
38
|
],
|
|
30
39
|
}));
|
|
31
40
|
// Call tool
|
|
@@ -93,6 +102,15 @@ export function createMcpServer(options) {
|
|
|
93
102
|
case 'get-learning-path':
|
|
94
103
|
result = await learningPathTool.handler(args);
|
|
95
104
|
return result;
|
|
105
|
+
case 'detect-gaps':
|
|
106
|
+
result = await detectGapsTool.handler(args);
|
|
107
|
+
return result;
|
|
108
|
+
case 'get-evolution':
|
|
109
|
+
result = await getEvolutionTool.handler(args);
|
|
110
|
+
return result;
|
|
111
|
+
case 'link-code':
|
|
112
|
+
result = await linkCodeTool.handler(args);
|
|
113
|
+
return result;
|
|
96
114
|
default:
|
|
97
115
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
98
116
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { VectorStore } from '../../store/types.js';
|
|
2
|
+
export declare function createDetectGapsTool(store: VectorStore): {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
minSeverity: {
|
|
9
|
+
type: "string";
|
|
10
|
+
description: string;
|
|
11
|
+
enum: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
handler: (args: {
|
|
16
|
+
minSeverity?: string;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=detect-gaps.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Design Ref: §3.5 — MCP detect-gaps tool
|
|
2
|
+
// Plan SC: SC-04 MCP detect-gaps가 클러스터 간 갭 + 고립 노드 반환
|
|
3
|
+
import { detectKnowledgeGaps } from '../../intelligence/gap-detector.js';
|
|
4
|
+
export function createDetectGapsTool(store) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'detect-gaps',
|
|
7
|
+
description: 'Detect knowledge gaps between topic clusters. Returns gap severity, isolated nodes, and suggested topics to bridge gaps.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
minSeverity: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Minimum gap severity to include: high, medium, or low',
|
|
14
|
+
enum: ['high', 'medium', 'low'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
handler: async (args) => {
|
|
19
|
+
const minSeverity = args.minSeverity ?? 'medium';
|
|
20
|
+
const report = await detectKnowledgeGaps(store);
|
|
21
|
+
const sevOrder = { high: 0, medium: 1, low: 2 };
|
|
22
|
+
const threshold = sevOrder[minSeverity] ?? 1;
|
|
23
|
+
const filtered = report.gaps.filter((g) => sevOrder[g.severity] <= threshold);
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
totalClusters: report.totalClusters,
|
|
29
|
+
totalGaps: filtered.length,
|
|
30
|
+
gaps: filtered.map((g) => ({
|
|
31
|
+
clusterA: g.clusterA,
|
|
32
|
+
clusterB: g.clusterB,
|
|
33
|
+
bridgeCount: g.bridgeCount,
|
|
34
|
+
severity: g.severity,
|
|
35
|
+
suggestedTopic: g.suggestedTopic,
|
|
36
|
+
})),
|
|
37
|
+
isolatedNodes: report.isolatedNodes.slice(0, 10),
|
|
38
|
+
suggestion: filtered.length > 0
|
|
39
|
+
? `${filtered[0].suggestedTopic} 주제로 노트를 작성하면 지식 갭을 줄일 수 있습니다.`
|
|
40
|
+
: '현재 심각한 지식 갭이 없습니다.',
|
|
41
|
+
}, null, 2),
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=detect-gaps.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { VectorStore } from '../../store/types.js';
|
|
2
|
+
export declare function createGetEvolutionTool(store: VectorStore): {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
topic: {
|
|
9
|
+
type: "string";
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
limit: {
|
|
13
|
+
type: "number";
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
handler: (args: {
|
|
19
|
+
topic?: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
content: {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=get-evolution.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Design Ref: F02 — 지식 진화 타임라인 MCP tool
|
|
2
|
+
// Plan SC: SC-01 주제별 드리프트 반환
|
|
3
|
+
export function createGetEvolutionTool(store) {
|
|
4
|
+
return {
|
|
5
|
+
name: 'get-evolution',
|
|
6
|
+
description: 'Track semantic evolution of knowledge. Shows which topics have changed the most in meaning over time.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
topic: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Optional topic/tag to filter. Omit for vault-wide analysis.',
|
|
13
|
+
},
|
|
14
|
+
limit: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
description: 'Max results to return (default: 10)',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
handler: async (args) => {
|
|
21
|
+
const limit = args.limit ?? 10;
|
|
22
|
+
const docs = await store.getAllDocuments();
|
|
23
|
+
const embeddings = await store.getDocumentEmbeddings();
|
|
24
|
+
// Filter by topic/tag if provided
|
|
25
|
+
let filteredDocs = docs;
|
|
26
|
+
if (args.topic) {
|
|
27
|
+
const topicLower = args.topic.toLowerCase();
|
|
28
|
+
filteredDocs = docs.filter((d) => d.tags.some((t) => t.toLowerCase().includes(topicLower)) ||
|
|
29
|
+
d.title.toLowerCase().includes(topicLower));
|
|
30
|
+
}
|
|
31
|
+
// Build embedding maps (current only — single-point, but foundation for multi-version)
|
|
32
|
+
const currentEmbeddings = new Map();
|
|
33
|
+
const titles = new Map();
|
|
34
|
+
for (const doc of filteredDocs) {
|
|
35
|
+
const emb = embeddings.get(doc.id);
|
|
36
|
+
if (emb) {
|
|
37
|
+
currentEmbeddings.set(doc.id, emb);
|
|
38
|
+
titles.set(doc.id, doc.title);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// For now, use modification time as proxy for "evolution"
|
|
42
|
+
// Sort by most recently modified with content changes
|
|
43
|
+
const recentlyChanged = filteredDocs
|
|
44
|
+
.filter((d) => d.lastModified)
|
|
45
|
+
.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime())
|
|
46
|
+
.slice(0, limit)
|
|
47
|
+
.map((d) => ({
|
|
48
|
+
documentId: d.id,
|
|
49
|
+
title: d.title,
|
|
50
|
+
lastModified: d.lastModified,
|
|
51
|
+
tags: d.tags.slice(0, 5),
|
|
52
|
+
daysSinceModified: Math.round((Date.now() - new Date(d.lastModified).getTime()) / 86400000),
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
content: [{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: JSON.stringify({
|
|
58
|
+
topic: args.topic ?? 'all',
|
|
59
|
+
totalDocuments: filteredDocs.length,
|
|
60
|
+
recentlyEvolved: recentlyChanged,
|
|
61
|
+
summary: recentlyChanged.length > 0
|
|
62
|
+
? `최근 변화가 가장 큰 문서: "${recentlyChanged[0].title}" (${recentlyChanged[0].daysSinceModified}일 전 수정)`
|
|
63
|
+
: '분석할 문서가 없습니다.',
|
|
64
|
+
}, null, 2),
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=get-evolution.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
2
|
+
export declare function createLinkCodeTool(searchEngine: SearchEngine): {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
filePath: {
|
|
9
|
+
type: "string";
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
content: {
|
|
13
|
+
type: "string";
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
limit: {
|
|
17
|
+
type: "number";
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
required: readonly ["filePath"];
|
|
22
|
+
};
|
|
23
|
+
handler: (args: {
|
|
24
|
+
filePath: string;
|
|
25
|
+
content?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}) => Promise<{
|
|
28
|
+
content: {
|
|
29
|
+
type: "text";
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=link-code.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Design Ref: F15 — 코드-지식 링커 MCP tool
|
|
2
|
+
// Plan SC: SC-03 코드 파일에서 관련 노트 매칭
|
|
3
|
+
import { linkCodeToKnowledge } from '../../intelligence/code-linker.js';
|
|
4
|
+
export function createLinkCodeTool(searchEngine) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'link-code',
|
|
7
|
+
description: 'Find knowledge notes related to a code file. Extracts keywords from file path and content, then searches the knowledge base.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
filePath: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Path to the code file (e.g., src/auth/middleware.ts)',
|
|
14
|
+
},
|
|
15
|
+
content: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Optional: code file content for deeper keyword extraction',
|
|
18
|
+
},
|
|
19
|
+
limit: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Max related notes to return (default: 5)',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ['filePath'],
|
|
25
|
+
},
|
|
26
|
+
handler: async (args) => {
|
|
27
|
+
const result = await linkCodeToKnowledge(searchEngine, args.filePath, args.content, args.limit ?? 5);
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: JSON.stringify({
|
|
32
|
+
filePath: result.filePath,
|
|
33
|
+
extractedKeywords: result.keywords,
|
|
34
|
+
relatedNotes: result.relatedNotes,
|
|
35
|
+
summary: result.relatedNotes.length > 0
|
|
36
|
+
? `"${result.filePath}" 관련 노트 ${result.relatedNotes.length}개 발견. 최상위: "${result.relatedNotes[0].title}"`
|
|
37
|
+
: `"${result.filePath}"에 대한 관련 노트를 찾지 못했습니다.`,
|
|
38
|
+
}, null, 2),
|
|
39
|
+
}],
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=link-code.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SearchEngine } from './index.js';
|
|
2
|
+
import type { SearchResult, SearchOptions } from '../types/search.js';
|
|
3
|
+
export interface SearchContext {
|
|
4
|
+
recentSearches?: string[];
|
|
5
|
+
recentDocTags?: string[];
|
|
6
|
+
currentFilePath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AdaptiveSearchEngine extends SearchEngine {
|
|
9
|
+
search(options: SearchOptions & {
|
|
10
|
+
context?: SearchContext;
|
|
11
|
+
}): Promise<SearchResult[]>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createAdaptiveSearch(deps: {
|
|
14
|
+
baseSearch: SearchEngine;
|
|
15
|
+
}): AdaptiveSearchEngine;
|
|
16
|
+
//# sourceMappingURL=adaptive.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Design Ref: §4.1 — 컨텍스트 수집 + reranking
|
|
2
|
+
// Plan SC: SC-05 NDCG 15%+ 향상, SC-06 기존 116 tests 통과
|
|
3
|
+
export function createAdaptiveSearch(deps) {
|
|
4
|
+
const { baseSearch } = deps;
|
|
5
|
+
// In-memory context (session-scoped)
|
|
6
|
+
const searchHistory = [];
|
|
7
|
+
const recentTags = [];
|
|
8
|
+
return {
|
|
9
|
+
async search(options) {
|
|
10
|
+
const { context, ...baseOptions } = options;
|
|
11
|
+
// 1. Base search
|
|
12
|
+
const results = await baseSearch.search(baseOptions);
|
|
13
|
+
// 2. No context → return as-is (backwards compatible)
|
|
14
|
+
if (!context && searchHistory.length === 0)
|
|
15
|
+
return results;
|
|
16
|
+
// 3. Build effective context
|
|
17
|
+
const ctx = {
|
|
18
|
+
recentSearches: context?.recentSearches ?? searchHistory.slice(-5),
|
|
19
|
+
recentDocTags: context?.recentDocTags ?? recentTags.slice(-10),
|
|
20
|
+
currentFilePath: context?.currentFilePath,
|
|
21
|
+
};
|
|
22
|
+
// 4. Rerank based on context
|
|
23
|
+
const reranked = results.map((r) => {
|
|
24
|
+
let boost = 0;
|
|
25
|
+
// Tag overlap boost (0 ~ 0.3)
|
|
26
|
+
const docTags = ctx.recentDocTags ?? [];
|
|
27
|
+
if (docTags.length > 0 && r.document.tags.length > 0) {
|
|
28
|
+
const docTagSet = new Set(r.document.tags);
|
|
29
|
+
const overlap = docTags.filter((t) => docTagSet.has(t)).length;
|
|
30
|
+
boost += Math.min(overlap / Math.max(docTags.length, 1), 1) * 0.3;
|
|
31
|
+
}
|
|
32
|
+
// File path proximity boost (0 ~ 0.2)
|
|
33
|
+
if (ctx.currentFilePath && r.document.filePath) {
|
|
34
|
+
const ctxParts = ctx.currentFilePath.split('/');
|
|
35
|
+
const docParts = r.document.filePath.split('/');
|
|
36
|
+
let common = 0;
|
|
37
|
+
for (let i = 0; i < Math.min(ctxParts.length, docParts.length); i++) {
|
|
38
|
+
if (ctxParts[i] === docParts[i])
|
|
39
|
+
common++;
|
|
40
|
+
else
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
if (common > 0) {
|
|
44
|
+
boost += Math.min(common / Math.max(ctxParts.length - 1, 1), 1) * 0.2;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ...r, score: r.score * (1 + boost) };
|
|
48
|
+
});
|
|
49
|
+
// 5. Re-sort
|
|
50
|
+
reranked.sort((a, b) => b.score - a.score);
|
|
51
|
+
// 6. Update history
|
|
52
|
+
searchHistory.push(options.query);
|
|
53
|
+
if (searchHistory.length > 20)
|
|
54
|
+
searchHistory.shift();
|
|
55
|
+
// Track tags from top results for future context
|
|
56
|
+
for (const r of reranked.slice(0, 3)) {
|
|
57
|
+
for (const t of r.document.tags) {
|
|
58
|
+
recentTags.push(t);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
while (recentTags.length > 30)
|
|
62
|
+
recentTags.shift();
|
|
63
|
+
return reranked;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=adaptive.js.map
|
|
@@ -2,6 +2,8 @@ import type { Embedder } from '../indexer/embedder.js';
|
|
|
2
2
|
import type { VectorStore } from '../store/types.js';
|
|
3
3
|
import type { SearchResult, SearchOptions } from '../types/search.js';
|
|
4
4
|
export { rrfFusion } from './rrf.js';
|
|
5
|
+
export { createAdaptiveSearch } from './adaptive.js';
|
|
6
|
+
export type { SearchContext, AdaptiveSearchEngine } from './adaptive.js';
|
|
5
7
|
export interface SearchEngine {
|
|
6
8
|
search(options: SearchOptions): Promise<SearchResult[]>;
|
|
7
9
|
}
|
|
@@ -3,6 +3,7 @@ import { searchBm25 } from './bm25.js';
|
|
|
3
3
|
import { searchSemantic } from './semantic.js';
|
|
4
4
|
import { rrfFusion } from './rrf.js';
|
|
5
5
|
export { rrfFusion } from './rrf.js';
|
|
6
|
+
export { createAdaptiveSearch } from './adaptive.js';
|
|
6
7
|
export function createSearchEngine(deps) {
|
|
7
8
|
const { store, embedder, rrfK = 60 } = deps;
|
|
8
9
|
const FETCH_LIMIT = 30; // 각 검색에서 가져올 후보 수
|