autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
1,418 lines • 72.7 kB
JavaScript
/**
* orchestrator.js — 内部 Agent AI-First Bootstrap 管线
*
* ⚠️ 本文件是「内部 Agent」专用 — 由 bootstrap.js Phase 5 调用。
* 外部 Agent (Cursor/Copilot) 不经过此管线,它们自行分析代码。
*
* 核心架构: PipelineStrategy 驱动 (Analyze → QualityGate → Produce → RejectionGate)
*
* 1. Analyze 阶段: 自由探索代码 (AST 工具 + 文件搜索)
* 2. QualityGate: 质量门控 (insightGateEvaluator)
* 3. Produce 阶段: 格式化输出 (submit_knowledge)
* 4. TierScheduler 分层并行执行
*
* @module pipeline/orchestrator
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { AgentMessage } from '#agent/AgentMessage.js';
import { ExplorationTracker } from '#agent/context/ExplorationTracker.js';
import { EpisodicConsolidator } from '#agent/domain/EpisodicConsolidator.js';
import { computeAnalystBudget } from '#agent/domain/insight-analyst.js';
import { MemoryCoordinator } from '#agent/memory/MemoryCoordinator.js';
import { MemoryEmbeddingStore } from '#agent/memory/MemoryEmbeddingStore.js';
import { PersistentMemory } from '#agent/memory/PersistentMemory.js';
import { SessionStore } from '#agent/memory/SessionStore.js';
import { PRESETS } from '#agent/presets.js';
import { getDimensionFocusKeywords } from '#domain/dimension/DimensionSop.js';
import Logger from '#infra/logging/Logger.js';
import { BootstrapEventEmitter } from '#service/bootstrap/BootstrapEventEmitter.js';
import { buildEvidenceStarters } from '../MissionBriefingBuilder.js';
import { generateSkill } from '../shared/skill-generator.js';
import { clearCheckpoints, loadCheckpoints, saveDimensionCheckpoint } from './checkpoint.js';
import { buildTierReflection, DIMENSION_CONFIGS_V3, getFullDimensionConfig, } from './dimension-configs.js';
import { DimensionContext, parseDimensionDigest } from './dimension-context.js';
import { IncrementalBootstrap } from './IncrementalBootstrap.js';
import { fillDimensionsMock } from './mock-pipeline.js';
import { TierScheduler } from './tier-scheduler.js';
const logger = Logger.getInstance();
// ──────────────────────────────────────────────────────────────────
// Panorama Context for strategyContext injection (M1 §5.2)
// ──────────────────────────────────────────────────────────────────
/**
* 从 panoramaResult 提取维度级全景上下文
* 注入 strategyContext.panorama,使 Agent 获得模块角色/层级/耦合/空白区信息
*/
function buildPanoramaContext(panoramaResult, _dimConfig) {
if (!panoramaResult) {
return null;
}
try {
const modules = panoramaResult.modules;
const layers = panoramaResult.layers;
const gaps = panoramaResult.gaps ?? [];
// 构建紧凑上下文
const layerNames = (layers?.levels ?? []).map((l) => `L${l.level}:${l.name}`).join(' → ');
const knownGaps = gaps.slice(0, 5).flatMap((g) => g.suggestedFocus ?? []);
// 找到与当前维度最相关的模块(如果 dimConfig 有 target 信息)
let moduleRole = null;
let moduleLayer = null;
let moduleCoupling = null;
if (modules instanceof Map && modules.size > 0) {
// 使用第一个模块作为代表(维度覆盖多模块时)
const firstMod = modules.values().next().value;
if (firstMod) {
moduleRole = firstMod.refinedRole ?? firstMod.inferredRole ?? null;
moduleLayer = firstMod.layer ?? null;
moduleCoupling = {
fanIn: firstMod.fanIn ?? 0,
fanOut: firstMod.fanOut ?? 0,
};
}
}
return {
moduleRole,
moduleLayer,
moduleCoupling,
knownGaps: [...new Set(knownGaps)],
layerContext: layerNames || null,
};
}
catch {
return null;
}
}
// ──────────────────────────────────────────────────────────────────
// fillDimensionsV3 — v3.0 管线入口
// ──────────────────────────────────────────────────────────────────
/**
* fillDimensionsV3 — v3.0 AI-First 维度填充管线
*
* @param view PipelineFillView — 从 handler 传入的类型化视图
* @param dimensions 当前运行的维度列表(rescan 可能是 gap 子集)
*/
export async function fillDimensionsV3(view, dimensions) {
const { snapshot, projectRoot } = view;
const ctx = view.ctx;
// 从 snapshot 提取 orchestrator 所需字段
const depGraphData = snapshot.dependencyGraph;
const guardAudit = snapshot.guardAudit;
const primaryLang = snapshot.language.primaryLang ?? 'unknown';
const astProjectSummary = snapshot.ast;
const incrementalPlan = snapshot.incrementalPlan;
const panoramaResult = snapshot.panorama;
const callGraphResult = snapshot.callGraph;
const existingRecipes = view.existingRecipes ?? null;
const targetFileMap = view.targetFileMap;
// 从 ctx 获取运行时服务
let taskManager = null;
try {
taskManager = ctx.container.get('bootstrapTaskManager');
}
catch {
/* not available */
}
const sessionId = view.bootstrapSession?.id ?? '';
const sessionAbortSignal = taskManager?.getSessionAbortSignal?.() ?? null;
const isIncremental = incrementalPlan?.canIncremental && incrementalPlan?.mode === 'incremental';
const emitter = new BootstrapEventEmitter(ctx.container);
logger.info(`[Insight-v3] ═══ fillDimensionsV3 entered — ${isIncremental ? 'INCREMENTAL' : 'FULL'} pipeline`);
let allFiles = snapshot.allFiles;
// ═══════════════════════════════════════════════════════════
// Step 0: AI 可用性检查 (v7.2: 使用 AgentFactory + AiProviderManager)
// ═══════════════════════════════════════════════════════════
let agentFactory = null;
let isMockMode = false;
try {
// 通过 AiProviderManager 统一检查 mock 模式
const manager = ctx.container.singletons?._aiProviderManager;
isMockMode = manager?.isMock ?? false;
if (!isMockMode) {
agentFactory = ctx.container.get('agentFactory');
}
}
catch {
/* not available */
}
if (!agentFactory && !isMockMode) {
logger.error('[Insight-v3] AI Provider not available — bootstrap requires AI');
emitter.emitProgress('bootstrap:ai-unavailable', {
message: 'AI Provider 不可用,Bootstrap 需要 AI 才能运行。请先配置 AI Provider(如 OpenAI、Anthropic 等)后重试。',
});
for (const dim of dimensions) {
emitter.emitDimensionComplete(dim.id, { type: 'skipped', reason: 'ai-unavailable' });
}
return;
}
// Mock AI: 走 mock-pipeline 轻量管线
if (isMockMode) {
logger.info('[Insight-v3] Mock AI detected — routing to mock-pipeline');
await fillDimensionsMock(view, dimensions);
return;
}
// ═══════════════════════════════════════════════════════════
// Step 0.5: 构建 ProjectGraph
// ═══════════════════════════════════════════════════════════
let projectGraph = null;
try {
projectGraph =
(await ctx.container.buildProjectGraph?.(projectRoot, {
maxFiles: 500,
timeoutMs: 15_000,
})) ?? null;
if (projectGraph) {
const overview = await projectGraph.getOverview();
logger.info(`[Insight-v3] ProjectGraph: ${overview.totalClasses} classes, ${overview.totalProtocols} protocols (${overview.buildTimeMs}ms)`);
}
}
catch (e) {
logger.warn(`[Insight-v3] ProjectGraph build failed: ${e instanceof Error ? e.message : String(e)}`);
}
// ═══════════════════════════════════════════════════════════
// Step 1: 构建 Agents + 上下文
// ═══════════════════════════════════════════════════════════
logger.info('[Insight-v7] Using unified AgentRuntime pipeline (no legacy Analyst/Producer wrappers)');
// 注入文件缓存到容器 (v7.2: 通过容器传递)
ctx.container.singletons._fileCache = allFiles;
// 项目信息
const projectInfo = {
name: path.basename(projectRoot),
lang: primaryLang || 'unknown',
fileCount: allFiles?.length || 0,
};
// 跨维度上下文 (保留 DimensionContext 用于兼容)
const dimContext = new DimensionContext({
projectName: projectInfo.name,
primaryLang: projectInfo.lang,
fileCount: projectInfo.fileCount,
targetCount: Object.keys(targetFileMap || {}).length,
modules: Object.keys(targetFileMap || {}),
depGraph: depGraphData ?? undefined,
astMetrics: astProjectSummary?.projectMetrics ?? undefined,
guardSummary: guardAudit?.summary ?? undefined,
});
// v4.0: SessionStore — 替代 EpisodicMemory + ToolResultCache
// v5.0: 增量模式下从快照恢复已完成维度的记忆
let sessionStore;
if (isIncremental && incrementalPlan.restoredEpisodic) {
sessionStore = incrementalPlan.restoredEpisodic;
const restoredDims = sessionStore.getCompletedDimensions();
logger.info(`[Insight-v3] Restored SessionStore: ${restoredDims.length} dims [${restoredDims.join(', ')}]`);
// 同步恢复 DimensionContext 的 digests (兼容)
for (const dimId of restoredDims) {
const report = sessionStore.getDimensionReport(dimId);
if (report?.digest) {
dimContext.addDimensionDigest(dimId, report.digest);
}
}
}
else {
sessionStore = new SessionStore({
projectName: projectInfo.name,
primaryLang: projectInfo.lang,
fileCount: projectInfo.fileCount,
modules: Object.keys(targetFileMap || {}),
});
}
// v4.1: PersistentMemory — 项目级永久语义记忆 (Tier 3)
// 加载历史 bootstrap 记忆 → 注入 Analyst promptBuilder
let semanticMemory = null;
try {
const db = ctx.container.get('database');
if (db) {
// Phase 4: 将 EmbedProvider 作为 embeddingFn 注入 PersistentMemory
let embeddingFn;
try {
const ep = ctx.container.singletons?._embedProvider ?? ctx.container.singletons?.aiProvider;
if (ep && typeof ep.embed === 'function') {
const provider = ep;
embeddingFn = async (text) => {
const result = await provider.embed(text);
return result;
};
}
}
catch {
// EmbedProvider 不可用时 fallback 到无向量模式
}
semanticMemory = new PersistentMemory(db, {
logger,
embeddingFn,
embeddingStore: new MemoryEmbeddingStore(projectRoot),
});
const smStats = semanticMemory.getStats();
if (smStats.total > 0) {
logger.info(`[Insight-v3] Loaded ${smStats.total} semantic memories from previous bootstrap ` +
`(fact: ${smStats.byType.fact || 0}, insight: ${smStats.byType.insight || 0}, preference: ${smStats.byType.preference || 0})`);
}
}
}
catch (smErr) {
logger.warn(`[Insight-v3] SemanticMemory init failed (non-blocking): ${smErr instanceof Error ? smErr.message : String(smErr)}`);
}
// Phase E: CodeEntityGraph — 代码实体关系图谱 (供 Analyst prompt 注入)
let codeEntityGraphInst = null;
try {
const { CodeEntityGraph } = await import('#service/knowledge/CodeEntityGraph.js');
const entityRepo = ctx.container.get('codeEntityRepository');
const edgeRepo = ctx.container.get('knowledgeEdgeRepository');
if (entityRepo && edgeRepo) {
codeEntityGraphInst = new CodeEntityGraph(entityRepo, edgeRepo, { projectRoot, logger });
const topo = await codeEntityGraphInst.getTopology();
if (topo.totalEntities > 0) {
logger.info(`[Insight-v3] CodeEntityGraph: ${topo.totalEntities} entities, ${topo.totalEdges} edges`);
}
}
}
catch (cegErr) {
logger.warn(`[Insight-v3] CodeEntityGraph init failed (non-blocking): ${cegErr instanceof Error ? cegErr.message : String(cegErr)}`);
}
// v5.0: MemoryCoordinator — 统一记忆协调器 (会话级)
const memoryCoordinator = new MemoryCoordinator({
persistentMemory: semanticMemory,
sessionStore,
mode: 'bootstrap',
});
// ═══════════════════════════════════════════════════════════
// Step 2: 按维度分层执行 (Analyst → Gate → Producer)
// ═══════════════════════════════════════════════════════════
const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '3', 10);
const enableParallel = process.env.ASD_PARALLEL_BOOTSTRAP !== 'false';
const scheduler = new TierScheduler();
// 包含所有维度(含 Enhancement Pack 动态追加的维度)
const activeDimIds = dimensions.map((d) => d.id);
// v5.0: 增量模式 — 仅执行受影响维度, 跳过未变更维度
const incrementalSkippedDims = [];
if (isIncremental) {
const affected = new Set(incrementalPlan.affectedDimensions);
for (const dimId of activeDimIds) {
if (!affected.has(dimId) && incrementalPlan.skippedDimensions.includes(dimId)) {
incrementalSkippedDims.push(dimId);
// 标记为已完成 (使用历史结果)
emitter.emitDimensionComplete(dimId, {
type: 'incremental-restored',
reason: 'no-change-detected',
});
}
}
if (incrementalSkippedDims.length > 0) {
logger.info(`[Insight-v3] ⏩ Incremental skip: [${incrementalSkippedDims.join(', ')}] ` +
`(using historical results)`);
}
}
logger.info(`[Insight-v3] Active dimensions: [${activeDimIds.join(', ')}], concurrency=${enableParallel ? concurrency : 1}${isIncremental ? `, incremental skip: [${incrementalSkippedDims.join(', ')}]` : ''}`);
// ── P3: 断点续传 — 加载有效 checkpoints ──
const completedCheckpoints = await loadCheckpoints(projectRoot);
const skippedDims = [];
for (const [dimId, checkpoint] of completedCheckpoints) {
if (activeDimIds.includes(dimId)) {
// 恢复 DimensionContext 中的 digest
if (checkpoint.digest) {
dimContext.addDimensionDigest(dimId, checkpoint.digest);
// v4.0: 同步恢复到 SessionStore
sessionStore.addDimensionDigest(dimId, checkpoint.digest);
}
emitter.emitDimensionComplete(dimId, {
type: 'checkpoint-restored',
...checkpoint,
});
skippedDims.push(dimId);
logger.info(`[Insight-v3] ⏩ 跳过已完成维度 (checkpoint): "${dimId}"`);
}
}
const candidateResults = { created: 0, failed: 0, errors: [] };
const dimensionCandidates = {};
const dimensionStats = {}; // P4.2: 维度级统计
// ── 跨维度去重集合 (实例级持久化,等效旧 ChatAgent.#globalSubmittedTitles/Patterns) ──
const globalSubmittedTitles = new Set();
const globalSubmittedPatterns = new Set();
const globalSubmittedTriggers = new Set();
// ── Rescan 模式: 预种已有 recipe 标题到去重集合,防止重复创建 ──
const existingRecipesList = existingRecipes;
if (existingRecipesList && existingRecipesList.length > 0) {
for (const r of existingRecipesList) {
// ★ 只预种 healthy 标题,decaying 的允许被替代
if (r.title && r.status !== 'decaying') {
globalSubmittedTitles.add(r.title.toLowerCase().trim());
}
// ★ trigger 全部预种(含 decaying),防止 trigger 冲突
if (r.trigger) {
globalSubmittedTriggers.add(r.trigger.toLowerCase().trim());
}
}
logger.info(`[Insight-v3] Rescan mode: seeded ${globalSubmittedTitles.size} titles + ${globalSubmittedTriggers.size} triggers into dedup set`);
}
// 构建 rescanContext 供 Analyst/Producer prompt 使用(区分 healthy/decaying)
const rescanContext = existingRecipesList
? {
// healthy 列表 — 告诉 Agent "不要重复"
existingRecipes: existingRecipesList.filter((r) => r.status !== 'decaying'),
// decaying 列表 — 告诉 Agent "可以替换"
decayingRecipes: existingRecipesList.filter((r) => r.status === 'decaying'),
// trigger 仍全部占用(含 decaying)
occupiedTriggers: existingRecipesList.map((r) => r.trigger).filter(Boolean),
coverageByDim: existingRecipesList.reduce((acc, r) => {
// 只有 healthy 计入覆盖
if (r.status !== 'decaying') {
const dim = r.knowledgeType || 'unknown';
acc[dim] = (acc[dim] || 0) + 1;
}
return acc;
}, {}),
}
: null;
/** 执行单个维度: Analyst → Gate → Producer */
async function executeDimension(dimId) {
// v5.0: 增量模式 — 跳过未受影响的维度 (使用历史 EpisodicMemory)
if (incrementalSkippedDims.includes(dimId)) {
const report = sessionStore.getDimensionReport(dimId);
const dimResult = {
candidateCount: report?.candidatesSummary?.length || 0,
rejectedCount: 0,
analysisChars: report?.analysisText?.length || 0,
referencedFiles: report?.referencedFiles?.length || 0,
referencedFilesList: report?.referencedFiles || [],
durationMs: 0,
toolCallCount: 0,
tokenUsage: { input: 0, output: 0 },
skipped: true,
restoredFromIncremental: true,
};
dimensionStats[dimId] = dimResult;
logger.info(`[Insight-v3] ⏩ "${dimId}" — incremental skip (historical result)`);
return dimResult;
}
// P3: 跳过已有 checkpoint 的维度
if (skippedDims.includes(dimId)) {
const cp = completedCheckpoints.get(dimId);
const cpResult = {
candidateCount: cp?.candidateCount || 0,
rejectedCount: cp?.rejectedCount || 0,
analysisChars: cp?.analysisChars || 0,
referencedFiles: cp?.referencedFiles || 0,
durationMs: cp?.durationMs || 0,
toolCallCount: cp?.toolCallCount || 0,
tokenUsage: cp?.tokenUsage || { input: 0, output: 0 },
skipped: true,
restoredFromCheckpoint: true,
};
// P4.2: 将恢复的维度也记入统计
dimensionStats[dimId] = cpResult;
candidateResults.created += cpResult.candidateCount;
// P3+: 恢复 analysisText 到 dimensionCandidates + EpisodicMemory 供 Skill 生成
if (cp?.analysisText) {
const restoredFiles = cp.referencedFilesList || [];
dimensionCandidates[dimId] = {
analysisReport: {
analysisText: cp.analysisText,
referencedFiles: restoredFiles,
findings: [],
metadata: {},
},
producerResult: { candidateCount: cp.candidateCount || 0, toolCalls: [] },
};
sessionStore.storeDimensionReport(dimId, {
analysisText: cp.analysisText,
findings: [],
referencedFiles: restoredFiles,
candidatesSummary: [],
});
logger.info(`[Insight-v3] ✅ Checkpoint "${dimId}": analysisText restored (${cp.analysisText.length} chars) — Skill generation enabled`);
}
return cpResult;
}
const dim = dimensions.find((d) => d.id === dimId);
if (!dim) {
return { candidateCount: 0, error: 'dimension not found' };
}
// 合并 v3 配置和原始维度配置 — 优先使用 getFullDimensionConfig()
// Enhancement Pack 动态维度可能不在 DIMENSION_CONFIGS_V3 中 — 从 dim 本身构建配置
const fullConfig = getFullDimensionConfig(dimId);
const v3Config = DIMENSION_CONFIGS_V3[dimId];
const dimConfig = fullConfig
? {
...fullConfig,
// focusKeywords: 用于 EpisodicMemory 跨维度 findings 相关性匹配
focusKeywords: fullConfig.focusKeywords || [],
}
: v3Config
? {
...v3Config,
id: dimId,
label: dim.label,
guide: dim.guide || '',
focusKeywords: getDimensionFocusKeywords(dimId, dim.guide || ''),
skillWorthy: dim.skillWorthy,
dualOutput: dim.dualOutput,
skillMeta: dim.skillMeta,
knowledgeTypes: dim.knowledgeTypes || v3Config.allowedKnowledgeTypes,
}
: {
id: dimId,
label: dim.label,
guide: dim.guide || '',
focusKeywords: getDimensionFocusKeywords(dimId, dim.guide || ''),
outputType: dim.dualOutput ? 'dual' : dim.skillWorthy ? 'skill' : 'candidate',
allowedKnowledgeTypes: dim.knowledgeTypes || [],
skillWorthy: dim.skillWorthy,
dualOutput: dim.dualOutput,
skillMeta: dim.skillMeta,
knowledgeTypes: dim.knowledgeTypes || [],
};
// Session 有效性检查
if (taskManager && !taskManager.isSessionValid(sessionId)) {
logger.warn(`[Insight-v3] Session superseded — skipping "${dimId}"`);
return { candidateCount: 0, error: 'session-superseded' };
}
emitter.emitDimensionStart(dimId);
logger.info(`[Insight-v3] ── Dimension "${dimId}" (${dimConfig.label}) ──`);
const dimStartTime = Date.now();
try {
// ═══ v3.0: 增强 PipelineStrategy 驱动 ═══
const analystScopeId = `${dimId}:analyst`;
memoryCoordinator.createDimensionScope(analystScopeId);
const v3OutputType = DIMENSION_CONFIGS_V3[dimId]?.outputType;
const needsCandidates = v3OutputType
? v3OutputType !== 'skill'
: !dimConfig.skillWorthy || dimConfig.dualOutput;
// ── 获取 Preset 的标准 stages 配置作为基础 ──
const presetStages = PRESETS.insight.strategy.stages;
const evolutionPresetStages = PRESETS.evolution.strategy.stages;
// ── 判断当前维度是否有现有 Recipe(按维度过滤后) ──
const dimExistingRecipes = [
...(rescanContext?.existingRecipes?.filter((r) => r.knowledgeType === dimId) ?? []),
...(rescanContext?.decayingRecipes?.filter((r) => r.knowledgeType === dimId) ?? []),
];
const hasExistingRecipes = dimExistingRecipes.length > 0;
// ── 构建 per-dimension 的 stages ──
// NOTE: onToolCall 不再注入 ac.recordToolCall — ToolExecutionPipeline 的
// traceRecord 中间件已通过 loopCtx.trace 统一记录,避免同一 AC 上双重记录。
const analyzeStage = {
...presetStages[0],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- pipeline stage shapes from PRESETS; needs PipelineStage interface (T4)
let stages;
if (needsCandidates) {
const produceStage = {
...presetStages[2],
promptBuilder: (ctx) => {
memoryCoordinator.allocateBudget('producer');
return presetStages[2].promptBuilder?.(ctx);
},
};
if (hasExistingRecipes) {
// 当前维度有旧 Recipe: Evolve→EvolutionGate→Analyze→QualityGate→Produce→RejectionGate
stages = [
evolutionPresetStages[0], // evolve
evolutionPresetStages[1], // evolution_gate
analyzeStage,
presetStages[1], // quality_gate
produceStage,
presetStages[3], // rejection_gate
];
}
else {
// 当前维度无旧 Recipe: Analyze→QualityGate→Produce→RejectionGate
stages = [
analyzeStage,
presetStages[1], // quality_gate
produceStage,
presetStages[3], // rejection_gate
];
}
}
else {
// Skill-only 维度: 仅 Analyze
stages = [analyzeStage];
}
// ── 创建 Runtime (使用增强 PipelineStrategy) ──
const runtime = agentFactory.createRuntime('insight', {
lang: primaryLang || projectInfo.lang || null,
strategy: {
type: 'pipeline',
maxRetries: 1,
stages,
},
});
runtime.setFileCache(allFiles);
// ── 构建消息 + strategyContext ──
const message = AgentMessage.internal(`Bootstrap dimension: ${dimConfig.label}`, {
sessionId,
dimension: dimId,
phase: 'bootstrap',
});
const strategyContext = {
dimConfig,
projectInfo,
dimContext,
sessionStore,
semanticMemory,
codeEntityGraph: codeEntityGraphInst,
projectGraph: null, // ProjectGraph 在 orchestrator 级别可用时注入
dimId,
activeContext: memoryCoordinator.getActiveContext(analystScopeId),
outputType: dimConfig.outputType || 'analysis',
// §M1: Panorama 全景上下文 (Phase 1.8 数据注入)
panorama: buildPanoramaContext(panoramaResult, dimConfig),
// §ES: Evidence Starters — 从 Phase 1-4 数据提取维度级证据启发
evidenceStarters: buildEvidenceStarters(dimConfig, {
astData: astProjectSummary,
guardAudit,
depGraphData,
callGraphResult,
panoramaResult,
}),
// §R1: Rescan 模式 — 已有 recipe 上下文 (避免重复分析/创建)
rescanContext: rescanContext
? {
existingRecipes: rescanContext.existingRecipes.filter((r) => r.knowledgeType === dimId),
decayingRecipes: rescanContext.decayingRecipes.filter((r) => r.knowledgeType === dimId),
occupiedTriggers: rescanContext.occupiedTriggers,
gap: Math.max(0, 5 - (rescanContext.coverageByDim[dimId] || 0)),
existing: rescanContext.coverageByDim[dimId] || 0,
}
: null,
// §EVO: Evolution Stage — 当前维度全部现有 Recipe(healthy + decaying),附带 audit hint
existingRecipes: dimExistingRecipes.map((r) => ({
id: r.id,
title: r.title,
trigger: r.trigger,
content: r.content,
sourceRefs: r.sourceRefs,
auditHint: 'auditScore' in r && r.auditScore != null
? {
relevanceScore: r.auditScore,
verdict: r.status === 'decaying' ? 'decay' : 'watch',
evidence: r.auditEvidence ?? {},
decayReasons: r.decayReason
? [String(r.decayReason)]
: [],
}
: null,
})),
// Evolution 上下文字段 — buildEvolverPrompt 直接从 strategyContext 读取
dimensionId: dimId,
dimensionLabel: dimConfig.label,
projectOverview: {
primaryLang: primaryLang || projectInfo.lang || 'unknown',
fileCount: projectInfo.fileCount || 0,
modules: Object.keys(targetFileMap || {}),
},
// ── 引擎增强参数 (PipelineStrategy → reactLoop 透传) ──
contextWindow: agentFactory?.createContextWindow({ isSystem: true }),
// B1 fix: 分析阶段使用 analyst 策略 (SCAN→EXPLORE→VERIFY→SUMMARIZE)
// B3 fix: 透传完整 ANALYST_BUDGET
// B4 fix: 自适应预算 — 根据项目文件数缩放 maxIterations (24~40)
tracker: ExplorationTracker.resolve({ source: 'system', strategy: 'analyst' }, computeAnalystBudget(projectInfo.fileCount || 0)),
trace: memoryCoordinator.getActiveContext(analystScopeId),
memoryCoordinator,
sharedState: {
submittedTitles: globalSubmittedTitles,
submittedPatterns: globalSubmittedPatterns,
submittedTriggers: globalSubmittedTriggers,
_dimensionMeta: {
id: dimId,
outputType: dimConfig.outputType || 'candidate',
allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
},
_projectLanguage: primaryLang || projectInfo.lang || null,
_dimensionScopeId: analystScopeId,
},
source: 'system',
};
// ── 执行 ──
// 外层超时 = 安全网 (各阶段已有独立超时: Analyst 300s + Producer 180s + 硬缓冲 60s)
const outerTimeoutMs = 3_600_000; // 1 小时——维度分析本身耗时长
const runResult = await Promise.race([
runtime.execute(message, { strategyContext, abortSignal: sessionAbortSignal }),
new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap runtime timeout for "${dimId}"`)), outerTimeoutMs)),
]);
// ── 提取结果 ──
const analyzeResult = runResult?.phases?.analyze;
const gateResult = runResult?.phases?.quality_gate;
const produceResult = runResult?.phases?.produce;
const analysisText = (analyzeResult?.reply || runResult?.reply || '').trim();
const artifact = gateResult?.artifact || {
analysisText,
referencedFiles: [],
findings: [],
metadata: { toolCallCount: 0 },
};
const runtimeToolCalls = runResult?.toolCalls || [];
const combinedTokenUsage = runResult?.tokenUsage || { input: 0, output: 0 };
// 引用文件: 优先从 artifact 取, 回退从 toolCalls 提取
const referencedFiles = artifact.referencedFiles?.length > 0
? artifact.referencedFiles
: [
...new Set(runtimeToolCalls.flatMap((tc) => {
const a = tc?.args || tc?.params || {};
const files = [];
if (typeof a.filePath === 'string' && a.filePath.trim()) {
files.push(a.filePath.trim());
}
if (Array.isArray(a.filePaths)) {
for (const f of a.filePaths) {
if (typeof f === 'string' && f.trim()) {
files.push(f.trim());
}
}
}
return files;
})),
];
const analysisReport = {
dimensionId: dimId,
analysisText: artifact.analysisText || analysisText,
findings: artifact.findings || [],
referencedFiles,
evidenceMap: artifact.evidenceMap || null,
negativeSignals: artifact.negativeSignals || [],
metadata: {
toolCallCount: runtimeToolCalls.length,
tokenUsage: combinedTokenUsage,
artifactVersion: artifact.metadata?.artifactVersion || 1,
},
};
// ── Producer 结果统计 ──
const submitCalls = runtimeToolCalls.filter((tc) => {
const tool = tc?.tool || tc?.name;
return tool === 'submit_knowledge' || tool === 'submit_with_check';
});
const successCount = submitCalls.filter((tc) => {
const res = tc?.result;
if (!res) {
return true;
}
if (typeof res === 'string') {
return !res.includes('rejected') && !res.includes('error');
}
return (res.status !== 'rejected' &&
res.status !== 'error');
}).length;
const rejectedCount = submitCalls.length - successCount;
const producerResult = {
candidateCount: needsCandidates ? successCount : 0,
rejectedCount: needsCandidates ? rejectedCount : 0,
toolCalls: runtimeToolCalls,
reply: produceResult?.reply || analysisText,
tokenUsage: combinedTokenUsage,
};
candidateResults.created += producerResult.candidateCount;
dimensionCandidates[dimId] = { analysisReport, producerResult };
// ── Producer 阶段详细日志 ──
if (needsCandidates) {
const producerToolCalls = produceResult?.toolCalls || [];
const producerToolNames = producerToolCalls.map((tc) => tc?.tool || tc?.name || 'unknown');
const toolBreakdown = {};
for (const name of producerToolNames) {
toolBreakdown[name] = (toolBreakdown[name] || 0) + 1;
}
const breakdownStr = Object.entries(toolBreakdown)
.map(([k, v]) => `${k}=${v}`)
.join(', ');
logger.info(`[Producer] "${dimId}": submitted=${submitCalls.length}, accepted=${successCount}, rejected=${rejectedCount}, ` +
`producerToolCalls=${producerToolCalls.length} (${breakdownStr || 'none'}), ` +
`analysisInput=${analysisText.length} chars`);
if (successCount === 0 && submitCalls.length === 0) {
logger.warn(`[Producer] "${dimId}": ⚠ Producer 未提交任何候选。` +
`分析文本=${analysisText.length} chars, findings=${(analysisReport.findings || []).length}, ` +
`producerIterations=${producerToolCalls.length}, degraded=${runResult?.degraded || false}`);
}
}
// ── Memory Update ──
const ac = memoryCoordinator.getActiveContext(analystScopeId);
const distilled = ac
? ac.distill()
: { keyFindings: [], totalObservations: 0, toolCallSummary: [] };
sessionStore.storeDimensionReport(dimId, {
analysisText: analysisReport.analysisText,
findings: analysisReport.findings.length > 0 ? analysisReport.findings : distilled.keyFindings,
referencedFiles: analysisReport.referencedFiles || [],
candidatesSummary: [],
workingMemoryDistilled: distilled,
});
logger.info(`[Insight-v3] Dimension "${dimId}": analysis=${analysisReport.analysisText.length} chars, ` +
`files=${analysisReport.referencedFiles.length}, findings=${(analysisReport.findings || distilled.keyFindings).length}, ` +
`toolCalls=${runtimeToolCalls.length}, degraded=${runResult?.degraded || false} (${Date.now() - dimStartTime}ms)`);
// ── Token 用量持久化 (fire-and-forget) ──
try {
const tokenStore = ctx.container?.get?.('tokenUsageStore');
if (tokenStore) {
const aiProv = runtime.aiProvider;
tokenStore.record({
source: 'system',
dimension: dimId,
provider: aiProv?.name || null,
model: aiProv?.model || null,
inputTokens: combinedTokenUsage.input || 0,
outputTokens: combinedTokenUsage.output || 0,
durationMs: Date.now() - dimStartTime,
toolCalls: runtimeToolCalls.length,
sessionId: sessionId || null,
});
try {
const realtime = ctx.container?.get?.('realtimeService');
realtime?.broadcastTokenUsageUpdated?.();
}
catch {
/* optional */
}
}
}
catch {
/* token logging should never break execution */
}
// ── v5.1: analysisText 过短补强 ──
if (needsCandidates && analysisReport.analysisText.length < 100) {
const findings = analysisReport.findings || [];
if (findings.length >= 3) {
const dimLabel = dimConfig.label || dimId;
const synthesized = [
`## ${dimLabel}`,
'',
analysisReport.analysisText.trim(),
'',
'### 关键发现',
'',
...findings.slice(0, 10).map((f, i) => {
const text = typeof f === 'string' ? f : f.finding;
return `${i + 1}. ${text}`;
}),
];
const memDistilled = distilled;
if (memDistilled?.toolCallSummary?.length > 0) {
synthesized.push('', '### 探索记录', '');
for (const s of memDistilled.toolCallSummary.slice(0, 10)) {
synthesized.push(`- ${s}`);
}
}
const originalLen = analysisReport.analysisText.length;
analysisReport.analysisText = synthesized.join('\n');
logger.info(`[Insight-v3] analysisText 补强 "${dimId}": ${originalLen} → ${analysisReport.analysisText.length} chars ` +
`(from ${findings.length} findings)`);
}
}
// ── DimensionDigest ──
const digest = parseDimensionDigest(producerResult.reply) || {
summary: `v3 分析: ${analysisReport.analysisText.substring(0, 200)}...`,
candidateCount: producerResult.candidateCount,
keyFindings: [],
crossRefs: {},
gaps: [],
};
dimContext.addDimensionDigest(dimId, digest);
sessionStore.addDimensionDigest(dimId, digest);
// 候选摘要记录到 DimensionContext + SessionStore
for (const tc of producerResult.toolCalls || []) {
const tool = tc.tool || tc.name;
if (tool === 'submit_knowledge' || tool === 'submit_with_check') {
const args = tc.params || tc.args || {};
const candidateSummary = {
title: String(args.title || ''),
subTopic: String(args.category || ''),
summary: String(args.summary || ''),
};
dimContext.addSubmittedCandidate(dimId, candidateSummary);
sessionStore.addSubmittedCandidate(dimId, candidateSummary);
}
}
emitter.emitDimensionComplete(dimId, {
type: needsCandidates ? 'candidate' : 'skill',
extracted: producerResult.candidateCount,
created: producerResult.candidateCount,
status: 'v3-pipeline-complete',
degraded: runResult?.degraded || false,
durationMs: Date.now() - dimStartTime,
toolCallCount: runtimeToolCalls.length,
source: 'enhanced-pipeline-strategy',
});
const dimTokenUsage = combinedTokenUsage;
const qualityScores = artifact.qualityReport;
const dimResult = {
candidateCount: producerResult.candidateCount,
rejectedCount: producerResult.rejectedCount || 0,
analysisChars: analysisReport.analysisText.length,
referencedFiles: analysisReport.referencedFiles.length,
durationMs: Date.now() - dimStartTime,
toolCallCount: runtimeToolCalls.length,
tokenUsage: dimTokenUsage,
analysisText: analysisReport.analysisText,
referencedFilesList: analysisReport.referencedFiles || [],
qualityGate: qualityScores
? {
totalScore: qualityScores.totalScore,
scores: qualityScores.scores,
action: gateResult?.action || (runResult?.degraded ? 'degrade' : 'pass'),
}
: null,
};
dimensionStats[dimId] = dimResult;
// P3: 保存 checkpoint — 仅当有实质分析内容时(避免 degraded/空结果污染后续 run)
if (analysisReport.analysisText.length >= 50) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- digest shape compatible at runtime
await saveDimensionCheckpoint(projectRoot, sessionId, dimId, dimResult, digest);
}
else {
logger.warn(`[Insight-v3] ⚠ 跳过 checkpoint 保存: "${dimId}" analysisText 过短 (${analysisReport.analysisText.length} chars)`);
}
return dimResult;
}
catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
logger.error(`[Insight-v3] Dimension "${dimId}" failed: ${errMsg}`);
candidateResults.errors.push({ dimId, error: errMsg });
emitter.emitDimensionComplete(dimId, { type: 'error', reason: errMsg });
return { candidateCount: 0, error: errMsg };
}
}
// ═══════════════════════════════════════════════════════════
// Step 3: 执行 (并行 or 串行)
// ═══════════════════════════════════════════════════════════
const t0 = Date.now();
// tierHints: Enhancement Pack 维度通过 tierHint 字段声明首选 Tier
const tierHints = {};
for (const dim of dimensions) {
if (typeof dim.tierHint === 'number') {
tierHints[dim.id] = dim.tierHint;
}
}
if (enableParallel) {
const results = await scheduler.execute(executeDimension, {
concurrency,
activeDimIds,
tierHints,
shouldAbort: () => !!(taskManager && !taskManager.isSessionValid(sessionId)),
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- DimensionStat structurally compatible with scheduler's DimensionResult
onTierComplete: (tierIndex, tierResults) => {
const tierStats = [...tierResults.values()];
const totalCandidates = tierStats.reduce((s, r) => s + (r.candidateCount || 0), 0);
logger.info(`[Insight-v3] Tier ${tierIndex + 1} complete: ${tierResults.size} dimensions, ${totalCandidates} candidates`);
// v4.0: Tier 级 Reflection — 综合本 Tier 所有维度的发现
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- SessionStore structurally compatible
const reflection = buildTierReflection(tierIndex, tierResults, sessionStore);
sessionStore.addTierReflection(tierIndex, reflection);
logger.info(`[Insight-v3] Tier ${tierIndex + 1} reflection: ` +
`${reflection.topFindings.length} top findings, ` +
`${reflection.crossDimensionPatterns.length} patterns`);
}
catch (refErr) {
logger.warn(`[Insight-v3] Tier ${tierIndex + 1} reflection failed: ${refErr instanceof Error ? refErr.message : String(refErr)}`);
}
},
});
logger.info(`[Insight-v3] All tiers complete: ${results.size} dimensions in ${Date.now() - t0}ms`);
// v4.0: 记录 SessionStore 统计
const emStats = sessionStore.getStats();
logger.info(`[Insight-v3] Memory stats: ${emStats.completedDimensions} dims, ` +
`${emStats.totalFindings} findings, ${emStats.referencedFiles} files, ` +
`${emStats.crossReferences} cross-refs, ${emStats.tierReflections} reflections`);
if (emStats.cache) {
logger.info(`[Insight-v3] Cache stats: ${emStats.cache.hitRate} hit rate, ` +
`${emStats.cache.searchCacheSize} searches, ${emStats.cache.fileCacheSize} files`);
}
}
else {
// 串行: 按 TierScheduler 内部顺序逐个执行
for (const tier of scheduler.getTiers()) {
for (const dimId of tier) {
if (!activeDimIds.includes(dimId)) {
continue;
}
if (taskManager && !taskManager.isSessionValid(sessionId)) {
break;
}
await executeDimension(dimId);
}
}
logger.info(`[Insight-v3] Serial execution complete in ${Date.now() - t0}ms`);
}
// ═══════════════════════════════════════════════════════════
// Step 4: Project Skill 生成 (skillWorthy 维度)
//
// v3: 直接使用 Analyst 的分析文本作为 Skill 内容
// 使用 shared/skill-generator.js 统一质量门控和内容构建
// ═══════════════════════════════════════════════════════════
const skillResults = { created: 0, failed: 0, skills: [], errors: [] };
try {
for (const dim of dimensions) {
if (!dim.skillWorthy) {
continue;
}
const dimData = dimensionCandidates[dim.id];
if (!dimData?.analysisReport?.analysisText) {
continue;
}
if (taskManager && !taskManager.isSessionValid(sessionId)) {
break;
}
try {
const analysisText = dimData.analysisReport.analysisText;
const referencedFiles = dimData.analysisReport.referencedFiles || [];
// 从 SessionStore 获取结构化发现,供 Skill 生成使用
const dimReport = sessionStore.getDimensionReport(dim.id);
const keyFindings = (dimReport?.findings || [])
.sort((a, b) => (Number(b.importance) || 5) - (Number(a.importance) || 5))
.slice(0, 10)
.map((f) => String(f.finding || ''));
// 当 analysisText 过短(如 force-exit 时 AI 仅输出 JSON digest 被清洗后)
// 从 distilled findings 合成补充文本,避免 Skill 质量门控拦截
let effectiveText = analysisText;
if (analysisText.trim().length < 100 && keyFindings.length > 0) {
const distilled = dimReport?.workingMemoryDistilled;
const synthesized = [
`## ${dim.label || dim.id}`,
'',
analysisText.trim(),
'',
'## 关键发现',
'',
...keyFindings.map((f, i) => `${i + 1}. ${f}`),
];
if ((distilled?.toolCallSummary?.length ?? 0) > 0) {
synthesized.push('', '## 探索记录', '');
for (const s of distilled.toolCallSummary.slice(0, 10)) {
synthesized.push(`- ${s}`);
}
}
effectiveText = synthesized.join('\n');
logger.info(`[Insight-v3] Skill "${dim.id}": analysisText too short (${analysisText.trim().length} chars), ` +
`synthesized from ${keyFindings.length} findings → ${effectiveText.length} chars`);
}
const result = await generateSkill(ctx, dim, effectiveText, referencedFiles, keyFindings, 'bootstrap-v3');
if (result.success) {
skillResults.created++;
skillResults.skills.push(result.skillName);
emitter.emitDimensionComplete(dim.id, {
type: 'skill',
skillName: result.skillName,
sourceCount: referencedFiles.length,
});
}
else {
skillResults.failed++;
skillResults.errors.push({ dimId: dim.id, error: result.error ?? 'unknown' });
emitter.emitDimensionFailed(dim.id, new Error(result.error));
}
}
catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
logger.warn(`[Insight-v3] Skill generation failed for "${dim.id}": ${errMsg}`);
skillResults.failed++;
skillResults.errors.push({ dimId: dim.id, error: errMsg });
emitter.emitDimensionFailed(dim.id, err instanceof Error ? err : new Error(errMsg));
}
}
}
catch (e) {
logger.warn(`[Insight-v3] Skill generation module import failed: ${e instanceof Error ? e.message : String(e)}`);
}
// ═══════════════════════════════════════════════════════════
// Step 4.5: Candidate Relations → Code Entity Graph (Phase E)
//
// 将各维度 Producer 产出的候选关系写入代码实体图谱,
// 完善 inherits/calls/depends_on/data_flow 等语义边。
// ═══════════════════════════════════════════════════════════
try {
const { CodeEntityGraph } = await import('#service/knowledge/CodeEntityGraph.js');
const entityRepo = ctx.container.get('codeEntityRepository');
const edgeRepo = ctx.container.get('knowledgeEdgeRepository');
if (entityRepo && edgeRepo) {
const ceg = new CodeEntityGraph(entityRepo, edgeRepo, { projectRoot, logger });
// 收集所有维度产出的候选 (从 Producer toolCalls 中提取)
const allCandidates = [];
for (const dimData of Object.values(dimensionCandidates)) {
const toolCalls = dimData?.producerResult?.toolCalls || [];
for (const tc of toolCalls) {
const toolName = tc.tool || tc.name;
if (toolName === 'submit_knowledge' || toolName === 'submit_with_check') {
const params = tc.params || tc.args || {};
if (params.title) {
allCandidates.push({
title: params.title,
relations: params.relations || null,
});
}
}
}
}
if (allCandidates.length > 0) {
const relResult = await ceg.populateFromCandidateRelations(allCandidates);
logger.info(`[Insight-v3] Code Entity Graph relations: ${relResult.edgesCreated} edges from ${allCandidates.length} candidates (${relResult.durationMs}ms)`);
}
}
}
catch (cegErr) {
logger.warn(`[Insight-v3] Code Entity Graph relations failed (non-blocking): ${cegErr instanceof Error ? cegErr.message : String(cegErr)}`);
}
// ═══════════════════════════════════════════════════════════
// Step 5: Episodic → Semantic 固化 (Phase C)
//
// 将 EpisodicMemory 中的发现和洞察提炼为持久化的语义记忆,
// 存入 SQLite semantic_memories 表,供二次冷启动和日常对话使用。
// ═══════════════════════════════════════════════════════════
let consolidationResult = null;
try {
const db = ctx.container.get('database');
if (db) {
const semanticMemory = new PersistentMemory(db, {
logger,
embeddingStore: new MemoryEmbeddingStore(projectRoot),
});
const consolidator = new EpisodicConsolidator(semanticMemory, { logger });
consolidationResult = consolidator.consolidate(sessionStore, {
bootstrapSession: sessionId,
clearPrevious: true, // 全量冷启动: 先清除旧的 bootstrap 记忆
});
const smStats = semanticMemory.getStats();
logger.info(`[Insight-v3] Semantic Memory consolidation: ` +
`+${consolidationResult.total.added} ADD, ` +
`~${consolidationResult.total.updated} UPDATE, ` +
`⊕${consolidationResult.total.merged} MERGE | ` +
`Total: ${smStats.total} memories (avg importance: ${smStats.avgImportance})`);
// 按类型和来源分布
logger.info(`[Insight-v3] Memory by type: ${Object.entries(smStats.byType)
.map(([t, n]) => `${t}=${n}`)
.join(', ')} | ` +
`by source: ${Object.entries(smStats.bySource)
.map(([s, n]) => `${s}=${n}`)
.join(', ')}`);
// 蒸馏管线详情 (来自 EpisodicConsolidator 增强返回值)
const cr = consolidationResult;
if (cr.perDimension) {
const perDim = cr.perDimension;
const topDims = Object.entries(perDim)
.sort(([, a], [, b]) => b - a)
.slice(0, 6);
logger.info(`[Insight-v3] Memory per-dimension (top ${topDims.length}): ${topDims.map(([d, n]) => `${d}=${n}`).join(', ')}`);
}
if (cr.importanceDistribution) {
const hist = cr.importanceDistribution;
const histStr = Object.entries(hist)
.sort(([a], [b]) => Number(a) - Number(b))
.map(([k, v]) => `imp${k}=${v}`)
.join(' ');
logger.info(`[Insight-v3] Importance histogram: ${histStr}`);
}
}
else {
logger.warn('[Insight-v3] Database not available — skipping Semantic Memory consolidation');
}
}
catch (consolidateErr) {
logger.warn(`[Insight-v3] Semantic Memory consolidation failed (non-blocking): ${consolidateErr instanceof Error ? consolidateErr.message : String(consolidateErr)}`);
}
// ═══════════════════════════════════════════════════════════
// Summary + P4.2: Bootstrap Report
// ═══════════════════════════════════════════════════════════
const totalTimeMs = Date.now() - t0;
// P4.1: 汇总所有维度 token 用量
const totalTokenUsage = { input: 0, output: 0 };
const totalToolCalls = Object.values(dimensionStats).reduce((sum, s) => sum + (s.toolCallCount || 0), 0);
for (const stat of Object.values(dimensionStats)) {
if (stat.tokenUsage) {
totalTokenUsage.input += stat.tokenUsage.input || 0;
totalTokenUsage.output += stat.tokenUsage.output || 0;
}
}
logger.info([
`[Insight-v3] ═══ Pipeline complete ═══`,
isIncremental
? ` Mode: INCREMENTAL (${incrementalPlan.affectedDimensions.length} affected, ${incrementalSkippedDims.length} skipped)`
: '',
` Candidates: ${candidateResults.created} created, ${candidateResults.errors.length} errors`,
` Skills: ${skillResults.created} created, ${skillResults.failed} failed`,
consolidationResult
? ` Semantic Memory: +${consolidationResult.total.added} ADD, ~${consolidationResult.total.updated} UPDATE, ⊕${consolidationResult.total.merged} MERGE`
: '',
` Time: ${totalTimeMs}ms (${(totalTimeMs / 1000).toFixed(1)}s)`,
` Mode: ${enableParallel ? `parallel (concurrency=${concurrency})` : 'serial'}`,
` Tokens: input=${totalTokenUsage.input}, output=${totalTokenUsage.output}`,
` Tool calls: ${totalToolCalls}`,
skippedDims.length > 0 ? ` Checkpoints restored: [${skippedDims.join(', ')}]` : '',
incrementalSkippedDims.length > 0
? ` Incremental skip: [${incrementalSkippedDims.join(', ')}]`
: '',
]
.filter(Boolean)
.join('\n'));
// P4.2: 生成冷启动报告
try {
const report = {
version: '2.7.0',
timestamp: new Date().toISOString(),
project: {
name: projectInfo.name,
files: projectInfo.fileCount,
lang: projectInfo.lang,
},
duration: {
totalMs: totalTimeMs,
totalSec: Math.round(totalTimeMs / 1000),
},
dimensions: {},
totals: {
candidates: candidateResults.created,
skills: skillResults.created,
toolCalls: totalToolCalls,
tokenUsage: totalTokenUsage,
errors: candidateResults.errors.length,
},
checkpoints: {
restored: skippedDims,
},
incremental: isIncremental
? {
mode: 'incremental',
affectedDimensions: incrementalPlan.affectedDimensions,
skippedDimensions: incrementalSkippedDims,
diff: incrementalPlan.diff
? {
added: incrementalPlan.diff.added.length,
modified: incrementalPlan.diff.modified.length,
deleted: incrementalPlan.diff.deleted.length,
unchanged: incrementalPlan.diff.unchanged.length,
}
: null,
reason: incrementalPlan.reason,
}
: null,
semanticMemory: consolidationResult
? {
added: consolidationResult.total.added,
updated: consolidationResult.total.updated,
merged: consolidationResult.total.merged,
skipped: consolidationResult.total.skipped,
durationMs: consolidationResult.durationMs,
}
: null,
};
for (const [dimId, stat] of Object.entries(dimensionStats)) {
report.dimensions[dimId] = {
candidatesSubmitted: stat.candidateCount || 0,
candidatesRejected: stat.rejectedCount || 0,
analysisChars: stat.analysisChars || 0,
referencedFiles: stat.referencedFiles || 0,
durationMs: stat.durationMs || 0,
toolCallCount: stat.toolCallCount || 0,
tokenUsage: stat.tokenUsage || { input: 0, output: 0 },
qualityGate: stat.qualityGate || null,
};
}
// Phase E: 附加 Code Entity Graph 拓扑到报告
try {
const { CodeEntityGraph } = await import('#service/knowledge/CodeEntityGraph.js');
const entityRepo = ctx.container.get('codeEntityRepository');
const edgeRepo = ctx.container.get('knowledgeEdgeRepository');
if (entityRepo && edgeRepo) {
const ceg = new CodeEntityGraph(entityRepo, edgeRepo, { projectRoot, logger });
const topo = await ceg.getTopology();
report.codeEntityGraph = {
entities: topo.entities,
edges: topo.edges,
totalEntities: topo.totalEntities,
totalEdges: topo.totalEdges,
hotNodes: topo.hotNodes?.slice(0, 5),
};
}
}
catch {
/* non-blocking */
}
const reportDir = path.join(projectRoot, '.autosnippet');
await fs.mkdir(reportDir, { recursive: true });
await fs.writeFile(path.join(reportDir, 'bootstrap-report.json'), JSON.stringify(report, null, 2));
logger.info(`[Insight-v3] 📊 Bootstrap report saved to .autosnippet/bootstrap-report.json`);
}
catch (reportErr) {
logger.warn(`[Insight-v3] Bootstrap report generation failed: ${reportErr instanceof Error ? reportErr.message : String(reportErr)}`);
}
// P3: 成功完成后清理 checkpoints
await clearCheckpoints(projectRoot);
// v5.0: 保存 Bootstrap 快照 (用于下次增量 Bootstrap)
try {
const db = ctx.container.get('database');
if (db && allFiles) {
const ib = new IncrementalBootstrap(db, projectRoot, { logger });
const snapshotId = ib.saveSnapshot({
sessionId,
allFiles,
dimensionStats,
episodicMemory: sessionStore,
meta: {
durationMs: totalTimeMs,
candidateCount: candidateResults.created,
primaryLang: primaryLang || projectInfo.lang,
},
plan: isIncremental ? incrementalPlan : null,
});
logger.info(`[Insight-v3] 📸 Snapshot saved: ${snapshotId}`);
}
}
catch (snapErr) {
logger.warn(`[Insight-v3] Snapshot save failed (non-blocking): ${snapErr instanceof Error ? snapErr.message : String(snapErr)}`);
}
// 释放文件缓存
allFiles = null;
ctx.container.singletons._fileCache = null;
// ── Cursor Delivery: 生成 4 通道交付物料 ──
try {
const { getServiceContainer } = await import('#inject/ServiceContainer.js');
const container = getServiceContainer();
if (container.services.cursorDeliveryPipeline) {
const pipeline = container.get('cursorDeliveryPipeline');
const deliveryResult = await pipeline.deliver();
logger.info(`[Insight-v3] 🚀 Cursor Delivery complete — ` +
`A: ${deliveryResult.channelA.rulesCount} rules, ` +
`B: ${deliveryResult.channelB.topicCount} topics, ` +
`C: ${deliveryResult.channelC.synced} skills, ` +
`D: ${deliveryResult.channelD?.documentsCount || 0} documents, ` +
`F: ${deliveryResult.channelF?.filesWritten || 0} agent files`);
}
}
catch (deliveryErr) {
logger.warn(`[Insight-v3] Cursor Delivery failed (non-blocking): ${deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr)}`);
}
// ── Repo Wiki: 自动生成项目文档 Wiki ──
try {
const { getServiceContainer: getWikiContainer } = await import('#inject/ServiceContainer.js');
const wikiContainer = getWikiContainer();
const { WikiGenerator } = await import('#service/wiki/WikiGenerator.js');
// 同步 wiki 路由的任务状态,让前端轮询 /wiki/status 能看到进度
let patchWikiTask = null;
let realtimeService = null;
try {
const wikiRoute = await import('#http/routes/wiki.js');
patchWikiTask = wikiRoute.patchWikiTask;
}
catch {
/* ok */
}
try {
realtimeService = (wikiContainer.singletons?.realtimeService || null);
}
catch {
/* ok */
}
// 标记任务开始
patchWikiTask?.({
status: 'running',
startedAt: Date.now(),
phase: null,
progress: 0,
message: 'Bootstrap Wiki 生成中...',
finishedAt: null,
result: null,
error: null,
});
let moduleService = null, knowledgeService = null, codeEntityGraph = null;
try {
moduleService = wikiContainer.get('moduleService');
}
catch {
/* optional */
}
try {
knowledgeService = wikiContainer.get('knowledgeService');
}
catch {
/* optional */
}
try {
codeEntityGraph = wikiContainer.get('codeEntityGraph');
}
catch {
/* optional */
}
const wiki = new WikiGenerator({
projectRoot,
moduleService,
knowledgeService,
projectGraph, // 来自 Step 0.5 构建的 ProjectGraph
codeEntityGraph,
aiProvider: wikiContainer.singletons?.aiProvider || null,
onProgress: (phase, progress, message) => {
// 同步到 wiki 路由的任务状态
patchWikiTask?.({ phase, progress, message });
// 通过 Socket.io 推送进度
if (realtimeService) {
try {
realtimeService.broadcastEvent?.('wiki:progress', {
phase,
progress,
message,
timestamp: Date.now(),
});
}
catch {
/* non-critical */
}
}
},
options: { language: process.env.ASD_WIKI_LANG || 'zh' },
});
const wikiResult = (await wiki.generate());
if (wikiResult.success) {
logger.info(`[Insight-v3] 📖 Wiki generated — ${wikiResult.filesGenerated} files, ` +
`AI: ${wikiResult.aiComposed || 0}, Synced: ${wikiResult.syncedDocs || 0}, ` +
`Dedup removed: ${wikiResult.dedup?.removed ? wikiResult.dedup.removed.length : 0}`);
}
// 标记任务完成
patchWikiTask?.({
status: wikiResult.success ? 'done' : 'error',
finishedAt: Date.now(),
result: wikiResult,
error: wikiResult.success ? null : wikiResult.error || 'Unknown error',
progress: 100,
});
if (realtimeService) {
try {
realtimeService.broadcastEvent?.('wiki:completed', {
success: wikiResult.success,
filesGenerated: wikiResult.filesGenerated,
duration: wikiResult.duration,
});
}
catch {
/* non-critical */
}
}
}
catch (wikiErr) {
const wikiErrMsg = wikiErr instanceof Error ? wikiErr.message : String(wikiErr);
logger.warn(`[Insight-v3] Wiki generation failed (non-blocking): ${wikiErrMsg}`);
try {
const wikiRoute = await import('#http/routes/wiki.js');
wikiRoute.patchWikiTask?.({
status: 'error',
finishedAt: Date.now(),
error: wikiErrMsg,
});
}
catch {
/* ok */
}
}
}
/**
* 清除增量 Bootstrap 快照 — 供 bootstrapKnowledge 在手动冷启动时调用
* @param ctx { container, logger }
*/
async function clearSnapshotsImpl(projectRoot, ctx) {
try {
const db = ctx.container.get('database');
if (db) {
const { BootstrapSnapshot } = await import('./BootstrapSnapshot.js');
const snap = new BootstrapSnapshot(db, { logger: ctx.logger });
snap.clearProject(projectRoot);
ctx.logger.info('[Bootstrap] Cleared incremental snapshots — forcing full rebuild');
}
}
catch (err) {
ctx.logger.warn(`[Bootstrap] clearSnapshots failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
}
}
export { clearCheckpoints };
export { clearSnapshotsImpl as clearSnapshots };
export default fillDimensionsV3;