memento-mcp-server 1.11.1 → 1.12.0-a

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 (124) hide show
  1. package/README.en.md +21 -6
  2. package/README.md +38 -7
  3. package/dist/algorithms/hybrid-search-engine.d.ts +34 -1
  4. package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
  5. package/dist/algorithms/hybrid-search-engine.js +186 -17
  6. package/dist/algorithms/hybrid-search-engine.js.map +1 -1
  7. package/dist/algorithms/search-ranking.d.ts +15 -1
  8. package/dist/algorithms/search-ranking.d.ts.map +1 -1
  9. package/dist/algorithms/search-ranking.js +41 -4
  10. package/dist/algorithms/search-ranking.js.map +1 -1
  11. package/dist/config/environment.d.ts.map +1 -1
  12. package/dist/config/environment.js +4 -0
  13. package/dist/config/environment.js.map +1 -1
  14. package/dist/config/index.d.ts.map +1 -1
  15. package/dist/config/index.js +6 -0
  16. package/dist/config/index.js.map +1 -1
  17. package/dist/config/ranking-weights-loader.d.ts +37 -0
  18. package/dist/config/ranking-weights-loader.d.ts.map +1 -0
  19. package/dist/config/ranking-weights-loader.js +109 -0
  20. package/dist/config/ranking-weights-loader.js.map +1 -0
  21. package/dist/constants/relation-constants.d.ts +95 -0
  22. package/dist/constants/relation-constants.d.ts.map +1 -0
  23. package/dist/constants/relation-constants.js +95 -0
  24. package/dist/constants/relation-constants.js.map +1 -0
  25. package/dist/database/migration/migrations/005-relation-engine-schema.d.ts +65 -0
  26. package/dist/database/migration/migrations/005-relation-engine-schema.d.ts.map +1 -0
  27. package/dist/database/migration/migrations/005-relation-engine-schema.js +295 -0
  28. package/dist/database/migration/migrations/005-relation-engine-schema.js.map +1 -0
  29. package/dist/database/migration/migrations/005-relation-engine-schema.sql +64 -0
  30. package/dist/services/anchor/anchor-interfaces.d.ts +1 -0
  31. package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -1
  32. package/dist/services/anchor/anchor-interfaces.js.map +1 -1
  33. package/dist/services/anchor/anchor-search-service.d.ts +16 -0
  34. package/dist/services/anchor/anchor-search-service.d.ts.map +1 -1
  35. package/dist/services/anchor/anchor-search-service.js +136 -17
  36. package/dist/services/anchor/anchor-search-service.js.map +1 -1
  37. package/dist/services/batch-scheduler.d.ts +11 -0
  38. package/dist/services/batch-scheduler.d.ts.map +1 -1
  39. package/dist/services/batch-scheduler.js +99 -0
  40. package/dist/services/batch-scheduler.js.map +1 -1
  41. package/dist/services/llm-based-relation-extractor.d.ts +156 -0
  42. package/dist/services/llm-based-relation-extractor.d.ts.map +1 -0
  43. package/dist/services/llm-based-relation-extractor.js +1350 -0
  44. package/dist/services/llm-based-relation-extractor.js.map +1 -0
  45. package/dist/services/relation-extractor.d.ts +73 -0
  46. package/dist/services/relation-extractor.d.ts.map +1 -0
  47. package/dist/services/relation-extractor.js +231 -0
  48. package/dist/services/relation-extractor.js.map +1 -0
  49. package/dist/services/relation-graph.d.ts +275 -0
  50. package/dist/services/relation-graph.d.ts.map +1 -0
  51. package/dist/services/relation-graph.js +869 -0
  52. package/dist/services/relation-graph.js.map +1 -0
  53. package/dist/services/relation-quality-validator.d.ts +211 -0
  54. package/dist/services/relation-quality-validator.d.ts.map +1 -0
  55. package/dist/services/relation-quality-validator.js +415 -0
  56. package/dist/services/relation-quality-validator.js.map +1 -0
  57. package/dist/services/rule-based-relation-extractor.d.ts +66 -0
  58. package/dist/services/rule-based-relation-extractor.d.ts.map +1 -0
  59. package/dist/services/rule-based-relation-extractor.js +258 -0
  60. package/dist/services/rule-based-relation-extractor.js.map +1 -0
  61. package/dist/tools/add-relation-tool.d.ts +34 -0
  62. package/dist/tools/add-relation-tool.d.ts.map +1 -0
  63. package/dist/tools/add-relation-tool.js +163 -0
  64. package/dist/tools/add-relation-tool.js.map +1 -0
  65. package/dist/tools/extract-relations-tool.d.ts +28 -0
  66. package/dist/tools/extract-relations-tool.d.ts.map +1 -0
  67. package/dist/tools/extract-relations-tool.js +159 -0
  68. package/dist/tools/extract-relations-tool.js.map +1 -0
  69. package/dist/tools/get-relations-tool.d.ts +34 -0
  70. package/dist/tools/get-relations-tool.d.ts.map +1 -0
  71. package/dist/tools/get-relations-tool.js +155 -0
  72. package/dist/tools/get-relations-tool.js.map +1 -0
  73. package/dist/tools/index.d.ts +6 -1
  74. package/dist/tools/index.d.ts.map +1 -1
  75. package/dist/tools/index.js +16 -2
  76. package/dist/tools/index.js.map +1 -1
  77. package/dist/tools/remember-tool.d.ts +17 -0
  78. package/dist/tools/remember-tool.d.ts.map +1 -1
  79. package/dist/tools/remember-tool.js +195 -26
  80. package/dist/tools/remember-tool.js.map +1 -1
  81. package/dist/tools/remove-relation-tool.d.ts +45 -0
  82. package/dist/tools/remove-relation-tool.d.ts.map +1 -0
  83. package/dist/tools/remove-relation-tool.js +142 -0
  84. package/dist/tools/remove-relation-tool.js.map +1 -0
  85. package/dist/tools/search-local-tool.d.ts.map +1 -1
  86. package/dist/tools/search-local-tool.js +10 -3
  87. package/dist/tools/search-local-tool.js.map +1 -1
  88. package/dist/tools/types.d.ts +2 -0
  89. package/dist/tools/types.d.ts.map +1 -1
  90. package/dist/tools/types.js.map +1 -1
  91. package/dist/tools/visualize-relations-tool.d.ts +46 -0
  92. package/dist/tools/visualize-relations-tool.d.ts.map +1 -0
  93. package/dist/tools/visualize-relations-tool.js +157 -0
  94. package/dist/tools/visualize-relations-tool.js.map +1 -0
  95. package/dist/types/index.d.ts +8 -0
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/index.js +1 -0
  98. package/dist/types/index.js.map +1 -1
  99. package/dist/types/relation-graph.d.ts +215 -0
  100. package/dist/types/relation-graph.d.ts.map +1 -0
  101. package/dist/types/relation-graph.js +6 -0
  102. package/dist/types/relation-graph.js.map +1 -0
  103. package/dist/types/relation.d.ts +112 -0
  104. package/dist/types/relation.d.ts.map +1 -0
  105. package/dist/types/relation.js +67 -0
  106. package/dist/types/relation.js.map +1 -0
  107. package/dist/utils/cache-key-generator.d.ts +63 -0
  108. package/dist/utils/cache-key-generator.d.ts.map +1 -0
  109. package/dist/utils/cache-key-generator.js +76 -0
  110. package/dist/utils/cache-key-generator.js.map +1 -0
  111. package/dist/utils/database.d.ts.map +1 -1
  112. package/dist/utils/database.js +37 -17
  113. package/dist/utils/database.js.map +1 -1
  114. package/dist/utils/relation-visualizer.d.ts +81 -0
  115. package/dist/utils/relation-visualizer.d.ts.map +1 -0
  116. package/dist/utils/relation-visualizer.js +239 -0
  117. package/dist/utils/relation-visualizer.js.map +1 -0
  118. package/dist/utils/type-guards.d.ts +100 -0
  119. package/dist/utils/type-guards.d.ts.map +1 -0
  120. package/dist/utils/type-guards.js +144 -0
  121. package/dist/utils/type-guards.js.map +1 -0
  122. package/package.json +7 -2
  123. package/scripts/generate-relation-report.ts +481 -0
  124. package/scripts/weekly-relation-validation.ts +423 -0
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 관계 추출 품질 리포트 생성 스크립트
4
+ * PR 리뷰 시 "Relation Extraction Report" 자동 생성
5
+ *
6
+ * 사용법:
7
+ * npm run generate-relation-report
8
+ * npm run generate-relation-report -- --output report.md
9
+ * npm run generate-relation-report -- --method hybrid
10
+ * npm run generate-relation-report -- --sample 50
11
+ */
12
+
13
+ import { readFileSync, writeFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ import Database from 'better-sqlite3';
16
+ import { RelationExtractor } from '../src/services/relation-extractor.js';
17
+ import { RelationQualityValidator } from '../src/services/relation-quality-validator.js';
18
+ import type { ExpectedRelation, ExtractedRelation } from '../src/services/relation-quality-validator.js';
19
+ import { DatabaseUtils } from '../src/utils/database.js';
20
+ import { RelationEngineSchemaMigration } from '../src/database/migration/migrations/005-relation-engine-schema.js';
21
+ import type { MemoryItem } from '../src/types/index.js';
22
+
23
+ /**
24
+ * 명령줄 인자 파싱
25
+ */
26
+ interface CliOptions {
27
+ output?: string;
28
+ method?: 'rule' | 'llm' | 'hybrid';
29
+ sample?: number;
30
+ minConfidence?: number;
31
+ ci?: boolean;
32
+ allowSoftFail?: boolean;
33
+ }
34
+
35
+ function parseArgs(): CliOptions {
36
+ const args = process.argv.slice(2);
37
+ const options: CliOptions = {};
38
+
39
+ for (let i = 0; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === '--output' && args[i + 1]) {
42
+ options.output = args[i + 1];
43
+ i++;
44
+ } else if (arg === '--method' && args[i + 1]) {
45
+ const method = args[i + 1] as 'rule' | 'llm' | 'hybrid';
46
+ if (['rule', 'llm', 'hybrid'].includes(method)) {
47
+ options.method = method;
48
+ }
49
+ i++;
50
+ } else if (arg === '--sample' && args[i + 1]) {
51
+ options.sample = parseInt(args[i + 1], 10);
52
+ i++;
53
+ } else if (arg === '--min-confidence' && args[i + 1]) {
54
+ options.minConfidence = parseFloat(args[i + 1]);
55
+ i++;
56
+ } else if (arg === '--ci') {
57
+ options.ci = true;
58
+ } else if (arg === '--allow-soft-fail') {
59
+ options.allowSoftFail = true;
60
+ }
61
+ }
62
+
63
+ return options;
64
+ }
65
+
66
+ /**
67
+ * 테스트 데이터셋 로드
68
+ */
69
+ function loadTestDataset(sampleSize?: number): ExpectedRelation[] {
70
+ const testsetPath = join(process.cwd(), 'tests', 'fixtures', 'relation_testset.json');
71
+ const testsetContent = readFileSync(testsetPath, 'utf-8');
72
+ const testset = JSON.parse(testsetContent) as Array<{
73
+ source_id: string;
74
+ target_id: string;
75
+ expected_relation_type: string;
76
+ expected_confidence_range: [number, number];
77
+ source_content: string;
78
+ target_content: string;
79
+ }>;
80
+
81
+ const relations = testset.map(item => ({
82
+ source_id: item.source_id,
83
+ target_id: item.target_id,
84
+ expected_relation_type: item.expected_relation_type as any,
85
+ expected_confidence_range: item.expected_confidence_range,
86
+ source_content: item.source_content,
87
+ target_content: item.target_content
88
+ }));
89
+
90
+ // 샘플링이 요청된 경우 랜덤 샘플링
91
+ if (sampleSize && sampleSize < relations.length) {
92
+ const shuffled = [...relations].sort(() => Math.random() - 0.5);
93
+ return shuffled.slice(0, sampleSize);
94
+ }
95
+
96
+ return relations;
97
+ }
98
+
99
+ /**
100
+ * 테스트용 기본 스키마 생성
101
+ */
102
+ function createBaseSchema(db: Database.Database): void {
103
+ db.exec(`
104
+ CREATE TABLE IF NOT EXISTS memory_item (
105
+ id TEXT PRIMARY KEY,
106
+ type TEXT CHECK (type IN ('working','episodic','semantic','procedural')) NOT NULL,
107
+ content TEXT NOT NULL,
108
+ importance REAL CHECK (importance >= 0 AND importance <= 1) DEFAULT 0.5,
109
+ privacy_scope TEXT CHECK (privacy_scope IN ('private','team','public')) DEFAULT 'private',
110
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
111
+ last_accessed TIMESTAMP,
112
+ pinned BOOLEAN DEFAULT FALSE,
113
+ tags TEXT,
114
+ source TEXT,
115
+ view_count INTEGER DEFAULT 0,
116
+ cite_count INTEGER DEFAULT 0,
117
+ edit_count INTEGER DEFAULT 0
118
+ );
119
+ `);
120
+
121
+ db.exec(`
122
+ CREATE TABLE IF NOT EXISTS memento_schema_version (
123
+ version INTEGER PRIMARY KEY,
124
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
125
+ );
126
+ `);
127
+ }
128
+
129
+ /**
130
+ * 테스트용 메모리 생성
131
+ */
132
+ function createTestMemory(
133
+ db: Database.Database,
134
+ id: string,
135
+ content: string,
136
+ type: string = 'episodic'
137
+ ): void {
138
+ DatabaseUtils.run(db, `
139
+ INSERT INTO memory_item (id, type, content)
140
+ VALUES (?, ?, ?)
141
+ `, [id, type, content]);
142
+ }
143
+
144
+ /**
145
+ * 관계 추출 수행
146
+ */
147
+ async function extractRelations(
148
+ testDataset: ExpectedRelation[],
149
+ relationExtractor: RelationExtractor,
150
+ method: 'rule' | 'llm' | 'hybrid',
151
+ minConfidence: number
152
+ ): Promise<ExtractedRelation[]> {
153
+ const memoryMap = new Map<string, MemoryItem>();
154
+
155
+ // 모든 메모리 생성
156
+ for (const testCase of testDataset) {
157
+ if (!memoryMap.has(testCase.source_id)) {
158
+ memoryMap.set(testCase.source_id, {
159
+ id: testCase.source_id,
160
+ type: 'episodic',
161
+ content: testCase.source_content,
162
+ importance: 0.5,
163
+ privacy_scope: 'private',
164
+ created_at: new Date().toISOString()
165
+ });
166
+ }
167
+
168
+ if (!memoryMap.has(testCase.target_id)) {
169
+ memoryMap.set(testCase.target_id, {
170
+ id: testCase.target_id,
171
+ type: 'episodic',
172
+ content: testCase.target_content,
173
+ importance: 0.5,
174
+ privacy_scope: 'private',
175
+ created_at: new Date().toISOString()
176
+ });
177
+ }
178
+ }
179
+
180
+ // 소스 메모리별로 그룹화
181
+ const sourceGroups = new Map<string, ExpectedRelation[]>();
182
+ for (const testCase of testDataset) {
183
+ if (!sourceGroups.has(testCase.source_id)) {
184
+ sourceGroups.set(testCase.source_id, []);
185
+ }
186
+ sourceGroups.get(testCase.source_id)!.push(testCase);
187
+ }
188
+
189
+ // 관계 추출 수행
190
+ const extractedRelations: ExtractedRelation[] = [];
191
+
192
+ for (const [sourceId, testCases] of sourceGroups.entries()) {
193
+ const sourceMemory = memoryMap.get(sourceId)!;
194
+ const targetMemories = testCases.map(tc => memoryMap.get(tc.target_id)!).filter(Boolean);
195
+
196
+ if (targetMemories.length > 0) {
197
+ try {
198
+ const candidates = await relationExtractor.extractRelations(
199
+ sourceMemory,
200
+ targetMemories,
201
+ { method, minConfidence }
202
+ );
203
+
204
+ for (const candidate of candidates) {
205
+ extractedRelations.push({
206
+ source_id: candidate.source_id,
207
+ target_id: candidate.target_id,
208
+ relation_type: candidate.relation_type,
209
+ confidence: candidate.confidence
210
+ });
211
+ }
212
+ } catch (error) {
213
+ console.warn(`관계 추출 실패: ${sourceId}`, error);
214
+ }
215
+ }
216
+ }
217
+
218
+ return extractedRelations;
219
+ }
220
+
221
+ /**
222
+ * Markdown 리포트 생성
223
+ */
224
+ function generateMarkdownReport(
225
+ metrics: any,
226
+ options: CliOptions,
227
+ qualityValidator: RelationQualityValidator
228
+ ): string {
229
+ const timestamp = new Date().toISOString();
230
+ const method = options.method || 'hybrid';
231
+ const sampleSize = options.sample || '전체';
232
+
233
+ let report = `# Relation Extraction Quality Report\n\n`;
234
+ report += `**생성 일시**: ${timestamp}\n`;
235
+ report += `**추출 방법**: ${method}\n`;
236
+ report += `**샘플 크기**: ${sampleSize}\n`;
237
+ report += `**최소 신뢰도**: ${options.minConfidence || 0.5}\n\n`;
238
+ report += `---\n\n`;
239
+
240
+ // 전체 메트릭
241
+ report += `## 전체 메트릭\n\n`;
242
+ report += `| 메트릭 | 값 |\n`;
243
+ report += `|--------|-----|\n`;
244
+ report += `| **Precision** | ${(metrics.precision * 100).toFixed(2)}% |\n`;
245
+ report += `| **Recall** | ${(metrics.recall * 100).toFixed(2)}% |\n`;
246
+ report += `| **F1-Score** | ${(metrics.f1Score * 100).toFixed(2)}% |\n`;
247
+ report += `| **신뢰도 준수율** | ${(metrics.confidenceComplianceRate * 100).toFixed(2)}% |\n`;
248
+ report += `| **예상 관계 수** | ${metrics.totalExpected} |\n`;
249
+ report += `| **추출된 관계 수** | ${metrics.totalExtracted} |\n`;
250
+ report += `| **True Positives** | ${metrics.truePositives} |\n`;
251
+ report += `| **False Positives** | ${metrics.falsePositives} |\n`;
252
+ report += `| **False Negatives** | ${metrics.falseNegatives} |\n\n`;
253
+
254
+ // 관계 유형별 메트릭
255
+ report += `## 관계 유형별 메트릭\n\n`;
256
+ report += `| 관계 유형 | Precision | Recall | F1-Score | TP | FP | FN |\n`;
257
+ report += `|----------|-----------|--------|----------|----|----|----|\n`;
258
+
259
+ const relationTypes = ['CAUSES', 'DEPENDS_ON', 'FOLLOWS', 'CONTRASTS_WITH', 'REFERENCES', 'BELONGS_TO'] as const;
260
+ for (const type of relationTypes) {
261
+ const typeMetrics = metrics.typeMetrics[type];
262
+ if (typeMetrics) {
263
+ report += `| **${type}** | ${(typeMetrics.precision * 100).toFixed(2)}% | ${(typeMetrics.recall * 100).toFixed(2)}% | ${(typeMetrics.f1Score * 100).toFixed(2)}% | ${typeMetrics.truePositives} | ${typeMetrics.falsePositives} | ${typeMetrics.falseNegatives} |\n`;
264
+ } else {
265
+ report += `| **${type}** | - | - | - | - | - | - |\n`;
266
+ }
267
+ }
268
+ report += `\n`;
269
+
270
+ // 상세 분석이 있는 경우
271
+ if (metrics.typeAnalysis) {
272
+ report += `## 관계 유형별 상세 분석\n\n`;
273
+
274
+ for (const type of relationTypes) {
275
+ const analysis = metrics.typeAnalysis[type];
276
+ if (analysis) {
277
+ report += `### ${type}\n\n`;
278
+ report += `- **Precision**: ${(analysis.precision * 100).toFixed(2)}%\n`;
279
+ report += `- **Recall**: ${(analysis.recall * 100).toFixed(2)}%\n`;
280
+ report += `- **F1-Score**: ${(analysis.f1Score * 100).toFixed(2)}%\n`;
281
+ report += `- **평균 신뢰도**: ${analysis.averageConfidence.toFixed(3)}\n`;
282
+ report += `- **최소 신뢰도**: ${analysis.minConfidence.toFixed(3)}\n`;
283
+ report += `- **최대 신뢰도**: ${analysis.maxConfidence.toFixed(3)}\n`;
284
+ report += `- **신뢰도 표준편차**: ${analysis.confidenceStdDev.toFixed(3)}\n`;
285
+
286
+ if (analysis.mostConfusedWith !== null) {
287
+ report += `- **가장 많이 혼동되는 유형**: ${analysis.mostConfusedWith}\n`;
288
+ report += `- **혼동률**: ${(analysis.confusionRate * 100).toFixed(2)}%\n`;
289
+ }
290
+
291
+ report += `\n`;
292
+ }
293
+ }
294
+ }
295
+
296
+ // 혼동 행렬이 있는 경우
297
+ if (metrics.confusionMatrix) {
298
+ report += `## 혼동 행렬\n\n`;
299
+ report += `**전체 정확도**: ${(metrics.confusionMatrix.overallAccuracy * 100).toFixed(2)}%\n\n`;
300
+
301
+ report += `### 관계 유형별 정확도\n\n`;
302
+ report += `| 관계 유형 | 정확도 |\n`;
303
+ report += `|----------|--------|\n`;
304
+ for (const type of relationTypes) {
305
+ const accuracy = metrics.confusionMatrix.typeAccuracy[type];
306
+ if (accuracy !== undefined) {
307
+ report += `| **${type}** | ${(accuracy * 100).toFixed(2)}% |\n`;
308
+ }
309
+ }
310
+ report += `\n`;
311
+ }
312
+
313
+ // 임계값 검증
314
+ const thresholds = {
315
+ precision: 0.70,
316
+ recall: 0.65,
317
+ f1Score: 0.68
318
+ };
319
+
320
+ report += `## 임계값 검증\n\n`;
321
+ report += `| 메트릭 | 임계값 | 실제 값 | 상태 |\n`;
322
+ report += `|--------|--------|---------|------|\n`;
323
+ report += `| **Precision** | ${(thresholds.precision * 100).toFixed(2)}% | ${(metrics.precision * 100).toFixed(2)}% | ${metrics.precision >= thresholds.precision ? '✅ 통과' : '❌ 실패'} |\n`;
324
+ report += `| **Recall** | ${(thresholds.recall * 100).toFixed(2)}% | ${(metrics.recall * 100).toFixed(2)}% | ${metrics.recall >= thresholds.recall ? '✅ 통과' : '❌ 실패'} |\n`;
325
+ report += `| **F1-Score** | ${(thresholds.f1Score * 100).toFixed(2)}% | ${(metrics.f1Score * 100).toFixed(2)}% | ${metrics.f1Score >= thresholds.f1Score ? '✅ 통과' : '❌ 실패'} |\n\n`;
326
+
327
+ const failedMetrics: string[] = [];
328
+ if (metrics.precision < thresholds.precision) {
329
+ failedMetrics.push(`Precision: 예상 ${(thresholds.precision * 100).toFixed(2)}%, 실제 ${(metrics.precision * 100).toFixed(2)}%`);
330
+ }
331
+ if (metrics.recall < thresholds.recall) {
332
+ failedMetrics.push(`Recall: 예상 ${(thresholds.recall * 100).toFixed(2)}%, 실제 ${(metrics.recall * 100).toFixed(2)}%`);
333
+ }
334
+ if (metrics.f1Score < thresholds.f1Score) {
335
+ failedMetrics.push(`F1-Score: 예상 ${(thresholds.f1Score * 100).toFixed(2)}%, 실제 ${(metrics.f1Score * 100).toFixed(2)}%`);
336
+ }
337
+
338
+ if (failedMetrics.length > 0) {
339
+ report += `### 실패한 메트릭\n\n`;
340
+ for (const failure of failedMetrics) {
341
+ report += `- **${failure}**\n`;
342
+ }
343
+ report += `\n`;
344
+ }
345
+
346
+ report += `---\n\n`;
347
+ report += `*이 리포트는 자동으로 생성되었습니다.*\n`;
348
+
349
+ return report;
350
+ }
351
+
352
+ /**
353
+ * 메인 함수
354
+ */
355
+ async function main() {
356
+ const options = parseArgs();
357
+
358
+ console.log('📊 관계 추출 품질 리포트 생성 시작...\n');
359
+ console.log(`옵션:`, options);
360
+
361
+ // Given: 데이터베이스 및 서비스 초기화
362
+ const db = new Database(':memory:');
363
+ createBaseSchema(db);
364
+
365
+ const migration = new RelationEngineSchemaMigration();
366
+ migration.up(db);
367
+
368
+ const relationExtractor = new RelationExtractor();
369
+ const qualityValidator = new RelationQualityValidator();
370
+
371
+ // 테스트 데이터셋 로드
372
+ console.log('📂 테스트 데이터셋 로드 중...');
373
+ const testDataset = loadTestDataset(options.sample);
374
+ console.log(`✅ ${testDataset.length}건의 테스트 케이스 로드 완료\n`);
375
+
376
+ // 메모리 생성
377
+ console.log('💾 테스트 메모리 생성 중...');
378
+ for (const testCase of testDataset) {
379
+ if (!db.prepare('SELECT id FROM memory_item WHERE id = ?').get(testCase.source_id)) {
380
+ createTestMemory(db, testCase.source_id, testCase.source_content);
381
+ }
382
+ if (!db.prepare('SELECT id FROM memory_item WHERE id = ?').get(testCase.target_id)) {
383
+ createTestMemory(db, testCase.target_id, testCase.target_content);
384
+ }
385
+ }
386
+ console.log('✅ 메모리 생성 완료\n');
387
+
388
+ // 관계 추출
389
+ const method = options.method || 'hybrid';
390
+ const minConfidence = options.minConfidence || 0.5;
391
+ console.log(`🔍 관계 추출 수행 중... (방법: ${method}, 최소 신뢰도: ${minConfidence})`);
392
+ const extractedRelations = await extractRelations(
393
+ testDataset,
394
+ relationExtractor,
395
+ method,
396
+ minConfidence
397
+ );
398
+ console.log(`✅ ${extractedRelations.length}건의 관계 추출 완료\n`);
399
+
400
+ // 품질 메트릭 계산
401
+ console.log('📈 품질 메트릭 계산 중...');
402
+ const metrics = qualityValidator.calculateQualityMetricsWithAnalysis(
403
+ testDataset,
404
+ extractedRelations
405
+ );
406
+ console.log('✅ 메트릭 계산 완료\n');
407
+
408
+ // 리포트 생성
409
+ console.log('📝 리포트 생성 중...');
410
+ const report = generateMarkdownReport(metrics, options, qualityValidator);
411
+
412
+ // 리포트 출력 또는 파일 저장
413
+ if (options.output) {
414
+ writeFileSync(options.output, report, 'utf-8');
415
+ console.log(`✅ 리포트가 ${options.output}에 저장되었습니다.\n`);
416
+ } else {
417
+ console.log(report);
418
+ }
419
+
420
+ // 데이터베이스 정리
421
+ db.close();
422
+
423
+ // CI 모드: 임계값 검증 및 exit code 처리
424
+ if (options.ci) {
425
+ const thresholds = {
426
+ precision: 0.70,
427
+ recall: 0.65,
428
+ f1Score: 0.68
429
+ };
430
+
431
+ const failedMetrics: Array<{ metric: string; expected: number; actual: number }> = [];
432
+
433
+ if (metrics.precision < thresholds.precision) {
434
+ failedMetrics.push({
435
+ metric: 'Precision',
436
+ expected: thresholds.precision,
437
+ actual: metrics.precision
438
+ });
439
+ }
440
+ if (metrics.recall < thresholds.recall) {
441
+ failedMetrics.push({
442
+ metric: 'Recall',
443
+ expected: thresholds.recall,
444
+ actual: metrics.recall
445
+ });
446
+ }
447
+ if (metrics.f1Score < thresholds.f1Score) {
448
+ failedMetrics.push({
449
+ metric: 'F1-Score',
450
+ expected: thresholds.f1Score,
451
+ actual: metrics.f1Score
452
+ });
453
+ }
454
+
455
+ if (failedMetrics.length > 0) {
456
+ console.log('\n❌ 정확도 임계값 미달:\n');
457
+ for (const failure of failedMetrics) {
458
+ console.log(` - ${failure.metric}: 예상 ${(failure.expected * 100).toFixed(2)}%, 실제 ${(failure.actual * 100).toFixed(2)}%`);
459
+ }
460
+
461
+ if (options.allowSoftFail) {
462
+ console.log('\n⚠️ allow_soft_fail=true 옵션으로 인해 CI는 통과하지만 경고를 출력합니다.\n');
463
+ process.exit(0); // 경고만 출력하고 CI 통과
464
+ } else {
465
+ console.log('\n❌ CI 실패: 정확도 임계값을 충족하지 못했습니다.\n');
466
+ process.exit(1); // CI 실패
467
+ }
468
+ } else {
469
+ console.log('\n✅ 모든 정확도 임계값을 충족했습니다.\n');
470
+ process.exit(0);
471
+ }
472
+ } else {
473
+ console.log('✨ 리포트 생성 완료!');
474
+ }
475
+ }
476
+
477
+ // 스크립트 실행
478
+ main().catch(error => {
479
+ console.error('❌ 오류 발생:', error);
480
+ process.exit(1);
481
+ });