UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

441 lines (440 loc) 21.3 kB
/** * insight-gate.js — Insight 质量门控领域函数 * * 从旧 HandoffProtocol.js 完整迁移的纯函数模块: * - 分析文本清洗 (sanitizeAnalysisText) * - AnalysisReport 构建 (v1) * - AnalysisArtifact 构建 (v2, 含 evidenceMap/findings/negativeSignals) * - 多维度质量评分 (buildQualityScores) * - 质量门控 (v1 + v2) * - 重试 Prompt 构建 * - PipelineStrategy gate.evaluator 适配器 (insightGateEvaluator) * * 被 PipelineStrategy 的 bootstrap preset 直接引用。 * * @module insight-gate */ import Logger from '#infra/logging/Logger.js'; import { EvidenceCollector, } from './EvidenceCollector.js'; const logger = Logger.getInstance(); // ────────────────────────────────────────────────────────────────── // AnalysisReport 构建 // ────────────────────────────────────────────────────────────────── /** * 清理 Analyst 分析文本中可能泄漏的系统 nudge / graceful exit 指令。 * 这些内容如果传给 Producer,会干扰其正常工作流。 */ export function sanitizeAnalysisText(text) { if (!text) { return ''; } const patterns = [ /\*{0,2}⚠️?\s*(?:你已使用|轮次即将耗尽|仅剩|请立即停止|必须立即结束)[^\n]*\n?/gi, /\*{0,2}请立即停止所有工具调用[^\n]*\*{0,2}\n?/gi, /请在回复中直接输出\s*dimensionDigest\s*JSON[^\n]*\n?/gi, /> ?(?:remainingTasks|如果所有信号都已覆盖)[^\n]*\n?/gi, /> ?⚠️ 严禁输出任何非 JSON 内容[^\n]*\n?/gi, /```json\s*\n\s*\{\s*"dimensionDigest"\s*:[\s\S]*?\n```/g, /^-{2,3}\s*\n\s*第\s*\d+\/\d+\s*轮[^\n]*\n(-{2,3}\s*\n)?/gm, /^-{3}\s*$/gm, /^#{1,3}\s*(?:计划偏差分析|最终总结阶段|执行计划|下一步计划|分析计划)\s*\n[\s\S]*?(?=\n#{1,3}\s|\n\n(?=[^#\s-]))/gm, /^\(提示[::][^)]*\)\s*\n?/gm, /^(?:Wait,|Let me|I'll stop here|I will stop|I need to|I should|I have enough)[^\n]*\n?/gm, /^[-•]\s*尝试使用\s*`[^`]+`[^\n]*\n?/gm, /^💡\s*提示[::]?\s*\n?/gm, /^请(?:继续|接续)[。.]?\s*$/gm, /📊\s*中期反思\s*\([^)]*\):?\s*\n(?:[\s\S]*?(?=\n#{1,3}\s(?!探索计划|第\s*\d)|\n(?=📊)|$))/gm, /^你最近的思考方向:\s*\n(?:[\s\S]*?(?=\n#{1,3}\s(?!探索计划|第\s*\d)|\n(?=📊)|$))/gm, /^#{1,3}\s*探索计划\s*\n(?:[\s\S]*?(?=\n#{1,3}\s(?!探索计划)|\n\n(?=[^#\s\d-])|\n(?=📊)|$))/gm, /^\s*\d+\.\s+#{1,3}\s*探索计划[^\n]*\n(?:\d+\.\s+\*{0,2}[^\n]*\n?)*/gm, /^#{1,3}\s*第\s*\d+\s*轮[::][^\n]*\n(?:[\s\S]*?(?=\n#{1,3}\s(?!探索计划|第\s*\d)|\n\n(?=#{1,3}\s)|\n(?=📊)|$))/gm, /^行动效率[::][^\n]*\n?/gm, /^累计[::]\s*\d+\s*文件[^\n]*\n?/gm, /^📋\s*计划进度[::][^\n]*\n?/gm, /^请评估[::]\s*\n(?:\s*\d+\.\s+[^\n]*\n?)*/gm, /^\([请由注](?:在继续|于当前|意[::])[^)]*\)\s*\n?/gm, /^(?:\d+\.\s+)?(?:`[^`]*`\s+)?(?:已经读取|未完成步骤仅剩|计划更新|更新后的计划)[^\n]*\n?/gm, /^更新后的计划[::]\s*\n(?:\s*\d+\.\s+[^\n]*\n?)*/gm, /^\s*\d+\.\s*$/gm, /^>\s*(?:searchHints|remainingTasks|candidateCount|crossRefs|keyFindings|gaps)\s*[::][^\n]*\n?/gm, /^\*{0,2}(?:请在|请直接|请确保|请务必|现在开始|输出你的|不要输出|不要再|不要包含)\s*[^。\n]*(?:分析文本|分析总结|分析报告|JSON|工具|输出|文本|报告)[^。\n]*[。.]?\s*\*{0,2}$/gm, /^\*{0,2}重要\s*[::][^。\n]*\*{0,2}$/gm, /^注意[::]\s*到达第\s*\d+\s*轮时[^\n]*$/gm, /^第\s*\d+\/\d+\s*轮\s*\|[^\n]*$/gm, ]; let cleaned = text; for (const pat of patterns) { cleaned = cleaned.replace(pat, ''); } cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim(); return cleaned; } /** * 从 Analyst 的执行结果构建 AnalysisReport (v1) * * @param analystResult { reply, toolCalls } * @param dimensionId 维度 ID * @param [projectGraph] ProjectGraph 实例 */ export function buildAnalysisReport(analystResult, dimensionId, projectGraph = null) { const referencedFiles = new Set(); const searchQueries = []; const classesExplored = []; for (const call of analystResult.toolCalls || []) { const tool = call.tool || call.name; const args = call.params || call.args || {}; const result = call.result; switch (tool) { case 'read_project_file': if (args.filePath) { referencedFiles.add(args.filePath); } break; case 'search_project_code': if (args.pattern || args.query) { searchQueries.push((args.pattern || args.query)); } if (typeof result === 'string') { const fileMatches = result.match(/(?:^|\n)([\w/.-]+\.(?:go|mod|sum|py|pyi|java|kt|kts|js|ts|jsx|tsx|mjs|cjs|swift|m|h|c|cpp|cc|hpp|cs|rb|rs|sql|json|yaml|yml|toml|xml|html|css|scss|less|sh|md|txt|gradle|properties|proto|vue|svelte|graphql|cfg|conf|ini|env|lock|rst))(?::\d+)?/gi); if (fileMatches) { for (const m of fileMatches) { const clean = m.trim().replace(/:\d+$/, '').replace(/^\n/, ''); if (clean.length > 2 && clean.length < 120) { referencedFiles.add(clean); } } } } break; case 'get_class_info': if (args.className) { classesExplored.push(args.className); if (projectGraph) { const info = projectGraph.getClassInfo(args.className); if (info?.filePath) { referencedFiles.add(info.filePath); } } } break; case 'get_protocol_info': if (args.protocolName && projectGraph) { const info = projectGraph.getProtocolInfo(args.protocolName); if (info?.filePath) { referencedFiles.add(info.filePath); } } break; case 'get_file_summary': if (args.filePath) { referencedFiles.add(args.filePath); } break; } } // 从分析文本中提取文件路径 const text = sanitizeAnalysisText(analystResult.reply || ''); const FILE_EXT_RE = /[\w/.-]+\.(?:go|mod|sum|py|pyi|java|kt|kts|js|ts|jsx|tsx|mjs|cjs|swift|m|h|c|cpp|cc|hpp|cs|rb|rs|sql|json|yaml|yml|toml|xml|html|css|scss|less|sh|md|txt|gradle|properties|proto|vue|svelte|graphql|cfg|conf|ini|env|lock|rst)\b/gi; const textFileRefs = text.match(FILE_EXT_RE); if (textFileRefs) { for (const f of textFileRefs) { if (f.length > 2 && f.length < 120) { referencedFiles.add(f); } } } return { analysisText: text, referencedFiles: [...referencedFiles], searchQueries, classesExplored, dimensionId, metadata: { iterations: analystResult.toolCalls?.length || 0, toolCallCount: analystResult.toolCalls?.length || 0, tokenUsage: analystResult.tokenUsage || null, reasoningQuality: analystResult.reasoningQuality || null, }, }; } // ────────────────────────────────────────────────────────────────── // AnalysisArtifact 构建 (v2) // ────────────────────────────────────────────────────────────────── /** * 从 Analyst 执行结果构建 AnalysisArtifact (v2 增强版) * * 在 v1 AnalysisReport 基础上增加: * - evidenceMap: 文件 → 代码片段 + 摘要 * - explorationLog: 工具调用意图 + 结果摘要序列 * - negativeSignals: 搜索但未找到的模式 * - findings: 来自 ActiveContext 的结构化发现 * - qualityReport: 多维度质量评分 * * @param analystResult { reply, toolCalls } * @param dimensionId 维度 ID * @param [projectGraph] ProjectGraph 实例 * @param [activeContext] ActiveContext 实例 */ export function buildAnalysisArtifact(analystResult, dimensionId, projectGraph = null, activeContext = null) { const toolCalls = analystResult.toolCalls || []; const baseReport = buildAnalysisReport(analystResult, dimensionId, projectGraph); const collector = new EvidenceCollector(); for (let i = 0; i < toolCalls.length; i++) { collector.processToolCall(toolCalls[i], i); } const evidence = collector.build(); const distilled = activeContext?.distill() || { keyFindings: [], toolCallSummary: [] }; const findings = distilled.keyFindings.map((f) => ({ finding: f.finding, evidence: typeof f.evidence === 'string' ? f.evidence : Array.isArray(f.evidence) ? f.evidence.join(', ') : f.evidence ? String(f.evidence) : '', importance: f.importance, })); const allFiles = new Set(baseReport.referencedFiles); for (const filePath of evidence.evidenceMap.keys()) { allFiles.add(filePath); } const qualityReport = buildQualityScores(baseReport.analysisText, findings, evidence); return { // Layer 1: Core analysisText: baseReport.analysisText, findings, referencedFiles: [...allFiles], dimensionId, // Layer 2: Detail evidenceMap: evidence.evidenceMap, explorationLog: evidence.explorationLog, negativeSignals: evidence.negativeSignals, // Layer 3: Raw fullToolTrace: toolCalls, // Quality qualityReport, // Metadata metadata: { ...baseReport.metadata, artifactVersion: 2, }, // v1 backward compat searchQueries: baseReport.searchQueries, classesExplored: baseReport.classesExplored, }; } // ────────────────────────────────────────────────────────────────── // 多维度质量评分 (v2) // ────────────────────────────────────────────────────────────────── /** * 计算 AnalysisArtifact 的多维度质量评分 * * 4 维度各 0-100, 加权: * depthScore (30%) — 文件覆盖深度 * breadthScore (20%) — 工具使用广度 * evidenceScore (30%) — 证据充分性 * coherenceScore (20%) — 分析连贯性 */ function buildQualityScores(analysisText, findings, evidence) { const scores = {}; const uniqueFilesRead = evidence.evidenceMap?.size || 0; const snippetCount = [...(evidence.evidenceMap?.values() || [])].reduce((sum, e) => sum + e.codeSnippets.length, 0); scores.depthScore = Math.min(100, uniqueFilesRead * 15 + snippetCount * 5); const toolTypes = new Set((evidence.explorationLog || []).map((e) => e.tool)); const logLen = evidence.explorationLog?.length || 0; const effectiveRatio = logLen > 0 ? (evidence.explorationLog || []).filter((e) => e.effective).length / logLen : 0; scores.breadthScore = Math.min(100, toolTypes.size * 20 + effectiveRatio * 40); const findingCount = findings?.length || 0; const evidencedFindings = (findings || []).filter((f) => f.evidence && f.evidence.length > 0).length; scores.evidenceScore = findingCount > 0 ? Math.min(100, (evidencedFindings / findingCount) * 60 + findingCount * 10) : 0; const textLen = analysisText?.length || 0; const hasHeaders = /#{1,3}\s/.test(analysisText || ''); const hasLists = /\d+\.\s|[-•]\s/.test(analysisText || ''); scores.coherenceScore = Math.min(100, (textLen > 500 ? 40 : textLen / 12.5) + (hasHeaders ? 20 : 0) + (hasLists ? 20 : 0) + (findingCount >= 3 ? 20 : findingCount * 7)); const totalScore = Math.round(scores.depthScore * 0.3 + scores.breadthScore * 0.2 + scores.evidenceScore * 0.3 + scores.coherenceScore * 0.2); const suggestions = []; if (scores.depthScore < 50) { suggestions.push('Need more read_project_file to examine code'); } if (scores.evidenceScore < 50) { suggestions.push('Findings lack file-level evidence'); } if (scores.coherenceScore < 50) { suggestions.push('Analysis text is too short or unstructured'); } return { scores, totalScore, suggestions }; } // ────────────────────────────────────────────────────────────────── // 质量门控 (Gate) // ────────────────────────────────────────────────────────────────── /** * 分析质量门控 * * 自动检测 v1 (AnalysisReport) 和 v2 (AnalysisArtifact): * - v2: 从 qualityReport.totalScore 计算 * - v1: 使用 4 条规则 * * @param [options.outputType] 'analysis' | 'dual' | 'candidate' * @returns } */ export function analysisQualityGate(report, options = {}) { if (report.qualityReport?.scores) { return applyGateThresholds(report.qualityReport, options); } return analysisQualityGateV1(report, options); } function applyGateThresholds(qualityReport, options = {}) { const { totalScore } = qualityReport; const needsCandidates = options.outputType === 'dual' || options.outputType === 'candidate'; const threshold = needsCandidates ? 60 : 45; if (totalScore >= threshold) { return { pass: true }; } if (totalScore >= threshold - 20) { return { pass: false, reason: `Quality score ${totalScore}/${threshold}`, action: 'retry', }; } return { pass: false, reason: `Quality score ${totalScore}/${threshold}`, action: 'degrade', }; } function analysisQualityGateV1(report, options = {}) { const needsCandidates = options.outputType === 'dual' || options.outputType === 'candidate'; const minChars = needsCandidates ? 400 : 200; const minFileRefs = needsCandidates ? 3 : 2; if (report.analysisText.length < minChars) { return { pass: false, reason: 'Analysis too short', action: 'retry' }; } if (report.referencedFiles.length < minFileRefs) { return { pass: false, reason: 'Too few file references', action: 'retry' }; } const refusalPatterns = [ /I cannot|I'm unable|I don't have access/i, /无法分析|无法访问|没有足够/, ]; if (refusalPatterns.some((p) => p.test(report.analysisText))) { return { pass: false, reason: 'Agent refused to analyze', action: 'degrade' }; } const hasStructure = /#{1,3}\s/.test(report.analysisText) || /\d+\.\s/.test(report.analysisText) || /[-•]\s/.test(report.analysisText) || /[::].+\n/.test(report.analysisText) || report.analysisText.length >= 500 || (report.referencedFiles.length >= 3 && report.analysisText.length >= 200); if (!hasStructure) { return { pass: false, reason: 'Analysis lacks structure', action: 'retry' }; } return { pass: true }; } /** * 构建重试提示 * * @param reason Gate 失败原因 */ export function buildRetryPrompt(reason) { const hints = { 'Analysis too short': '你的分析不够深入。请使用更多工具(get_class_info、read_project_file、search_project_code)查看实际代码,输出至少 500 字的分析。', 'Too few file references': '你的分析缺少代码引用。请使用 get_class_info 和 read_project_file 查看至少 3 个相关文件,并在分析中引用具体文件和行号。', 'Analysis lacks structure': '请将分析组织成结构化的段落,使用编号列表或标题来区分不同的发现。每个发现应包含具体的文件路径和代码位置。', }; return (hints[reason] || '请更深入地分析代码,引用至少 3 个具体文件,每个发现都要有代码证据。'); } // ────────────────────────────────────────────────────────────────── // PipelineStrategy gate.evaluator 适配器 // ────────────────────────────────────────────────────────────────── /** * 面向 PipelineStrategy gate.evaluator 的包装函数。 * * 将 PipelineStrategy 的 (source, phaseResults, strategyContext) 签名 * 适配到 buildAnalysisArtifact + analysisQualityGate 调用链。 * * @param source 前一阶段 (analyze) 的 reactLoop 返回值 * @param phaseResults 所有阶段结果 * @param strategyContext orchestrator 注入的运行时上下文 * @returns } */ export function insightGateEvaluator(source, phaseResults, strategyContext = {}) { if (!source?.reply) { return { action: 'degrade', reason: 'No analysis output', artifact: null }; } const { projectGraph, activeContext, dimId, outputType } = strategyContext; const artifact = activeContext ? buildAnalysisArtifact(source, dimId, projectGraph, activeContext) : buildAnalysisReport(source, dimId, projectGraph); const gate = analysisQualityGate(artifact, { outputType: outputType || 'analysis' }); const qr = artifact.qualityReport; if (qr?.scores) { logger.info(`[QualityGate] dim="${dimId}" action=${gate.pass ? 'pass' : gate.action} ` + `total=${qr.totalScore} depth=${qr.scores.depthScore} breadth=${qr.scores.breadthScore} ` + `evidence=${qr.scores.evidenceScore} coherence=${qr.scores.coherenceScore}` + (qr.suggestions.length > 0 ? ` suggestions=[${qr.suggestions.join('; ')}]` : '')); } else { logger.info(`[QualityGate] dim="${dimId}" action=${gate.pass ? 'pass' : gate.action} reason="${gate.reason || 'v1-rules'}" (v1 fallback)`); } return { action: gate.action || (gate.pass ? 'pass' : 'retry'), reason: gate.reason || '', artifact, }; } /** * Evolution Gate 评估器 — 面向 PipelineStrategy gate.evaluator * * 检查 Evolution Agent 是否对所有现有 Recipe 做出了决策: * - evolved (submit_knowledge with supersedes) * - deprecated (confirm_deprecation) * - skipped (skip_evolution) * * 如果还有未处理的 Recipe,返回 retry 要求补充决策。 * * 兼容旧字段: 优先读 existingRecipes,回退 decayedRecipes。 */ export function evolutionGateEvaluator(source, _phaseResults, strategyContext = {}) { const totalRecipes = (strategyContext.existingRecipes ?? strategyContext.decayedRecipes ?? []) .length; const toolCalls = source?.toolCalls || []; // 统计各决策数 const evolved = toolCalls.filter((tc) => { const tool = tc.tool || tc.name; return tool === 'submit_knowledge' && tc.args?.supersedes; }).length; const deprecated = toolCalls.filter((tc) => { const tool = tc.tool || tc.name; return tool === 'confirm_deprecation'; }).length; const skipped = toolCalls.filter((tc) => { const tool = tc.tool || tc.name; return tool === 'skip_evolution'; }).length; const processed = evolved + deprecated + skipped; if (totalRecipes > 0 && processed < totalRecipes) { return { action: 'retry', reason: `只处理了 ${processed}/${totalRecipes} 个 Recipe,还有 ${totalRecipes - processed} 个未决策`, }; } return { action: 'pass', artifact: { evolved, deprecated, skipped, totalRecipes }, }; } // ────────────────────────────────────────────────────────────────── // 类型定义 (JSDoc) // ──────────────────────────────────────────────────────────────────