UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

337 lines (336 loc) 17.9 kB
/** * MCP Handler — autosnippet_rescan (增量知识更新) * * 保留已审核 Recipe,清理衍生缓存,全量/指定维度重新扫描, * 新知识通过批量提交走正常的进化架构。 * * 流程: * 1. snapshotRecipes — 快照保留知识 * 2. rescanClean — 清理衍生缓存 * 3. Phase 1-4 全量分析 * 4. RecipeRelevanceAuditor — 证据验证 + 快速衰退 * 5. 构建 Mission Briefing(含 allRecipes + evolutionGuide) * 6. 返回给 Agent 按维度执行: evolve → gap-fill → dimension_complete * * @module handlers/rescan-external */ import path from 'node:path'; import { CleanupService } from '#service/cleanup/CleanupService.js'; import { RecipeRelevanceAuditor } from '#service/evolution/RecipeRelevanceAuditor.js'; import { resolveProjectRoot } from '#shared/resolveProjectRoot.js'; import { buildProjectSnapshot } from '#types/project-snapshot-builder.js'; import { toSessionCache } from '#types/snapshot-views.js'; import { envelope } from '../envelope.js'; import { buildMissionBriefing } from './bootstrap/MissionBriefingBuilder.js'; import { extractCodeEntities, extractDependencyEdges } from './bootstrap/shared/audit-helpers.js'; import { runAllPhases } from './bootstrap/shared/bootstrap-phases.js'; import { getOrCreateSessionManager } from './bootstrap/shared/session-helpers.js'; import { buildLanguageExtension } from './LanguageExtensions.js'; // ── Helpers ───────────────────────────────────────────────── function truncate(s, max) { if (!s) { return ''; } return s.length <= max ? s : `${s.slice(0, max)}…`; } // ── 主入口 ───────────────────────────────────────────────── export async function rescanExternal(ctx, args) { const t0 = Date.now(); const projectRoot = resolveProjectRoot(ctx.container); const db = ctx.container.get('database'); // ═══════════════════════════════════════════════════════════ // Step 1: 快照现有知识 // ═══════════════════════════════════════════════════════════ const cleanupService = new CleanupService({ projectRoot, db, logger: ctx.logger, }); const recipeSnapshot = await cleanupService.snapshotRecipes(); ctx.logger.info(`[Rescan] Preserved ${recipeSnapshot.count} recipes`, { coverageByDimension: recipeSnapshot.coverageByDimension, }); // ═══════════════════════════════════════════════════════════ // Step 2: 清理衍生缓存 // ═══════════════════════════════════════════════════════════ const cleanResult = await cleanupService.rescanClean(); // ═══════════════════════════════════════════════════════════ // Step 2.5: Recipe 文件 ↔ DB 一致性恢复 + 向量索引重建 // ═══════════════════════════════════════════════════════════ // 2.5a: KnowledgeSyncService — 恢复 Recipe 文件 ↔ DB 一致性 // rescanClean 保留了 recipes/ 文件和 active/published/staging/evolving DB 记录, // 但清除了 recipe_source_refs 等桥接表,需重新同步。 try { const syncService = ctx.container.services.knowledgeSyncService ? ctx.container.get('knowledgeSyncService') : null; if (syncService) { const syncReport = syncService.sync(db, { force: true, }); ctx.logger.info('[Rescan] KnowledgeSyncService sync complete', { synced: syncReport.synced, created: syncReport.created, updated: syncReport.updated, }); } } catch (e) { ctx.logger.warn(`[Rescan] KnowledgeSyncService sync failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // NOTE: 不在 rescan 中调用 VectorService.fullBuild() // 理由:fullBuild 依赖外部 embedding API(LLM),在 MCP handler 同步路径中 // 引入 LLM 调用不合理(无超时、可能阻塞、需要 API key)。 // 向量索引会在后续 Agent 提交新知识时由 SyncCoordinator 增量更新。 // ═══════════════════════════════════════════════════════════ // Step 3: Phase 1-4 全量分析 // ═══════════════════════════════════════════════════════════ const phaseResults = await runAllPhases(projectRoot, ctx, { maxFiles: 500, contentMaxLines: 120, sourceTag: 'rescan-external', summaryPrefix: 'Rescan scan', clearOldData: false, // 已由 rescanClean 清理 generateReport: true, incremental: false, }); // 空项目 fast-path if (phaseResults.isEmpty) { return envelope({ success: true, data: { message: 'No source files found. Nothing to rescan.' }, meta: { tool: 'autosnippet_rescan', responseTimeMs: Date.now() - t0 }, }); } const { allFiles, primaryLang, depGraphData, langStats, astProjectSummary, codeEntityResult, callGraphResult, guardAudit, activeDimensions: allDimensions, targetsSummary, localPackageModules, langProfile, } = phaseResults; // ── Build immutable ProjectSnapshot ── const snapshot = buildProjectSnapshot({ projectRoot, sourceTag: 'rescan-external', ...phaseResults, report: phaseResults.report, }); // ═══════════════════════════════════════════════════════════ // Step 4: Recipe 证据验证 + 快速衰退 // ═══════════════════════════════════════════════════════════ const auditor = new RecipeRelevanceAuditor({ knowledgeRepo: ctx.container.get('knowledgeRepository'), proposalRepo: ctx.container.singletons ?.proposalRepository ? ctx.container.get('proposalRepository') : undefined, logger: ctx.logger, }); const codeEntities = extractCodeEntities(astProjectSummary); const dependencyEdges = extractDependencyEdges(depGraphData); const auditSummary = await auditor.audit(recipeSnapshot.entries, { fileList: allFiles.map((f) => f.relativePath || f.name), codeEntities, dependencyGraph: dependencyEdges, }); // ═══════════════════════════════════════════════════════════ // Step 5: 构建 Mission Briefing + 过滤维度 // ═══════════════════════════════════════════════════════════ // 按需过滤维度 const dimensions = args.dimensions?.length ? allDimensions.filter((d) => args.dimensions?.includes(d.id)) : allDimensions; // 创建 Session const sessionManager = getOrCreateSessionManager(ctx.container); const session = sessionManager.createSession({ projectRoot, dimensions, projectContext: { projectName: path.basename(projectRoot), primaryLang, fileCount: allFiles.length, modules: depGraphData?.nodes?.length || 0, }, }); // 缓存 Phase 结果 session.setSnapshotCache(toSessionCache(snapshot)); // 构建 projectMeta const projectMeta = { name: path.basename(projectRoot), primaryLanguage: primaryLang, secondaryLanguages: langProfile.secondary || [], isMultiLang: langProfile.isMultiLang || false, fileCount: allFiles.length, projectType: snapshot.discoverer.id, projectRoot, }; // 构建 Mission Briefing const briefing = buildMissionBriefing({ projectMeta, astData: astProjectSummary, codeEntityResult, callGraphResult, depGraphData, guardAudit, targets: targetsSummary, activeDimensions: dimensions, session, languageExtension: buildLanguageExtension(primaryLang), languageStats: langStats, panoramaResult: snapshot.panorama, localPackageModules, }); // 附加 warnings if (phaseResults.warnings.length > 0) { briefing.meta = briefing.meta || {}; briefing.meta.warnings = [...(briefing.meta.warnings || []), ...phaseResults.warnings]; } // ═══════════════════════════════════════════════════════════ // Step 6: 注入 evidenceHints (含 trigger/维度/doClause) // ═══════════════════════════════════════════════════════════ // 建立 recipeId → snapshot entry 映射,用于补充 audit 结果中缺少的字段 const snapshotById = new Map(recipeSnapshot.entries.map((e) => [e.id, e])); // ── 构建 allRecipes: 全部 Recipe (healthy + decaying) 含完整内容 + auditHint ── const allRecipes = auditSummary.results .filter((r) => r.verdict !== 'dead') // dead 已直接 deprecated .map((r) => { const snap = snapshotById.get(r.recipeId); const content = snap?.content; const sourceRefs = (snap?.sourceRefs ?? []); return { id: r.recipeId, title: r.title, trigger: snap?.trigger || '', knowledgeType: snap?.knowledgeType || '', doClause: snap?.doClause || '', lifecycle: snap?.lifecycle || 'active', // 完整内容(截断控制) content: content ? { markdown: truncate(content.markdown, 500), rationale: truncate(content.rationale, 200), coreCode: truncate(content.coreCode, 400), } : null, sourceRefs: sourceRefs.slice(0, 5), // 系统预检 auditHint: { relevanceScore: r.relevanceScore, verdict: r.verdict, decayReasons: r.decayReasons || [], }, }; }); const decayCount = allRecipes.filter((r) => r.auditHint.verdict === 'decay' || r.auditHint.verdict === 'severe').length; // ── 按维度分组现有 recipes,计算每维度的补齐配额 ── // 覆盖采用加权策略: // - active/evolving: 确认知识,始终计入覆盖 // - staging + audit healthy/watch: 待审但仍有效的候选,计入覆盖 // - staging + audit decay/severe/dead: 已过时的候选,不计入覆盖但占位去重 const TARGET_PER_DIM = 5; const auditVerdictMap = new Map(auditSummary.results.map((r) => [r.recipeId, r.verdict])); const coverageByDim = {}; for (const entry of recipeSnapshot.entries) { const dim = entry.knowledgeType || 'unknown'; const isConfirmed = entry.lifecycle === 'active' || entry.lifecycle === 'evolving'; const verdict = auditVerdictMap.get(entry.id); // staging 条目:无 audit 结果时默认计入(首次冷启动后无 audit 数据),audit 通过时计入 const isHealthyStaging = entry.lifecycle === 'staging' && (!verdict || verdict === 'healthy' || verdict === 'watch'); if (isConfirmed || isHealthyStaging) { if (!coverageByDim[dim]) { coverageByDim[dim] = []; } coverageByDim[dim].push({ title: entry.title, trigger: entry.trigger, doClause: entry.doClause || '', }); } } const dimensionGaps = dimensions.map((d) => { const existing = coverageByDim[d.id] || []; return { dimensionId: d.id, existingCount: existing.length, gap: Math.max(0, TARGET_PER_DIM - existing.length), existingTriggers: existing.map((e) => e.trigger).filter(Boolean), }; }); const totalGap = dimensionGaps.reduce((sum, g) => sum + g.gap, 0); // occupiedTriggers 包含全量(含 audit-failed 的 staging),防止 trigger 冲突 const occupiedTriggers = recipeSnapshot.entries.map((e) => e.trigger).filter(Boolean); // ── 注入 evidenceHints (allRecipes + evolutionGuide + dimensionGaps) ── briefing.evidenceHints = { allRecipes, rescanMode: true, dimensionGaps, evolutionGuide: { decayCount, totalCount: allRecipes.length, instructions: decayCount > 0 ? `${decayCount} 个 Recipe 标记为衰退,需优先验证。每个维度内先 evolve 再补齐。` : '所有 Recipe 状态健康,快速确认后补齐新知识。', }, constraints: { occupiedTriggers, rules: [ '禁止提交 occupiedTriggers 列表中已存在的 trigger', '每个维度的补齐数量参考 dimensionGaps[].gap,gap=0 的维度可以跳过或只提交真正的新发现', '专注于尚未覆盖的新模式,不要重复已有知识的内容', ], }, }; // ── 覆盖 executionPlan.workflow 为 rescan 专属版本 (per-dimension evolve + gap-fill) ── const briefingRecord = briefing; if (briefingRecord.executionPlan && typeof briefingRecord.executionPlan === 'object') { briefingRecord.executionPlan.workflow = '【增量扫描模式 — 按维度 Evolve + Gap-Fill】 ' + '对每个维度 (按 tiers 顺序): ' + 'Step 1 — Evolve (维度级首步): ' + '过滤 allRecipes 中本维度的 Recipe,读 sourceRefs 源码验证 → ' + '调用 autosnippet_evolve({ decisions: [本维度决策] }) → ' + 'Step 2 — Gap-Fill: ' + '分析代码发现新模式 → 调用 autosnippet_submit_knowledge 提交 (数量参考 gap 值) → ' + 'Step 3 — Complete: 调用 autosnippet_dimension_complete 完成维度'; } const dimGapLog = dimensionGaps .map((g) => `${g.dimensionId}(${g.existingCount}→gap ${g.gap})`) .join(', '); ctx.logger.info(`[Rescan] Mission Briefing ready: ${allFiles.length} files, ${dimensions.length} dims, ` + `preserved: ${recipeSnapshot.count}, decayed: ${decayCount}, totalGap: ${totalGap} — session ${session.id}`); ctx.logger.info(`[Rescan] Dimension gaps: ${dimGapLog}`); // ── 构建 gap 摘要信息 ── const gapSummaryParts = dimensionGaps .filter((g) => g.gap > 0) .map((g) => `${g.dimensionId}(需补${g.gap}条)`); const coveredDims = dimensionGaps.filter((g) => g.gap === 0).length; const gapSummary = gapSummaryParts.length > 0 ? `需补齐维度: ${gapSummaryParts.join('、')}。` : '所有维度已充分覆盖,仅在发现全新模式时提交。'; return envelope({ success: true, data: { rescan: { preservedRecipes: recipeSnapshot.count, cleanedTables: cleanResult.clearedTables.length, cleanedFiles: cleanResult.deletedFiles, reason: args.reason || null, }, relevanceAudit: { totalAudited: auditSummary.totalAudited, healthy: auditSummary.healthy, watch: auditSummary.watch, decay: auditSummary.decay, severe: auditSummary.severe, dead: auditSummary.dead, proposalsCreated: auditSummary.proposalsCreated, immediateDeprecated: auditSummary.immediateDeprecated, }, ...briefing, }, message: `✅ Rescan 完成项目扫描,保留 ${recipeSnapshot.count} 个 Recipe(衰退 ${decayCount} 个),` + `${coveredDims}/${dimensions.length} 个维度已充分覆盖。` + `${gapSummary}` + `对每个维度执行三步:` + `(1) autosnippet_evolve — 过滤 allRecipes 中本维度 Recipe,读源码验证后提交决策 → ` + `(2) autosnippet_submit_knowledge — 分析代码,发现未覆盖的新模式 → ` + `(3) autosnippet_dimension_complete — 标记维度完成。` + `注意: evidenceHints.constraints.occupiedTriggers 中的 trigger 已被占用,请勿重复。`, meta: { tool: 'autosnippet_rescan', responseTimeMs: Date.now() - t0 }, }); }