UNPKG

mcp-server-debug-thinking

Version:

Graph-based MCP server for systematic debugging using Problem-Solution Trees and Hypothesis-Experiment-Learning cycles

997 lines 41.9 kB
import { v4 as uuidv4 } from "uuid"; import { getAutoEdgeType, } from "../types/graphActions.js"; import { logger } from "../utils/logger.js"; import { GraphStorage } from "./GraphStorage.js"; import { createJsonResponse } from "../utils/format.js"; export class GraphService { graph; storage; errorTypeIndex = new Map(); ERROR_TYPE_REGEX = /\b(type\s*error|reference\s*error|syntax\s*error|range\s*error|eval\s*error|uri\s*error|typeerror|referenceerror|syntaxerror|rangeerror|evalerror|urierror)\b/i; // パフォーマンス最適化用インデックス nodesByType = new Map(); edgesByNode = new Map(); parentIndex = new Map(); // 子ID → 親ID constructor() { this.graph = { nodes: new Map(), edges: new Map(), roots: [], metadata: { createdAt: new Date(), lastModified: new Date(), sessionCount: 0, }, }; this.storage = new GraphStorage(); } async initialize() { try { await this.storage.initialize(); const loadedGraph = await this.storage.loadGraph(); if (loadedGraph) { this.graph = loadedGraph; logger.success(`Loaded graph with ${this.graph.nodes.size} nodes and ${this.graph.edges.size} edges`); } // 高速検索のためのインデックスを構築(新規・既存どちらの場合も必要) this.buildErrorTypeIndex(); this.buildPerformanceIndexes(); } catch (error) { logger.error("Failed to initialize GraphService:", error); throw error; } } /** * CREATEアクション実装 * 新しいノードを作成し、親ノードが指定されていれば自動的に適切なエッジを生成 * 問題ノードの場合は類似問題も自動検索して返す */ async create(action) { try { const nodeId = uuidv4(); const now = new Date(); // ベースノード構造を作成(全ノードタイプ共通の基本情報) const node = { id: nodeId, type: action.nodeType, content: action.content, metadata: { createdAt: now, updatedAt: now, tags: action.metadata?.tags || [], ...action.metadata, }, }; // ノードタイプに応じた必須メタデータを自動設定 // (例: 問題ノードにstatus、仮説ノードにconfidence等) this.enrichNodeMetadata(node); // ノードをグラフの内部Map構造に追加 this.graph.nodes.set(nodeId, node); // パフォーマンスインデックスを更新 this.updateIndexesForNewNode(node); // 親ノードとの関係を自動判定して適切なエッジを作成 // (例: 問題→仮説なら'hypothesizes'エッジ) let edgeId; if (action.parentId) { const parentNode = this.graph.nodes.get(action.parentId); if (!parentNode) { return createJsonResponse({ success: false, message: `Parent node ${action.parentId} not found`, }, true); } // 親ノードタイプと子ノードタイプから適切なエッジタイプを自動判定 const edgeType = getAutoEdgeType(parentNode.type, action.nodeType); if (edgeType) { const edge = this.createEdge(action.parentId, nodeId, edgeType); this.graph.edges.set(edge.id, edge); edgeId = edge.id; // パフォーマンスインデックスを更新 this.updateIndexesForNewEdge(edge); } } else if (action.nodeType === "problem") { // 親が指定されていない問題ノードはルート問題として登録 this.graph.roots.push(nodeId); node.metadata.isRoot = true; } // グラフ全体の最終更新日時を記録 this.graph.metadata.lastModified = now; // ノードとエッジを永続化ストレージに保存 await this.storage.saveNode(node); if (edgeId) { const edge = this.graph.edges.get(edgeId); if (edge) { await this.storage.saveEdge(edge); } } // グラフメタデータも更新して保存 await this.storage.saveGraphMetadata(this.graph); // 問題ノードの場合、エラータイプ別インデックスを更新 // (例: 'TypeError'などを抽出して分類) if (action.nodeType === "problem") { const errorType = this.extractErrorType(action.content) || "other"; if (!this.errorTypeIndex.has(errorType)) { this.errorTypeIndex.set(errorType, new Set()); } this.errorTypeIndex.get(errorType)?.add(nodeId); logger.debug(`Added node ${nodeId} to error type index: ${errorType}`); } // ノードタイプに応じた次のアクション提案を生成 const suggestions = await this.generateSuggestions(node); // 問題ノードの場合、過去の類似問題とその解決策を自動検索 let similarProblems; if (action.nodeType === "problem") { const similarResult = await this.findSimilarProblems({ pattern: action.content, limit: 5, minSimilarity: 0.2, }); // 類似問題が見つかった場合のみレスポンスに含める if (similarResult.problems.length > 0) { similarProblems = similarResult.problems.map((p) => ({ nodeId: p.nodeId, content: p.content, similarity: p.similarity, status: p.status, solutions: p.solutions || [], })); } } const response = { success: true, nodeId, edgeId, message: `Created ${action.nodeType} node`, suggestions, similarProblems, }; return createJsonResponse(response); } catch (error) { logger.error("Error in create action:", error); return createJsonResponse({ success: false, message: error instanceof Error ? error.message : "Unknown error", }, true); } } /** * CONNECTアクション実装 * 既存の2つのノード間に明示的な関係(エッジ)を作成 * 矛盾する関係(supports vs contradicts等)を自動検出 */ async connect(action) { try { const fromNode = this.graph.nodes.get(action.from); const toNode = this.graph.nodes.get(action.to); if (!fromNode || !toNode) { return createJsonResponse({ success: false, message: `Node(s) not found: ${!fromNode ? action.from : ""} ${!toNode ? action.to : ""}`, }, true); } // 既存の関係と矛盾するエッジがないかチェック // (例: 同じノード間に'supports'と'contradicts'が共存しないように) const conflicts = this.checkForConflicts(action); // 指定されたパラメータでエッジを作成 // strengthは0-1の範囲で関係の強さを表現 const edge = this.createEdge(action.from, action.to, action.type, action.strength, action.metadata); this.graph.edges.set(edge.id, edge); // パフォーマンスインデックスを更新 this.updateIndexesForNewEdge(edge); // グラフ全体の最終更新日時を記録 this.graph.metadata.lastModified = new Date(); // ノードとエッジを永続化ストレージに保存 await this.storage.saveEdge(edge); // グラフメタデータも更新して保存 await this.storage.saveGraphMetadata(this.graph); const response = { success: true, edgeId: edge.id, message: `Connected ${fromNode.type} to ${toNode.type} with ${action.type}`, conflicts: conflicts.length > 0 ? { conflictingEdges: conflicts.map((e) => e.id), explanation: "This connection may contradict existing relationships", } : undefined, }; return createJsonResponse(response); } catch (error) { logger.error("Error in connect action:", error); return createJsonResponse({ success: false, message: error instanceof Error ? error.message : "Unknown error", }, true); } } /** * QUERYアクション実装 * グラフ内のデータを様々な方法で検索・分析 * 実行時間を計測してパフォーマンス情報も返す */ async query(action) { const startTime = Date.now(); try { let results; switch (action.type) { case "similar-problems": results = await this.findSimilarProblems(action.parameters || {}); break; case "recent-activity": results = await this.getRecentActivity(action.parameters); break; default: return createJsonResponse({ success: false, message: `Unknown query type: ${action.type}`, }, true); } const response = { success: true, results, queryTime: Date.now() - startTime, }; return createJsonResponse(response); } catch (error) { logger.error("Error in query action:", error); return createJsonResponse({ success: false, message: error instanceof Error ? error.message : "Unknown error", queryTime: Date.now() - startTime, }, true); } } /** * ヘルパーメソッド * 内部処理用のユーティリティ関数群 */ /** * ノードタイプに応じたデフォルトメタデータを自動設定 * - 問題ノード: status='open', isRoot=false * - 仮説ノード: confidence=50(未設定時), testable=true * - 学習ノード: confidence=70(未設定時) */ enrichNodeMetadata(node) { switch (node.type) { case "problem": if (!node.metadata.status) { node.metadata.status = "open"; } node.metadata.isRoot = false; break; case "hypothesis": if (!node.metadata.confidence) { node.metadata.confidence = 50; } node.metadata.testable = true; break; case "learning": if (!node.metadata.confidence) { node.metadata.confidence = 70; } break; } } /** * エッジオブジェクトを作成 * strengthを自動的に0-1の範囲に正規化 * ユニークIDとタイムスタンプを自動付与 */ createEdge(from, to, type, strength = 1, metadata) { return { id: uuidv4(), type, from, to, strength: Math.max(0, Math.min(1, strength)), metadata: { createdAt: new Date(), ...metadata, }, }; } /** * 関係の矛盾をチェック * 'supports'と'contradicts'のような相反する関係が * 同じノード間に存在しないよう検証 * @returns 矛盾するエッジの配列 */ checkForConflicts(action) { const conflicts = []; // 'contradicts'エッジを作成しようとした場合、 // 同じノード間に既存の'supports'エッジがないか確認 if (action.type === "contradicts") { // 同一方向の'supports'関係を検索 for (const edge of this.graph.edges.values()) { if (edge.type === "supports" && edge.from === action.from && edge.to === action.to) { conflicts.push(edge); } } } else if (action.type === "supports") { // 同一方向の'contradicts'関係を検索 for (const edge of this.graph.edges.values()) { if (edge.type === "contradicts" && edge.from === action.from && edge.to === action.to) { conflicts.push(edge); } } } return conflicts; } /** * ノードタイプに応じた次のアクション提案を生成 * - 問題ノード: 関連する問題のIDリスト * - 仮説ノード: 推奨される実験方法 */ async generateSuggestions(node) { const suggestions = {}; if (node.type === "problem") { // 関連する問題を最大3件まで検索 const similar = await this.findSimilarProblems({ pattern: node.content, limit: 3, }); if (similar.problems.length > 0) { suggestions.relatedProblems = similar.problems.map((p) => p.nodeId); } } else if (node.type === "hypothesis") { // 仮説を検証するための標準的な実験手法を提案 suggestions.recommendedExperiments = [ "Test the hypothesis in isolation", "Create a minimal reproducible example", "Check assumptions with logging", ]; } return suggestions; } /** * クエリ実装メソッド群 * 様々な検索・分析機能の実装 */ /** * 類似問題検索 * エラータイプ別インデックスを活用して高速に類似問題を検索 * 類似度計算はエラータイプ、キーフレーズ、単語ベースで実施 * 解決済み問題を優先的に返す */ async findSimilarProblems(params) { const problems = []; const pattern = params?.pattern || ""; const minSimilarity = params?.minSimilarity !== undefined ? params.minSimilarity : 0.2; const errorType = this.extractErrorType(pattern); // エラータイプ別インデックスを使用して検索対象を効率的に絞り込む let candidateNodeIds; if (errorType && this.errorTypeIndex.has(errorType)) { // 同一エラータイプの問題ノードのみを検索対象に const errorSet = this.errorTypeIndex.get(errorType); if (errorSet) { candidateNodeIds = errorSet; logger.debug(`Searching ${candidateNodeIds.size} nodes with error type: ${errorType}`); } else { candidateNodeIds = new Set(); } } else { // エラータイプが抽出できない場合は全問題ノードを検索対象に candidateNodeIds = new Set(); for (const [nodeId, node] of this.graph.nodes) { if (node.type === "problem") { candidateNodeIds.add(nodeId); } } logger.debug(`Searching all ${candidateNodeIds.size} problem nodes (no specific error type)`); } // 絞り込まれた候補に対して類似度計算を実施 for (const nodeId of candidateNodeIds) { const node = this.graph.nodes.get(nodeId); if (!node || node.type !== "problem") continue; const similarity = this.calculateSimilarity(pattern, node.content); // 最小類似度以上の問題のみを結果に含める if (similarity >= minSimilarity) { const solutions = this.findSolutionsForProblem(node.id); const nodeErrorType = this.extractErrorType(node.content); problems.push({ nodeId: node.id, content: node.content, similarity, errorType: nodeErrorType || undefined, status: node.metadata.status || "open", solutions, }); } } // 結果をソート: 1.解決済み問題を優先 2.類似度の高い順 problems.sort((a, b) => { // 解決済み(solved)の問題を優先表示 if (a.status === "solved" && b.status !== "solved") return -1; if (b.status === "solved" && a.status !== "solved") return 1; // ステータスが同じ場合は類似度の降順でソート return b.similarity - a.similarity; }); return { problems: problems.slice(0, params?.limit || 10), }; } /** * 特定の問題に対する解決策を検索 * 'solves'エッジを追跡して関連する解決策ノードを収集 * デバッグパス(問題→仮説→実験→観察→解決)も含める * @param problemId 問題ノードのID * @returns 解決策情報の配列(検証済みフラグとデバッグパス付き) */ findSolutionsForProblem(problemId) { const solutions = []; for (const edge of this.graph.edges.values()) { if (edge.type === "solves" && edge.to === problemId) { const solutionNode = this.graph.nodes.get(edge.from); if (solutionNode && solutionNode.type === "solution") { // 解決策から問題までのデバッグパスを構築 const debugPath = this.buildDebugPath(problemId, solutionNode.id); solutions.push({ nodeId: solutionNode.id, content: solutionNode.content, verified: solutionNode.metadata.verified, debugPath: debugPath.map((node) => ({ nodeId: node.id, type: node.type, content: node.content, })), }); } } } return solutions; } /** * 問題から解決策までのデバッグパスを構築 * 親子関係を辿って経路を復元 */ buildDebugPath(problemId, solutionId) { const path = []; const solutionNode = this.graph.nodes.get(solutionId); if (!solutionNode) return []; // 解決策ノードから親を辿る let currentNode = solutionNode; const visited = new Set(); while (currentNode && !visited.has(currentNode.id)) { path.unshift(currentNode); visited.add(currentNode.id); if (currentNode.id === problemId) { break; } // 親ノードをインデックスから効率的に取得 const parentId = this.parentIndex.get(currentNode.id); if (parentId) { currentNode = this.graph.nodes.get(parentId); } else { // 親子関係インデックスにない場合は受信エッジから親を探す const nodeEdges = this.edgesByNode.get(currentNode.id); if (nodeEdges && nodeEdges.incoming.length > 0) { const parentEdge = nodeEdges.incoming[0]; currentNode = this.graph.nodes.get(parentEdge.from); } else { currentNode = undefined; } } } // 問題ノードがパスに含まれていない場合は追加 const problemNode = this.graph.nodes.get(problemId); if (problemNode && path.length > 0 && path[0].id !== problemId) { path.unshift(problemNode); } return path; } /** * エラーメッセージからエラータイプを抽出 * 正規表現で'TypeError', 'ReferenceError'等を検出 * @returns エラータイプ名(小文字、スペース区切り)またはnull */ extractErrorType(content) { const match = content.toLowerCase().match(this.ERROR_TYPE_REGEX); if (!match) return null; // "typeerror" -> "type error", "referenceerror" -> "reference error" に正規化 const errorType = match[0].toLowerCase(); if (errorType.includes(" ")) { return errorType; // すでにスペースがある場合はそのまま } // スペースなしのエラータイプを分割 const normalized = errorType.replace(/(type|reference|syntax|range|eval|uri)error/, "$1 error"); return normalized; } /** * 2つの問題テキストの類似度を計算(0-1の範囲) * 計算要素: * - エラータイプの類似度: 20% * - 部分文字列マッチング: 20% * - 編集距離による類似度: 15% * - キーフレーズの部分一致: 15% * - 単語ベースの類似度: 20% * - 重要な識別子の一致: 10% */ calculateSimilarity(pattern, content) { const p1 = pattern.toLowerCase(); const p2 = content.toLowerCase(); // 両方のテキストからエラータイプを抽出して比較準備 const errorType1 = this.extractErrorType(pattern); const errorType2 = this.extractErrorType(content); let score = 0; // スコア計算: エラータイプの類似度(20%の重み) if (errorType1 && errorType2) { if (errorType1 === errorType2) { score += 0.2; } else { // 関連するエラータイプのグループ化 const errorGroups = [ ["typeerror", "type error", "type mismatch"], ["referenceerror", "reference error", "not defined"], ["syntaxerror", "syntax error", "invalid syntax"], ["rangeerror", "range error", "out of range"], ]; let groupMatch = false; for (const group of errorGroups) { if (group.includes(errorType1) && group.includes(errorType2)) { groupMatch = true; break; } } score += groupMatch ? 0.12 : 0.04; } } else if (errorType1 || errorType2) { // 片方だけエラータイプがある場合も部分点 score += 0.04; } // スコア計算: 部分文字列マッチング(20%の重み) // 長いテキストの場合はスキップしてパフォーマンスを優先 if (p1.length < 200 && p2.length < 200) { const longestCommonSubstring = this.findLongestCommonSubstring(p1, p2); const avgLength = (p1.length + p2.length) / 2; if (longestCommonSubstring.length > 5) { // 5文字以上の共通部分文字列 score += Math.min(0.2, (longestCommonSubstring.length / avgLength) * 0.4); } } else { // 長いテキストの場合は単語の共通割合で代替 const words1 = new Set(p1.split(/\s+/)); const words2 = new Set(p2.split(/\s+/)); const commonWords = [...words1].filter(w => words2.has(w)).length; const totalUniqueWords = new Set([...words1, ...words2]).size; if (totalUniqueWords > 0) { score += (commonWords / totalUniqueWords) * 0.2; } } // スコア計算: 編集距離による類似度(15%の重み) // 短い文字列の場合は文字レベル、長い文字列の場合は単語レベルで計算 if (p1.length < 50 && p2.length < 50) { const charSimilarity = this.calculateLevenshteinSimilarity(p1, p2); score += charSimilarity * 0.15; } else { const wordSimilarity = this.calculateWordLevelSimilarity(pattern, content); score += wordSimilarity * 0.15; } // 高頻度エラーフレーズのパターン定義(部分一致も評価) const keyPhrases = [ /cannot\s+read\s+property/i, /cannot\s+access/i, /is\s+not\s+defined/i, /is\s+not\s+a\s+function/i, /undefined\s+or\s+null/i, /maximum\s+call\s+stack/i, /out\s+of\s+memory/i, /permission\s+denied/i, /failed\s+to/i, /unable\s+to/i, /expected.*but.*got/i, /missing\s+required/i, ]; // スコア計算: キーフレーズのファジーマッチング(15%の重み) let phraseScore = 0; for (const phrase of keyPhrases) { const match1 = phrase.test(p1); phrase.lastIndex = 0; const match2 = phrase.test(p2); phrase.lastIndex = 0; if (match1 && match2) { phraseScore += 1.0; } else if (match1 || match2) { // 片方だけマッチした場合、もう片方に類似表現があるかチェック const phraseStr = phrase.source.replace(/\\s\+/g, " "); const keywords = phraseStr.split(/\s+/).filter((k) => k.length > 3 && !k.includes("\\")); let partialMatch = 0; for (const keyword of keywords) { if (p1.includes(keyword) && p2.includes(keyword)) { partialMatch++; } } if (keywords.length > 0) { phraseScore += (partialMatch / keywords.length) * 0.5; } } } if (keyPhrases.length > 0) { score += (phraseScore / keyPhrases.length) * 0.15; } // スコア計算: 単語ベースの類似度(20%の重み) // トークン化を改善: 記号で分割、3文字以上の意味のある単語を抽出 const tokenize = (text) => { return text .split(/[\s\-_./\\:;,()[\]{}<>'"]+/) .filter((w) => w.length >= 3 && !/^\d+$/.test(w)); // 数字のみは除外 }; const words1 = tokenize(p1); const words2 = tokenize(p2); if (words1.length > 0 && words2.length > 0) { const commonWords = new Set(); const partialMatches = new Set(); for (const w1 of words1) { for (const w2 of words2) { if (w1 === w2) { commonWords.add(w1); } else if (w1.includes(w2) || w2.includes(w1)) { partialMatches.add(w1.length < w2.length ? w1 : w2); } } } const exactMatchScore = commonWords.size / Math.max(words1.length, words2.length); const partialMatchScore = partialMatches.size / Math.max(words1.length, words2.length); score += exactMatchScore * 0.15 + partialMatchScore * 0.05; } // スコア計算: 重要な識別子の一致(10%の重み) // ファイル名、関数名、変数名などを抽出 const identifierPattern = /['"`]([^'"`]+)['"`]|(\w+)\(/g; const extractIdentifiers = (text) => { const identifiers = new Set(); let match = identifierPattern.exec(text); while (match !== null) { const identifier = match[1] || match[2]; if (identifier) { identifiers.add(identifier.toLowerCase()); } match = identifierPattern.exec(text); } identifierPattern.lastIndex = 0; return identifiers; }; const ids1 = extractIdentifiers(pattern); const ids2 = extractIdentifiers(content); if (ids1.size > 0 && ids2.size > 0) { const commonIds = new Set([...ids1].filter((id) => ids2.has(id))); score += (commonIds.size / Math.max(ids1.size, ids2.size)) * 0.1; } // NaN や負の値を防ぐ if (Number.isNaN(score) || score < 0) { return 0; } return Math.min(score, 1.0); } /** * 最長共通部分文字列を見つける * 動的計画法を使用して効率的に計算 */ findLongestCommonSubstring(s1, s2) { if (!s1 || !s2) return ""; const m = s1.length; const n = s2.length; let maxLength = 0; let endPos = 0; // DPテーブル(メモリ効率のため1次元配列を使用) let prev = new Array(n + 1).fill(0); let curr = new Array(n + 1).fill(0); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (s1[i - 1] === s2[j - 1]) { curr[j] = prev[j - 1] + 1; if (curr[j] > maxLength) { maxLength = curr[j]; endPos = i; } } else { curr[j] = 0; } } // 配列をスワップ [prev, curr] = [curr, prev]; curr.fill(0); } return maxLength > 0 ? s1.substring(endPos - maxLength, endPos) : ""; } /** * レーベンシュタイン距離(編集距離)を計算 * 一方の文字列を他方に変換するために必要な最小の編集操作数 * @returns 正規化された類似度スコア (0-1の範囲、1が完全一致) */ calculateLevenshteinSimilarity(s1, s2) { if (!s1 && !s2) return 1; if (!s1 || !s2) return 0; const m = s1.length; const n = s2.length; // 空間効率のため、2行のみを使用 let prev = new Array(n + 1); let curr = new Array(n + 1); // 初期化 for (let j = 0; j <= n; j++) { prev[j] = j; } for (let i = 1; i <= m; i++) { curr[0] = i; for (let j = 1; j <= n; j++) { const cost = s1[i - 1] === s2[j - 1] ? 0 : 1; curr[j] = Math.min(prev[j] + 1, // 削除 curr[j - 1] + 1, // 挿入 prev[j - 1] + cost // 置換 ); } // 配列をスワップ [prev, curr] = [curr, prev]; } const distance = prev[n]; const maxLength = Math.max(m, n); // 距離を類似度に変換 (0-1の範囲) return maxLength > 0 ? 1 - distance / maxLength : 0; } /** * 単語レベルでのレーベンシュタイン距離を計算 * エラーメッセージの構造的な類似性を評価 */ calculateWordLevelSimilarity(s1, s2) { const words1 = s1 .toLowerCase() .split(/\s+/) .filter((w) => w.length > 0); const words2 = s2 .toLowerCase() .split(/\s+/) .filter((w) => w.length > 0); if (words1.length === 0 && words2.length === 0) return 1; if (words1.length === 0 || words2.length === 0) return 0; const m = words1.length; const n = words2.length; const dp = Array(m + 1) .fill(null) .map(() => Array(n + 1).fill(0)); // 初期化 for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; // 単語レベルでの編集距離を計算 for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (words1[i - 1] === words2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { // 単語が完全に一致しない場合は編集コスト1とする(パフォーマンスのため) dp[i][j] = Math.min(dp[i - 1][j] + 1, // 削除 dp[i][j - 1] + 1, // 挿入 dp[i - 1][j - 1] + 1 // 置換 ); } } } const distance = dp[m][n]; const maxLength = Math.max(m, n); return maxLength > 0 ? 1 - distance / maxLength : 0; } /** * セッション管理用パブリックメソッド */ /** * グラフメタデータをストレージに保存 * シャットダウン時などに呼び出される */ async saveGraph() { await this.storage.saveGraphMetadata(this.graph); } /** * 最近のアクティビティを取得 * ノードを作成時刻の降順でソートして返す */ async getRecentActivity(params = {}) { const limit = params?.limit; // limit が undefined の場合はデフォルト値を使用 const effectiveLimit = limit === undefined ? 10 : limit; // limit が 0 の場合は空の結果を返す if (effectiveLimit === 0) { return { nodes: [], totalNodes: this.graph.nodes.size, }; } // すべてのノードを配列に変換して時刻でソート // 同じタイムスタンプの場合はIDで安定ソート const sortedNodes = Array.from(this.graph.nodes.values()) .sort((a, b) => { const timeDiff = b.metadata.createdAt.getTime() - a.metadata.createdAt.getTime(); if (timeDiff !== 0) return timeDiff; // タイムスタンプが同じ場合はIDの辞書順(UUIDなので作成順に近い) return b.id.localeCompare(a.id); }) .slice(0, effectiveLimit); const result = { nodes: sortedNodes.map((node) => { // インデックスを使ってこのノードに関連するエッジを効率的に取得 const nodeEdges = this.edgesByNode.get(node.id) || { incoming: [], outgoing: [] }; const edges = []; // 送信エッジ for (const edge of nodeEdges.outgoing) { edges.push({ type: edge.type, targetNodeId: edge.to, direction: "from", }); } // 受信エッジ for (const edge of nodeEdges.incoming) { edges.push({ type: edge.type, targetNodeId: edge.from, direction: "to", }); } // 親ノードをインデックスから取得 let parent; const parentId = this.parentIndex.get(node.id); if (parentId) { const parentNode = this.graph.nodes.get(parentId); if (parentNode) { parent = { nodeId: parentNode.id, type: parentNode.type, content: parentNode.content, }; } } return { nodeId: node.id, type: node.type, content: node.content, createdAt: node.metadata.createdAt.toISOString(), parent, edges, }; }), totalNodes: this.graph.nodes.size, }; return result; } /** * エラータイプ別インデックスの構築 * すべての問題ノードをスキャンしてエラータイプ別に分類 * エラータイプが不明な場合は'other'カテゴリに分類 */ buildErrorTypeIndex() { this.errorTypeIndex.clear(); for (const [nodeId, node] of this.graph.nodes) { if (node.type === "problem") { const errorType = this.extractErrorType(node.content); if (errorType) { if (!this.errorTypeIndex.has(errorType)) { this.errorTypeIndex.set(errorType, new Set()); } const errorTypeSet = this.errorTypeIndex.get(errorType); if (errorTypeSet) { errorTypeSet.add(nodeId); } } else { // エラータイプが不明な場合は "other" に分類 if (!this.errorTypeIndex.has("other")) { this.errorTypeIndex.set("other", new Set()); } const otherSet = this.errorTypeIndex.get("other"); if (otherSet) { otherSet.add(nodeId); } } } } logger.info(`Error type index built: ${this.errorTypeIndex.size} types, ${this.graph.nodes.size} total nodes`); } /** * パフォーマンス最適化用インデックスを構築 * ノードタイプ別、エッジ関係、親子関係のインデックスを作成 */ buildPerformanceIndexes() { // インデックスをクリア this.nodesByType.clear(); this.edgesByNode.clear(); this.parentIndex.clear(); // ノードタイプ別インデックスを構築 for (const [nodeId, node] of this.graph.nodes) { if (!this.nodesByType.has(node.type)) { this.nodesByType.set(node.type, new Set()); } const nodeTypeSet = this.nodesByType.get(node.type); if (nodeTypeSet) { nodeTypeSet.add(nodeId); } } // エッジ関係インデックスを構築 for (const [nodeId] of this.graph.nodes) { this.edgesByNode.set(nodeId, { incoming: [], outgoing: [] }); } for (const edge of this.graph.edges.values()) { // 送信元ノードの送信エッジ const fromEdges = this.edgesByNode.get(edge.from); if (fromEdges) { fromEdges.outgoing.push(edge); } // 宛先ノードの受信エッジ const toEdges = this.edgesByNode.get(edge.to); if (toEdges) { toEdges.incoming.push(edge); } // 親子関係インデックス(自動作成されたエッジから親子関係を抽出) const parentChildTypes = ["decomposes", "hypothesizes", "tests", "produces", "learns"]; if (parentChildTypes.includes(edge.type)) { this.parentIndex.set(edge.to, edge.from); } } logger.info(`Performance indexes built: ${this.nodesByType.size} node types, ${this.edgesByNode.size} nodes indexed`); } /** * ノードが追加された時のインデックス更新 */ updateIndexesForNewNode(node) { // ノードタイプ別インデックスを更新 if (!this.nodesByType.has(node.type)) { this.nodesByType.set(node.type, new Set()); } const nodeTypeSet = this.nodesByType.get(node.type); if (nodeTypeSet) { nodeTypeSet.add(node.id); } // エッジ関係インデックスを初期化 this.edgesByNode.set(node.id, { incoming: [], outgoing: [] }); } /** * エッジが追加された時のインデックス更新 */ updateIndexesForNewEdge(edge) { // エッジ関係インデックスを更新 const fromEdges = this.edgesByNode.get(edge.from); if (fromEdges) { fromEdges.outgoing.push(edge); } const toEdges = this.edgesByNode.get(edge.to); if (toEdges) { toEdges.incoming.push(edge); } // 親子関係インデックスを更新 const parentChildTypes = ["decomposes", "hypothesizes", "tests", "produces", "learns", "solves"]; if (parentChildTypes.includes(edge.type)) { this.parentIndex.set(edge.to, edge.from); } } getGraph() { return this.graph; } } //# sourceMappingURL=GraphService.js.map