memento-mcp-server 1.16.0-a → 1.16.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.
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ground Truth 데이터 생성 CLI 스크립트
4
+ *
5
+ * 사용법:
6
+ * npm run quality:ground-truth:generate
7
+ * npm run quality:ground-truth:generate -- --seed 12345
8
+ * npm run quality:ground-truth:generate -- --queries "React,TypeScript,database"
9
+ * npm run quality:ground-truth:generate -- --relevant-count 10
10
+ * npm run quality:ground-truth:generate -- --strategy random
11
+ * npm run quality:ground-truth:generate -- --output custom-path.json
12
+ *
13
+ * 예제:
14
+ * npm run quality:ground-truth:generate
15
+ * npm run quality:ground-truth:generate -- --seed 12345 --queries "React,TypeScript" --relevant-count 5
16
+ * npm run quality:ground-truth:generate -- --strategy pattern --output data/my-ground-truth.json
17
+ */
18
+
19
+ import { existsSync } from 'fs';
20
+ import { join } from 'path';
21
+ import Database from 'better-sqlite3';
22
+ import { initializeDatabase } from '../src/infrastructure/database/database/init.js';
23
+ import { DatabaseUtils } from '../src/shared/utils/database.js';
24
+ import {
25
+ generateGroundTruth,
26
+ saveGroundTruth,
27
+ loadGroundTruth,
28
+ type GroundTruthGenerationOptions,
29
+ type GroundTruth
30
+ } from '../src/test/helpers/vector-search-quality-metrics.js';
31
+ import { HybridSearchFactory } from '../src/domains/search/factories/hybrid-search.factory.js';
32
+ import type { HybridSearchQuery } from '../src/domains/search/algorithms/hybrid-search-engine.js';
33
+ import { getStopWords } from '../src/shared/utils/stopwords.js';
34
+
35
+ /**
36
+ * CLI 옵션
37
+ */
38
+ interface CliOptions {
39
+ seed?: number;
40
+ queries?: string;
41
+ relevantCount?: number;
42
+ strategy?: 'random' | 'first' | 'pattern' | 'search';
43
+ output?: string;
44
+ force?: boolean;
45
+ help?: boolean;
46
+ autoQueries?: boolean; // 메모리 내용에서 쿼리 자동 생성
47
+ queryCount?: number; // 자동 생성할 쿼리 수 (기본값: 5)
48
+ }
49
+
50
+ /**
51
+ * 명령줄 인자 파싱
52
+ */
53
+ function parseArgs(): CliOptions {
54
+ const args = process.argv.slice(2);
55
+ const options: CliOptions = {};
56
+
57
+ for (let i = 0; i < args.length; i++) {
58
+ const arg = args[i];
59
+ if (arg === '--seed' && args[i + 1]) {
60
+ options.seed = parseInt(args[i + 1], 10);
61
+ i++;
62
+ } else if (arg === '--queries' && args[i + 1]) {
63
+ options.queries = args[i + 1];
64
+ i++;
65
+ } else if (arg === '--relevant-count' && args[i + 1]) {
66
+ options.relevantCount = parseInt(args[i + 1], 10);
67
+ i++;
68
+ } else if (arg === '--strategy' && args[i + 1]) {
69
+ options.strategy = args[i + 1] as 'random' | 'first' | 'pattern';
70
+ i++;
71
+ } else if (arg === '--output' && args[i + 1]) {
72
+ options.output = args[i + 1];
73
+ i++;
74
+ } else if (arg === '--force') {
75
+ options.force = true;
76
+ } else if (arg === '--auto-queries') {
77
+ options.autoQueries = true;
78
+ } else if (arg === '--query-count' && args[i + 1]) {
79
+ options.queryCount = parseInt(args[i + 1], 10);
80
+ i++;
81
+ } else if (arg === '--help' || arg === '-h') {
82
+ options.help = true;
83
+ }
84
+ }
85
+
86
+ return options;
87
+ }
88
+
89
+ /**
90
+ * 도움말 출력
91
+ */
92
+ function printHelp(): void {
93
+ console.log(`
94
+ Ground Truth 데이터 생성 CLI
95
+
96
+ 사용법:
97
+ npm run quality:ground-truth:generate [options]
98
+
99
+ 옵션:
100
+ --seed <number> 시드 값 (재현성을 위해 사용, 기본값: 12345)
101
+ --queries <string> 쿼리 목록 (쉼표로 구분, 기본값: "React,TypeScript,database,MCP,optimization")
102
+ --relevant-count <number> 각 쿼리당 관련 결과 수 (기본값: 5)
103
+ --strategy <strategy> 관련 결과 선택 전략 (random|first|pattern|search, 기본값: random)
104
+ --auto-queries 메모리 내용에서 쿼리 자동 생성 (권장)
105
+ --query-count <number> 자동 생성할 쿼리 수 (기본값: 5, --auto-queries 사용 시)
106
+ --output <file> 출력 파일 경로 (기본값: data/vector-search-quality-ground-truth.json)
107
+ --force 기존 파일이 있어도 덮어쓰기
108
+ --help, -h 도움말 출력
109
+
110
+ 전략 설명:
111
+ random: 랜덤 선택 (시드 기반, 재현 가능)
112
+ first: 처음 N개 선택
113
+ pattern: 패턴 기반 선택 (쿼리별로 다른 패턴)
114
+ search: 실제 검색을 수행하여 관련 메모리 찾기 (--auto-queries와 함께 사용 권장)
115
+
116
+ 예제:
117
+ npm run quality:ground-truth:generate
118
+ npm run quality:ground-truth:generate -- --auto-queries --strategy search
119
+ npm run quality:ground-truth:generate -- --seed 12345 --queries "React,TypeScript" --relevant-count 5
120
+ npm run quality:ground-truth:generate -- --auto-queries --query-count 10 --strategy search
121
+ npm run quality:ground-truth:generate -- --strategy pattern --output data/my-ground-truth.json
122
+ npm run quality:ground-truth:generate -- --force
123
+ `);
124
+ }
125
+
126
+ /**
127
+ * 데이터베이스에서 메모리 ID 목록 조회
128
+ */
129
+ async function getMemoryIds(db: Database.Database, limit: number = 1000): Promise<string[]> {
130
+ const memories = await DatabaseUtils.all(
131
+ db,
132
+ 'SELECT id FROM memory_item ORDER BY created_at DESC LIMIT ?',
133
+ [limit]
134
+ );
135
+
136
+ return memories.map((memory: any) => memory.id);
137
+ }
138
+
139
+ /**
140
+ * 메모리 내용에서 키워드 추출
141
+ * 빈도 기반으로 주요 키워드를 추출합니다.
142
+ */
143
+ function extractKeywordsFromMemories(
144
+ memories: Array<{ content: string }>,
145
+ maxKeywords: number = 10
146
+ ): string[] {
147
+ const stopWords = getStopWords();
148
+ const wordFreq = new Map<string, number>();
149
+
150
+ // 각 메모리에서 단어 추출 및 빈도 계산
151
+ for (const memory of memories) {
152
+ const words = memory.content
153
+ .toLowerCase()
154
+ .replace(/[^\w\s가-힣]/g, ' ') // 특수문자 제거, 한글 유지
155
+ .split(/\s+/)
156
+ .filter(word => {
157
+ // 불용어 제거 및 최소 길이 체크
158
+ return word.length >= 2 &&
159
+ word.length <= 20 &&
160
+ !stopWords.has(word) &&
161
+ !/^\d+$/.test(word); // 숫자만 있는 단어 제외
162
+ });
163
+
164
+ for (const word of words) {
165
+ wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
166
+ }
167
+ }
168
+
169
+ // 빈도순으로 정렬하고 상위 키워드 선택
170
+ const sortedKeywords = Array.from(wordFreq.entries())
171
+ .sort((a, b) => b[1] - a[1])
172
+ .slice(0, maxKeywords * 2) // 더 많이 선택하여 필터링
173
+ .map(([word]) => word)
174
+ .filter(word => word.length >= 2); // 최소 길이 재확인
175
+
176
+ // 최종 키워드 선택 (중복 제거 및 길이 제한)
177
+ const uniqueKeywords = Array.from(new Set(sortedKeywords))
178
+ .slice(0, maxKeywords);
179
+
180
+ return uniqueKeywords;
181
+ }
182
+
183
+ /**
184
+ * 실제 검색을 수행하여 관련 메모리 찾기
185
+ */
186
+ async function generateGroundTruthFromSearch(
187
+ db: Database.Database,
188
+ queries: string[],
189
+ relevantCount: number = 5
190
+ ): Promise<GroundTruth[]> {
191
+ const groundTruths: GroundTruth[] = [];
192
+ const searchEngine = HybridSearchFactory.createDefaultEngine(db);
193
+
194
+ for (const query of queries) {
195
+ try {
196
+ const searchQuery: HybridSearchQuery = {
197
+ query: query,
198
+ limit: relevantCount * 2 // 더 많은 결과를 가져와서 필터링
199
+ };
200
+
201
+ const searchResult = await searchEngine.search(db, searchQuery);
202
+
203
+ // 검색 결과에서 상위 N개를 관련 메모리로 선택
204
+ const relevantIds = searchResult.items
205
+ .slice(0, relevantCount)
206
+ .map(item => item.id);
207
+
208
+ if (relevantIds.length > 0) {
209
+ groundTruths.push({
210
+ queryId: query,
211
+ relevantIds
212
+ });
213
+ }
214
+ } catch (error) {
215
+ console.warn(`⚠️ 쿼리 "${query}" 검색 실패:`, error instanceof Error ? error.message : String(error));
216
+ // 검색 실패 시 빈 Ground Truth 추가하지 않음
217
+ }
218
+ }
219
+
220
+ return groundTruths;
221
+ }
222
+
223
+ /**
224
+ * 메인 함수
225
+ */
226
+ async function main(): Promise<void> {
227
+ const options = parseArgs();
228
+
229
+ if (options.help) {
230
+ printHelp();
231
+ process.exit(0);
232
+ }
233
+
234
+ try {
235
+ // 데이터베이스 초기화
236
+ console.log('🗄️ SQLite 데이터베이스 초기화 중...');
237
+ const db = await initializeDatabase();
238
+
239
+ // 메모리 ID 목록 조회
240
+ console.log('📋 메모리 ID 목록 조회 중...');
241
+ const memoryIds = await getMemoryIds(db);
242
+
243
+ if (memoryIds.length === 0) {
244
+ console.error('❌ 데이터베이스에 메모리가 없습니다. 먼저 메모리를 저장해주세요.');
245
+ db.close();
246
+ process.exit(1);
247
+ }
248
+
249
+ console.log(`✅ ${memoryIds.length}개의 메모리 ID를 찾았습니다.`);
250
+
251
+ // 출력 파일 경로 결정
252
+ const defaultPath = join(process.cwd(), 'data', 'vector-search-quality-ground-truth.json');
253
+ const outputPath = options.output || defaultPath;
254
+
255
+ // 기존 파일 확인
256
+ if (existsSync(outputPath) && !options.force) {
257
+ console.log(`⚠️ 기존 Ground Truth 파일이 존재합니다: ${outputPath}`);
258
+ console.log(' --force 옵션을 사용하여 덮어쓸 수 있습니다.');
259
+ const loaded = loadGroundTruth(outputPath);
260
+ if (loaded) {
261
+ console.log(` 현재 파일에는 ${loaded.length}개의 Ground Truth가 있습니다.`);
262
+ }
263
+ db.close();
264
+ process.exit(0);
265
+ }
266
+
267
+ // 쿼리 목록 결정
268
+ let queries: string[] | undefined;
269
+
270
+ if (options.autoQueries) {
271
+ // 메모리 내용에서 키워드 자동 추출
272
+ console.log('🔍 메모리 내용에서 키워드 추출 중...');
273
+ const memories = await DatabaseUtils.all(
274
+ db,
275
+ 'SELECT content FROM memory_item ORDER BY created_at DESC LIMIT 100'
276
+ );
277
+
278
+ const queryCount = options.queryCount || 5;
279
+ const extractedKeywords = extractKeywordsFromMemories(memories, queryCount);
280
+ queries = extractedKeywords;
281
+
282
+ console.log(`✅ ${queries.length}개의 키워드 추출 완료:`, queries.join(', '));
283
+ } else if (options.queries) {
284
+ // 수동으로 지정된 쿼리 사용
285
+ queries = options.queries.split(',').map(q => q.trim()).filter(q => q.length > 0);
286
+ }
287
+
288
+ // Ground Truth 생성
289
+ let groundTruths: GroundTruth[];
290
+
291
+ if (options.strategy === 'search' && queries && queries.length > 0) {
292
+ // 실제 검색을 수행하여 관련 메모리 찾기
293
+ console.log('🔧 실제 검색을 수행하여 Ground Truth 생성 중...');
294
+ console.log(` 쿼리 수: ${queries.length}`);
295
+ console.log(` 쿼리당 관련 결과 수: ${options.relevantCount || 5}`);
296
+
297
+ groundTruths = await generateGroundTruthFromSearch(
298
+ db,
299
+ queries,
300
+ options.relevantCount || 5
301
+ );
302
+ } else {
303
+ // 기존 방식 (랜덤/패턴 선택)
304
+ const generationOptions: GroundTruthGenerationOptions = {
305
+ seed: options.seed,
306
+ queries: queries,
307
+ relevantCountPerQuery: options.relevantCount,
308
+ selectionStrategy: options.strategy
309
+ };
310
+
311
+ console.log('🔧 Ground Truth 생성 중...');
312
+ console.log(` 시드: ${generationOptions.seed || 12345}`);
313
+ console.log(` 쿼리 수: ${queries?.length || 5}`);
314
+ console.log(` 쿼리당 관련 결과 수: ${generationOptions.relevantCountPerQuery || 5}`);
315
+ console.log(` 선택 전략: ${generationOptions.selectionStrategy || 'random'}`);
316
+
317
+ groundTruths = generateGroundTruth(memoryIds, generationOptions);
318
+ }
319
+
320
+ // Ground Truth 저장
321
+ console.log(`💾 Ground Truth 저장 중: ${outputPath}`);
322
+ saveGroundTruth(groundTruths, outputPath);
323
+
324
+ console.log(`✅ Ground Truth 생성 완료!`);
325
+ console.log(` 생성된 Ground Truth 수: ${groundTruths.length}`);
326
+ console.log(` 저장 위치: ${outputPath}`);
327
+ console.log(`\n📊 생성된 Ground Truth 요약:`);
328
+ groundTruths.forEach((gt, index) => {
329
+ console.log(` ${index + 1}. 쿼리: "${gt.queryId}", 관련 결과: ${gt.relevantIds.length}개`);
330
+ });
331
+
332
+ console.log(`\n💡 다음 단계:`);
333
+ console.log(` 1. 품질 리포트 생성: npm run quality:report`);
334
+ console.log(` 2. Ground Truth 확인: cat ${outputPath}`);
335
+
336
+ db.close();
337
+ } catch (error) {
338
+ console.error('❌ 오류 발생:', error instanceof Error ? error.message : String(error));
339
+ if (error instanceof Error && error.stack) {
340
+ console.error(error.stack);
341
+ }
342
+ process.exit(1);
343
+ }
344
+ }
345
+
346
+ // 스크립트 직접 실행 시
347
+ if (import.meta.url === `file://${process.argv[1]}`) {
348
+ main().catch((error) => {
349
+ console.error('예상치 못한 오류:', error);
350
+ process.exit(1);
351
+ });
352
+ }
353
+
@@ -35,6 +35,7 @@ interface CliOptions {
35
35
  from?: string;
36
36
  to?: string;
37
37
  output?: string;
38
+ skipMeasure?: boolean; // 측정 건너뛰기 옵션
38
39
  }
39
40
 
40
41
  /**
@@ -67,6 +68,8 @@ function parseArgs(): CliOptions {
67
68
  } else if (arg === '--output' && args[i + 1]) {
68
69
  options.output = args[i + 1];
69
70
  i++;
71
+ } else if (arg === '--skip-measure') {
72
+ options.skipMeasure = true;
70
73
  } else if (arg === '--help' || arg === '-h') {
71
74
  printHelp();
72
75
  process.exit(0);
@@ -93,6 +96,7 @@ function printHelp(): void {
93
96
  --from <iso8601> 시작 시간 (ISO 8601 형식, 예: 2024-01-01T00:00:00Z)
94
97
  --to <iso8601> 종료 시간 (ISO 8601 형식, 예: 2024-12-31T23:59:59Z)
95
98
  --output <file> 출력 파일 경로 (지정하지 않으면 콘솔에 출력)
99
+ --skip-measure 품질 측정 건너뛰기 (기존 데이터로 리포트만 생성)
96
100
  --help, -h 도움말 출력
97
101
 
98
102
  예제:
@@ -124,6 +128,35 @@ async function main(): Promise<void> {
124
128
  to: options.to
125
129
  };
126
130
 
131
+ // 품질 측정 수행 (--skip-measure 옵션이 없는 경우)
132
+ if (!options.skipMeasure) {
133
+ console.log('🔍 품질 측정 수행 중...');
134
+ const context = options.context || 'default';
135
+ const namespaces = options.namespace ? [options.namespace] : undefined;
136
+
137
+ try {
138
+ const measurementResult = await qualityService.measureQuality({
139
+ measurement_type: 'manual',
140
+ context,
141
+ namespaces,
142
+ record: true
143
+ });
144
+
145
+ console.log(`✅ 품질 측정 완료`);
146
+ console.log(` 전체 상태: ${measurementResult.overall_status === 'pass' ? '✅ PASS' : measurementResult.overall_status === 'warning' ? '⚠️ WARNING' : '❌ FAIL'}`);
147
+ console.log(` 측정된 네임스페이스: ${measurementResult.namespaces.join(', ') || 'all'}`);
148
+ console.log(` 측정 시간: ${measurementResult.measured_at}`);
149
+ console.log('');
150
+ } catch (error) {
151
+ console.warn('⚠️ 품질 측정 중 오류 발생:', error instanceof Error ? error.message : String(error));
152
+ console.warn(' 기존 데이터로 리포트를 생성합니다.');
153
+ console.log('');
154
+ }
155
+ } else {
156
+ console.log('⏭️ 품질 측정 건너뛰기 (기존 데이터 사용)');
157
+ console.log('');
158
+ }
159
+
127
160
  // 리포트 생성
128
161
  console.log('📊 품질 리포트 생성 중...');
129
162
  const report = await qualityService.generateReport(reportOptions);