UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

469 lines (468 loc) 19.2 kB
/** * EvidenceCollector.js — 从 Analyst 工具调用中收集结构化证据 * * Bootstrap 质量门控核心组件: 将 Analyst 阶段的 toolCall 序列转化为 * 类型化的证据地图、探索日志和负空间信号,供 Producer 阶段直接引用。 * * 被 bootstrap-gate.js (buildAnalysisArtifact) 调用。 * * 设计原则: * - 不保留原始工具返回值 (体积过大) * - 按工具类型萃取关键信息 (代码片段、搜索命中、类结构) * - 记录负空间: 搜索但未找到的模式 → 告知 Producer "这不存在" * - 预算控制: 代码片段总量 ≤ 32KB (Layer 2 Detail) * * @module EvidenceCollector */ // ── 常量 ────────────────────────────────────────────────────────── /** 单个代码片段最大行数 */ const MAX_SNIPPET_LINES = 30; /** 每个文件最多保留的代码片段数 */ const MAX_SNIPPETS_PER_FILE = 3; /** 每个搜索模式最多保留的匹配条目 */ const MAX_SEARCH_MATCHES = 5; /** 默认代码片段总字符预算 */ const DEFAULT_SNIPPET_BUDGET = 32_000; // ── 主类 ────────────────────────────────────────────────────────── export class EvidenceCollector { /** 文件 → 证据条目 */ #evidenceMap = new Map(); /** 探索日志 */ #explorationLog = []; /** 负空间信号 */ #negativeSignals = []; /** 代码片段总字符预算 */ #snippetBudget; /** 当前已使用的片段字符数 */ #snippetCharsUsed = 0; /** @param [options.snippetBudget=32000] 代码片段总字符预算 */ constructor(options = {}) { this.#snippetBudget = options.snippetBudget ?? DEFAULT_SNIPPET_BUDGET; } // ─── 公开 API ────────────────────────────────────────── /** * 处理单个工具调用,提取证据 * * @param toolCall { tool/name, params/args, result } * @param [round=0] 调用序号 */ processToolCall(toolCall, round = 0) { const tool = toolCall.tool || toolCall.name; const args = toolCall.params || toolCall.args || {}; const result = toolCall.result; const hasResult = result != null && result !== ''; // 按工具类型提取证据 if (hasResult) { try { switch (tool) { case 'read_project_file': this.#extractFileEvidence(args, result); break; case 'search_project_code': case 'semantic_search_code': this.#extractSearchEvidence(args, result); break; case 'get_class_info': this.#extractClassEvidence(args, result); break; case 'get_protocol_info': this.#extractProtocolEvidence(args, result); break; case 'get_file_summary': this.#extractFileSummary(args, result); break; // note_finding → WorkingMemory 已处理,不在此重复采集 // get_project_overview / list_project_structure → 仅入日志 } } catch { // 证据提取失败不影响整体流程,仅记入探索日志 } } // 所有工具调用都记入探索日志 this.#explorationLog.push({ round, tool: tool, intent: this.#inferIntent(tool, args), resultSummary: this.#summarizeResult(tool, result), effective: hasResult && this.#isEffective(tool, result), }); } /** * 构建收集结果 * * @returns {{ * evidenceMap: Map<string, EvidenceEntry>, * explorationLog: ExplorationEntry[], * negativeSignals: NegativeSignal[] * }} */ build() { return { evidenceMap: this.#evidenceMap, explorationLog: this.#explorationLog, negativeSignals: this.#negativeSignals, }; } // ─── 工具特化提取 ───────────────────────────────────── /** * read_project_file — 提取代码片段 * 支持批量读取 (result.files) 和单文件读取 (result.content) */ #extractFileEvidence(args, result) { // 字符串结果 — 可能是错误消息或直接内容 if (typeof result === 'string') { if (this.#isErrorString(result)) { return; } const filePath = args.filePath; if (filePath) { this.#addCodeSnippet(filePath, result, args.startLine || 1); } return; } if (!result || typeof result !== 'object') { return; } // 批量读取: result.files 数组 if (Array.isArray(result.files)) { for (const f of result.files) { const filePath = f.path || f.filePath; if (filePath && f.content) { this.#addCodeSnippet(filePath, f.content, f.startLine || 1); } } return; } // 单文件: result.content const filePath = result.path || result.filePath || args.filePath; if (filePath && result.content) { this.#addCodeSnippet(filePath, result.content, result.startLine || args.startLine || 1); } } /** * search_project_code / semantic_search_code — 提取匹配 + 负空间信号 * 支持批量搜索 (result.batchResults) 和单模式搜索 (result.matches) */ #extractSearchEvidence(args, result) { const patterns = this.#extractSearchPatterns(args); if (typeof result === 'string') { if (this.#isErrorString(result) || result.length < 10) { for (const p of patterns) { this.#addNegativeSignal(p); } } return; } if (!result || typeof result !== 'object') { return; } const matches = result.matches || []; const batchResults = result.batchResults || {}; // 批量搜索 if (Object.keys(batchResults).length > 0) { for (const [pattern, sub] of Object.entries(batchResults)) { const subMatches = sub.matches || []; if (subMatches.length === 0) { this.#addNegativeSignal(pattern); } else { for (const m of subMatches.slice(0, MAX_SEARCH_MATCHES)) { this.#addSearchMatch(m, pattern); } } } return; } // 单模式搜索 if (matches.length === 0) { for (const p of patterns) { this.#addNegativeSignal(p); } } else { const searchNote = patterns[0] || '?'; for (const m of matches.slice(0, MAX_SEARCH_MATCHES)) { this.#addSearchMatch(m, searchNote); } } } /** get_class_info — 提取类结构 → evidenceMap */ #extractClassEvidence(args, result) { if (typeof result !== 'object' || !result) { return; } const className = result.className || args.className; const filePath = result.filePath; if (!filePath) { return; } const entry = this.#getOrCreateEntry(filePath); entry.role = entry.role || 'class-definition'; const parts = [`Class: ${className}`]; if (result.superClass) { parts.push(`Extends: ${result.superClass}`); } if (result.protocols?.length) { parts.push(`Implements: ${result.protocols.join(', ')}`); } if (result.methods?.length) { const names = result.methods .slice(0, 5) .map((m) => (typeof m === 'string' ? m : m.name || m.selector || '?')); parts.push(`Methods(${result.methods.length}): ${names.join(', ')}`); } if (result.properties?.length) { parts.push(`Props: ${result.properties.length}`); } const classSummary = parts.join(' | '); entry.summary = entry.summary ? `${entry.summary}; ${classSummary}` : classSummary; } /** get_protocol_info — 提取协议结构 → evidenceMap */ #extractProtocolEvidence(args, result) { if (typeof result !== 'object' || !result) { return; } const protocolName = result.protocolName || args.protocolName; const filePath = result.filePath; if (!filePath) { return; } const entry = this.#getOrCreateEntry(filePath); entry.role = entry.role || 'protocol-definition'; const parts = [`Protocol: ${protocolName}`]; if (result.methods?.length) { parts.push(`Methods: ${result.methods.length}`); } if (result.conformers?.length) { parts.push(`Conformers: ${result.conformers.slice(0, 5).join(', ')}`); } const summary = parts.join(' | '); entry.summary = entry.summary ? `${entry.summary}; ${summary}` : summary; } /** get_file_summary — 提取文件级摘要 → evidenceMap */ #extractFileSummary(args, result) { const filePath = args.filePath || (typeof result === 'object' && result?.filePath); if (!filePath) { return; } const entry = this.#getOrCreateEntry(filePath); const summaryText = typeof result === 'string' ? result.substring(0, 200) : result?.summary ? String(result.summary).substring(0, 200) : null; if (summaryText) { entry.summary = entry.summary ? `${entry.summary}; ${summaryText}` : summaryText; } } // ─── 内部辅助 ───────────────────────────────────────── /** 获取或创建 evidence entry */ #getOrCreateEntry(filePath) { let entry = this.#evidenceMap.get(filePath); if (!entry) { entry = { filePath, codeSnippets: [], summary: '' }; this.#evidenceMap.set(filePath, entry); } return entry; } /** 向 evidenceMap 添加代码片段 (带预算控制) */ #addCodeSnippet(filePath, content, startLine = 1) { if (!filePath || !content) { return; } if (this.#snippetCharsUsed >= this.#snippetBudget) { return; } const entry = this.#getOrCreateEntry(filePath); if (entry.codeSnippets.length >= MAX_SNIPPETS_PER_FILE) { return; } const lines = String(content).split('\n'); const trimmed = lines.slice(0, MAX_SNIPPET_LINES); const snippetContent = trimmed.join('\n'); if (!snippetContent) { return; } // 预算检查 if (this.#snippetCharsUsed + snippetContent.length > this.#snippetBudget) { return; } entry.codeSnippets.push({ startLine, endLine: startLine + trimmed.length - 1, content: snippetContent, }); this.#snippetCharsUsed += snippetContent.length; } /** 向 evidenceMap 添加搜索匹配 */ #addSearchMatch(match, searchNote) { if (!match?.file) { return; } const entry = this.#getOrCreateEntry(match.file); if (!match.line || !match.context) { return; } if (entry.codeSnippets.length >= MAX_SNIPPETS_PER_FILE) { return; } // 去重: 同一行不重复添加 if (entry.codeSnippets.some((s) => s.startLine === match.line)) { return; } const ctx = String(match.context).substring(0, 500); entry.codeSnippets.push({ startLine: match.line, endLine: match.line + (ctx.split('\n').length - 1), content: ctx, analystNote: `search: "${searchNote}"`, }); } /** 添加负空间信号 (去重) */ #addNegativeSignal(pattern) { if (!pattern) { return; } if (this.#negativeSignals.some((ns) => ns.searchPattern === pattern)) { return; } this.#negativeSignals.push({ searchPattern: pattern, result: 'not_found', implication: `未在项目中找到 "${pattern}" 相关模式`, }); } /** 检测错误字符串 */ #isErrorString(str) { return /not found|error|不存在|无法|failed/i.test(str); } /** 从搜索参数中提取搜索模式 */ #extractSearchPatterns(args) { if (args.patterns && Array.isArray(args.patterns)) { return args.patterns; } if (args.pattern) { return [args.pattern]; } if (args.query) { return [args.query]; } return []; } /** 推断工具调用意图 — WHY */ #inferIntent(tool, args) { switch (tool) { case 'read_project_file': if (args.filePaths?.length) { const preview = args.filePaths.slice(0, 3).join(', '); return `Read ${args.filePaths.length} files: ${preview}${args.filePaths.length > 3 ? '…' : ''}`; } return `Read ${args.filePath || '?'}`; case 'search_project_code': { const pats = this.#extractSearchPatterns(args); if (pats.length > 1) { return `Search ${pats.length} patterns: ${pats.slice(0, 3).join(', ')}`; } return `Search "${pats[0] || '?'}"`; } case 'semantic_search_code': return `Semantic search: "${args.query || '?'}"`; case 'get_class_info': return `Inspect class ${args.className || '?'}`; case 'get_protocol_info': return `Inspect protocol ${args.protocolName || '?'}`; case 'get_class_hierarchy': return `Get class hierarchy${args.rootClass ? ` from ${args.rootClass}` : ''}`; case 'get_project_overview': return 'Get project overview'; case 'list_project_structure': return `List ${args.directory || args.path || '/'}`; case 'get_file_summary': return `Summarize ${args.filePath || '?'}`; case 'get_method_overrides': return `Get overrides${args.methodName ? ` for ${args.methodName}` : ''}`; case 'get_category_map': return 'Get category map'; case 'note_finding': return `Note: ${(args.finding || '').substring(0, 50)}`; case 'get_previous_analysis': return `Get prev analysis${args.dimensionId ? ` for ${args.dimensionId}` : ''}`; case 'get_previous_evidence': return `Get prev evidence${args.query ? ` "${args.query}"` : ''}`; case 'query_code_graph': return `Query graph: ${(args.query || '').substring(0, 50)}`; default: return `${tool}(${JSON.stringify(args).substring(0, 50)})`; } } /** 生成工具结果摘要 — WHAT */ #summarizeResult(tool, result) { if (result == null) { return '(no result)'; } if (typeof result === 'string') { return result.length > 100 ? `${result.substring(0, 100)}…` : result; } if (typeof result !== 'object') { return String(result).substring(0, 100); } switch (tool) { case 'read_project_file': if (result.files) { return `${result.files.length} files read`; } if (result.content) { return `${(result.content || '').split('\n').length} lines from ${result.path || '?'}`; } return JSON.stringify(result).substring(0, 100); case 'search_project_code': case 'semantic_search_code': { const batchKeys = Object.keys(result.batchResults || {}); if (batchKeys.length > 0) { const total = batchKeys.reduce((s, k) => s + (result.batchResults[k]?.matches?.length || 0), 0); return `${total} matches across ${batchKeys.length} patterns`; } return `${(result.matches || []).length} matches`; } case 'get_class_info': return `class ${result.className || '?'}${result.superClass ? ` < ${result.superClass}` : ''}, ${result.methods?.length || 0} methods`; case 'get_class_hierarchy': return `${(result.classes || result.hierarchy || []).length} classes`; case 'get_project_overview': return 'overview loaded'; case 'list_project_structure': return `${(result.entries || result.children || []).length} entries`; default: return JSON.stringify(result).substring(0, 100); } } /** 判断工具调用是否有效 (获取到新信息) */ #isEffective(tool, result) { if (!result) { return false; } if (typeof result === 'string') { return !this.#isErrorString(result) && result.length > 10; } if (typeof result !== 'object') { return true; } switch (tool) { case 'read_project_file': return !!(result.content || result.files?.length); case 'search_project_code': case 'semantic_search_code': return ((result.matches?.length ?? 0) > 0 || Object.values(result.batchResults || {}).some((r) => (r.matches?.length ?? 0) > 0)); case 'get_class_info': return !!result.className; default: return true; } } } // ────────────────────────────────────────────────────────────────── // 类型定义 (JSDoc) // ────────────────────────────────────────────────────────────────── export default EvidenceCollector;