UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

364 lines (363 loc) 19.1 kB
/** * rescan-internal.ts — 内部 Agent 增量扫描 * * 与 rescan-external.ts(为外部 IDE Agent 生成 Mission Briefing)不同, * 本文件由内置 AI pipeline(fillDimensionsV3)在服务端自动完成知识补齐。 * * 流程: * 1. snapshotRecipes — 快照保留知识 * 2. rescanClean — 清理衍生缓存 * 2.5 Recipe 文件 ↔ DB 一致性恢复 * 3. Phase 1-4 全量分析(文件收集→AST→依赖→Guard→维度) * 4. RecipeRelevanceAuditor — 证据验证 + 快速衰退 * 5. 计算 gap 维度(需要补齐的维度) * 5.5 BootstrapSessionManager — 缓存 Phase 结果供复用 * 6. 快速返回骨架 → 异步 fillDimensionsV3 填充 gap 维度 * 7. 前端通过 Socket.io 接收维度完成进度 * * @module handlers/rescan-internal */ 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 { fillDimensionsV3 } from './bootstrap/pipeline/orchestrator.js'; import { buildTaskDefs, dispatchPipelineFill, startTaskManagerSession, } from './bootstrap/shared/async-fill-helpers.js'; import { extractCodeEntities, extractDependencyEdges } from './bootstrap/shared/audit-helpers.js'; import { runAllPhases } from './bootstrap/shared/bootstrap-phases.js'; import { summarizePanorama } from './bootstrap/shared/panorama-utils.js'; import { getOrCreateSessionManager } from './bootstrap/shared/session-helpers.js'; import { buildTargetFileMap } from './bootstrap/shared/target-file-map.js'; // ── 主入口 ────────────────────────────────────────────── const TARGET_PER_DIM = 5; /** * rescanInternal — 内部 Agent 增量扫描 * * 同步返回骨架(含 audit 摘要 + 异步会话 ID), * 后台通过 fillDimensionsV3 对 gap 维度执行 AI 补齐。 */ export async function rescanInternal(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-Internal] Preserved ${recipeSnapshot.count} recipes`, { coverageByDimension: recipeSnapshot.coverageByDimension, }); // ═══════════════════════════════════════════════════════════ // Step 2: 清理衍生缓存 // ═══════════════════════════════════════════════════════════ const cleanResult = await cleanupService.rescanClean(); // ═══════════════════════════════════════════════════════════ // Step 2.5: Recipe 文件 ↔ DB 一致性恢复 // ═══════════════════════════════════════════════════════════ try { let syncService = null; try { syncService = ctx.container.get('knowledgeSyncService'); } catch { /* not registered */ } if (syncService) { const syncReport = syncService.sync(db, { force: true, }); ctx.logger.info('[Rescan-Internal] KnowledgeSyncService sync complete', { synced: syncReport.synced, created: syncReport.created, updated: syncReport.updated, }); } } catch (e) { ctx.logger.warn(`[Rescan-Internal] KnowledgeSyncService sync failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // ═══════════════════════════════════════════════════════════ // Step 3: Phase 1-4 全量分析 // ═══════════════════════════════════════════════════════════ const contentMaxLines = 120; const phaseResults = await runAllPhases(projectRoot, ctx, { maxFiles: 500, contentMaxLines, sourceTag: 'rescan-internal', summaryPrefix: 'Rescan-Internal scan', clearOldData: false, // 已由 rescanClean 清理 generateReport: true, generateAstContext: true, incremental: false, }); 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, allTargets, primaryLang, depGraphData, astProjectSummary, guardAudit, activeDimensions: allDimensions, incrementalPlan: _incrementalPlan, } = phaseResults; // ── Build immutable ProjectSnapshot ── const snapshot = buildProjectSnapshot({ projectRoot, sourceTag: 'rescan-internal', ...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, }); ctx.logger.info('[Rescan-Internal] Relevance audit complete', { total: auditSummary.totalAudited, healthy: auditSummary.healthy, watch: auditSummary.watch, decay: auditSummary.decay, severe: auditSummary.severe, dead: auditSummary.dead, }); // ═══════════════════════════════════════════════════════════ // Step 4.5: ★ Evolution Pass 候选收集 // 收集 decay + severe 的 Recipe(dead 已直接 deprecated,不进入 Evolution) // ═══════════════════════════════════════════════════════════ const evolutionCandidates = auditSummary.results.filter((r) => r.verdict === 'decay' || r.verdict === 'severe'); if (evolutionCandidates.length > 0) { ctx.logger.info('[Rescan-Internal] Evolution candidates collected', { count: evolutionCandidates.length, byVerdict: { decay: evolutionCandidates.filter((c) => c.verdict === 'decay').length, severe: evolutionCandidates.filter((c) => c.verdict === 'severe') .length, }, }); } // ═══════════════════════════════════════════════════════════ // Step 5: 计算 gap 维度 + 过滤出需要补齐的维度 // ═══════════════════════════════════════════════════════════ // 按维度统计已有 recipe 覆盖(加权策略): // - active/evolving: 确认知识,始终计入 // - staging + audit healthy/watch: 有效候选,计入 // - staging + audit decay/severe/dead: 过时候选,不计入覆盖 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); const isHealthyStaging = entry.lifecycle === 'staging' && (!verdict || verdict === 'healthy' || verdict === 'watch'); if (isConfirmed || isHealthyStaging) { coverageByDim[dim] = (coverageByDim[dim] || 0) + 1; } } // 过滤需要补齐的维度 const requestedDimensions = args.dimensions?.length ? allDimensions.filter((d) => args.dimensions?.includes(d.id)) : allDimensions; const gapDimensions = requestedDimensions.filter((d) => { const existing = coverageByDim[d.id] || 0; return existing < TARGET_PER_DIM; }); const skippedDimensions = requestedDimensions.filter((d) => { const existing = coverageByDim[d.id] || 0; return existing >= TARGET_PER_DIM; }); ctx.logger.info('[Rescan-Internal] Gap analysis', { totalDimensions: requestedDimensions.length, gapDimensions: gapDimensions.length, skippedDimensions: skippedDimensions.length, gapDetails: gapDimensions.map((d) => ({ id: d.id, existing: coverageByDim[d.id] || 0, gap: TARGET_PER_DIM - (coverageByDim[d.id] || 0), })), }); // ═══════════════════════════════════════════════════════════ // Step 5.5: BootstrapSessionManager — 缓存 Phase 结果供复用 // (与 bootstrap-internal Phase 4.6 对齐) // ═══════════════════════════════════════════════════════════ let sessionId = null; try { const sessionManager = getOrCreateSessionManager(ctx.container); const bsSession = sessionManager.createSession({ projectRoot, dimensions: gapDimensions.map((d) => ({ ...d, skillMeta: d.skillMeta ?? undefined, })), projectContext: { projectName: path.basename(projectRoot), primaryLang, fileCount: allFiles.length, modules: depGraphData?.nodes?.length || 0, }, }); bsSession.setSnapshotCache(toSessionCache(snapshot)); sessionId = bsSession.id; } catch (e) { ctx.logger.warn(`[Rescan-Internal] BootstrapSessionManager setup failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); } // ═══════════════════════════════════════════════════════════ // Step 6: 构建 targetFileMap + 任务清单 → 快速返回骨架 // ═══════════════════════════════════════════════════════════ const targetFileMap = buildTargetFileMap(allFiles, contentMaxLines); // 任务定义仅包含 gap 维度 const taskDefs = buildTaskDefs(gapDimensions); // 启动 BootstrapTaskManager 会话 const bootstrapSession = startTaskManagerSession(ctx.container, taskDefs, ctx.logger, 'Rescan-Internal'); // 构建骨架响应 const responseData = { 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, }, gapAnalysis: { totalDimensions: requestedDimensions.length, gapDimensions: gapDimensions.length, skippedDimensions: skippedDimensions.map((d) => d.id), gaps: gapDimensions.map((d) => ({ dimensionId: d.id, label: d.label, existing: coverageByDim[d.id] || 0, gap: TARGET_PER_DIM - (coverageByDim[d.id] || 0), })), }, // Phase 1-4 分析摘要 (与 bootstrap-internal 对齐) languageStats: phaseResults.langStats || null, primaryLanguage: primaryLang, guardSummary: guardAudit ? { totalViolations: guardAudit.summary?.totalViolations || 0, errors: guardAudit.summary?.errors || 0, warnings: guardAudit.summary?.warnings || 0, } : null, astSummary: astProjectSummary ? { classes: astProjectSummary.classes?.length || 0, protocols: astProjectSummary.protocols?.length || 0, categories: astProjectSummary.categories?.length || 0, } : null, codeEntityGraph: phaseResults.codeEntityResult ? { totalEntities: phaseResults.codeEntityResult.entityCount || 0, totalEdges: phaseResults.codeEntityResult.edgeCount || 0, } : null, callGraph: phaseResults.callGraphResult ? { entitiesUpserted: phaseResults.callGraphResult.entitiesUpserted || 0, edgesCreated: phaseResults.callGraphResult.edgesCreated || 0, } : null, panorama: snapshot.panorama ? summarizePanorama(snapshot.panorama) : null, bootstrapSession: bootstrapSession ? bootstrapSession.toJSON() : null, sessionId, asyncFill: gapDimensions.length > 0, status: gapDimensions.length > 0 ? 'filling' : 'complete', files: allFiles.length, targets: allTargets.length, }; // ═══════════════════════════════════════════════════════════ // Step 7: 异步后台填充 gap 维度 // ═══════════════════════════════════════════════════════════ if (gapDimensions.length > 0 && !args.skipAsyncFill) { const fillView = { snapshot, ctx: ctx, bootstrapSession, targetFileMap, projectRoot, }; // 构建 existingRecipes(含审计状态 + 完整内容),供管线内 Evolution Stage 使用 const allExistingRecipes = recipeSnapshot.entries.map((e) => { const auditResult = auditSummary.results.find((r) => r.recipeId === e.id); const verdict = auditVerdictMap.get(e.id); const isDecaying = e.lifecycle === 'decaying' || verdict === 'decay' || verdict === 'severe'; return { id: e.id, title: e.title, trigger: e.trigger, knowledgeType: e.knowledgeType, status: isDecaying ? 'decaying' : 'healthy', decayReason: isDecaying && auditResult?.decayReasons ? auditResult.decayReasons.join('; ') : undefined, auditScore: auditResult?.relevanceScore, // Evolution Agent 需要完整内容来验证 Recipe 真实性 content: e.content, sourceRefs: e.sourceRefs, auditEvidence: auditResult ?.evidence, }; }); dispatchPipelineFill({ ...fillView, existingRecipes: allExistingRecipes }, gapDimensions, fillDimensionsV3, 'Rescan-Internal'); } else if (gapDimensions.length === 0) { ctx.logger.info('[Rescan-Internal] All dimensions fully covered — no async fill needed'); } // ── SkillHooks: onRescanComplete (fire-and-forget) ── try { const skillHooks = ctx.container.get('skillHooks'); skillHooks .run('onRescanComplete', { filesScanned: allFiles.length, targetsFound: allTargets.length, gapDimensions: gapDimensions.length, preservedRecipes: recipeSnapshot.count, auditSummary: { healthy: auditSummary.healthy, decay: auditSummary.decay, dead: auditSummary.dead, }, }, { projectRoot: ctx.container.get('database')?.filename || '' }) .catch(() => { }); // fire-and-forget } catch { /* skillHooks not available */ } return envelope({ success: true, data: responseData, message: gapDimensions.length > 0 ? `增量扫描骨架已创建:保留 ${recipeSnapshot.count} 个 Recipe,${gapDimensions.length} 个维度需要补齐,正在后台填充...` : `增量扫描完成:保留 ${recipeSnapshot.count} 个 Recipe,所有维度已充分覆盖。`, meta: { tool: 'autosnippet_rescan', responseTimeMs: Date.now() - t0 }, }); }