autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
295 lines (294 loc) • 12.4 kB
JavaScript
/**
* PanoramaAggregator — 全景数据汇总
*
* 编排 RoleRefiner → CouplingAnalyzer → LayerInferrer,
* 汇总为统一的 PanoramaResult,附加知识覆盖率和空白区检测。
*
* @module PanoramaAggregator
*/
var _a;
import { DimensionAnalyzer } from './DimensionAnalyzer.js';
import { profileTechStack } from './TechStackProfiler.js';
/* ═══ PanoramaAggregator Class ════════════════════════════ */
export class PanoramaAggregator {
#roleRefiner;
#couplingAnalyzer;
#layerInferrer;
#entityRepo;
#edgeRepo;
#knowledgeRepo;
#projectRoot;
#dimensionAnalyzer;
constructor(opts) {
this.#roleRefiner = opts.roleRefiner;
this.#couplingAnalyzer = opts.couplingAnalyzer;
this.#layerInferrer = opts.layerInferrer;
this.#entityRepo = opts.entityRepo;
this.#edgeRepo = opts.edgeRepo;
this.#knowledgeRepo = opts.knowledgeRepo;
this.#projectRoot = opts.projectRoot;
this.#dimensionAnalyzer =
opts.dimensionAnalyzer ??
new DimensionAnalyzer(opts.bootstrapRepo, opts.entityRepo, opts.knowledgeRepo, opts.projectRoot);
}
/**
* 计算完整全景数据
* @param moduleCandidates 模块候选列表
* @param options.configLayers 来自配置文件的层级声明(如 Boxfile layer 定义)
*/
async compute(moduleCandidates, options) {
// 1. RoleRefiner: 精化角色
const refinedRoles = await this.#roleRefiner.refineAll(moduleCandidates);
// 2. 构建模块-文件映射
const moduleFiles = new Map();
for (const mc of moduleCandidates) {
moduleFiles.set(mc.name, mc.files);
}
// 3. CouplingAnalyzer: 耦合分析(含外部依赖 fan-in)
const externalModules = await this.#collectExternalModules();
const coupling = await this.#couplingAnalyzer.analyze(moduleFiles, externalModules);
// 4. LayerInferrer: 层级推断(优先使用配置层级)
const modules = moduleCandidates.map((m) => m.name);
const configModuleLayerMap = new Map();
for (const mc of moduleCandidates) {
if (mc.configLayer) {
configModuleLayerMap.set(mc.name, mc.configLayer);
}
}
const layers = this.#layerInferrer.infer(coupling.edges, modules, coupling.cycles, {
configLayers: options?.configLayers,
moduleLayerMap: configModuleLayerMap.size > 0 ? configModuleLayerMap : undefined,
});
// 5. 构建层级映射 (模块名 → 层级号)
const moduleLayerMap = new Map();
for (const level of layers.levels) {
for (const mod of level.modules) {
moduleLayerMap.set(mod, level.level);
}
}
// 6. 项目级 recipe 总数(recipe scope 通常为 universal,不做模块强关联)
const projectRecipeCount = await this.#getProjectRecipeCount();
// 7. 计算总文件数
let totalFiles = 0;
for (const mc of moduleCandidates) {
totalFiles += mc.files.length;
}
// 8. 汇总 PanoramaModule
// 模块 recipeCount 按文件数等比分配项目级 recipe(反映覆盖贡献度)
const panoramaModules = new Map();
for (const mc of moduleCandidates) {
const refined = refinedRoles.get(mc.name);
const metrics = coupling.metrics.get(mc.name);
const recipeCount = totalFiles > 0 ? Math.round((projectRecipeCount * mc.files.length) / totalFiles) : 0;
panoramaModules.set(mc.name, {
name: mc.name,
inferredRole: mc.inferredRole,
refinedRole: refined?.refinedRole ?? mc.inferredRole,
roleConfidence: refined?.confidence ?? 0,
layer: moduleLayerMap.get(mc.name) ?? 0,
fanIn: metrics?.fanIn ?? 0,
fanOut: metrics?.fanOut ?? 0,
files: mc.files,
fileCount: mc.files.length,
recipeCount,
coverageRatio: mc.files.length > 0 ? recipeCount / mc.files.length : 0,
});
}
// 8.5 基于模块角色重命名层级(仅在拓扑推断模式下;配置层级保留原名)
if (!layers.configBased) {
this.#renameLayersByRole(layers, panoramaModules);
}
// 9. 多维度知识健康分析 (替代旧的基于模块文件数的覆盖率模型)
const moduleRoles = moduleCandidates.map((m) => {
const pm = panoramaModules.get(m.name);
return pm?.refinedRole ?? m.inferredRole;
});
const { radar, gaps } = await this.#dimensionAnalyzer.analyze(moduleRoles);
// 10. 调用流概要
const callFlowSummary = await this.#computeCallFlowSummary();
// 11. 外部依赖概况 + 技术栈画像
const externalDeps = await this.#buildExternalDepProfiles(coupling.externalDeps);
const techStack = externalDeps.length > 0 ? profileTechStack(externalDeps) : null;
return {
modules: panoramaModules,
layers,
cycles: coupling.cycles,
gaps,
healthRadar: radar,
callFlowSummary,
projectRecipeCount,
externalDeps,
techStack,
computedAt: Date.now(),
};
}
/* ─── Project Recipe Count ──────────────────────── */
async #getProjectRecipeCount() {
try {
return await this.#knowledgeRepo.countByCountableLifecycles();
}
catch {
return 0;
}
}
/* ─── External Dependencies ─────────────────────── */
/**
* 从 code_entities 收集标记为 external 的模块名
*/
async #collectExternalModules() {
try {
const rows = await this.#entityRepo.findModulesByNodeTypes(this.#projectRoot, [
'external',
'host',
]);
return new Set(rows.map((r) => r.entityId));
}
catch {
return new Set();
}
}
/**
* 将 CouplingAnalyzer 的外部依赖统计转为 ExternalDepProfile
* 并从 code_entities 补充 layer/version 元数据
*/
async #buildExternalDepProfiles(rawExternalDeps) {
if (rawExternalDeps.length === 0) {
return [];
}
// 查询外部依赖的元数据
const metadataMap = new Map();
try {
for (const dep of rawExternalDeps) {
const entity = await this.#entityRepo.findByEntityIdOnly(dep.name, this.#projectRoot);
if (entity?.metadata) {
const meta = entity.metadata;
metadataMap.set(dep.name, {
layer: meta.layer,
version: meta.version,
});
}
}
}
catch {
/* skip metadata enrichment errors */
}
return rawExternalDeps.map((dep) => {
const meta = metadataMap.get(dep.name);
return {
name: dep.name,
fanIn: dep.fanIn,
dependedBy: dep.dependedBy,
layer: meta?.layer,
version: meta?.version,
};
});
}
/* ─── Layer Naming (role-based) ─────────────────── */
/** 角色 → 层级名映射 */
static #ROLE_TO_LAYER = {
core: 'Foundation',
foundation: 'Foundation',
model: 'Model',
service: 'Service',
networking: 'Infrastructure',
storage: 'Infrastructure',
ui: 'UI',
feature: 'Feature',
config: 'Configuration',
test: 'Test',
app: 'Application',
};
/**
* 基于模块 refinedRole 投票重命名层级
* 比模块名 pattern 匹配更准确(避免 BDUIKit 被误匹配为 Foundation 等问题)
*/
#renameLayersByRole(layers, panoramaModules) {
const usedNames = new Set();
const maxLevel = Math.max(...layers.levels.map((l) => l.level), 0);
for (const level of layers.levels) {
// 只统计有文件的模块(排除 0 文件的第三方依赖干扰)
const roleVotes = new Map();
for (const modName of level.modules) {
const mod = panoramaModules.get(modName);
if (mod && mod.fileCount > 0) {
const role = mod.refinedRole || mod.inferredRole;
roleVotes.set(role, (roleVotes.get(role) ?? 0) + 1);
}
}
let layerName;
if (roleVotes.size === 0) {
// 全部是 0 文件模块 → 用位置推断
layerName =
level.level === 0 ? 'Foundation' : level.level === maxLevel ? 'Application' : 'Feature';
}
else {
// 选最高票角色
let bestRole = '';
let bestCount = 0;
for (const [role, count] of roleVotes) {
if (count > bestCount) {
bestRole = role;
bestCount = count;
}
}
layerName = _a.#ROLE_TO_LAYER[bestRole] ?? 'Feature';
// 位置修正:最底层优先 Foundation,最顶层优先 Application
if (level.level === 0 && roleVotes.has('core')) {
layerName = 'Foundation';
}
else if (level.level === maxLevel && layers.levels.length > 1) {
layerName = 'Application';
}
}
// 去重:优先尝试次高票角色名,仍冲突则追加位置描述
if (usedNames.has(layerName)) {
// 尝试次高票角色
let resolved = false;
if (roleVotes.size > 1) {
const sortedRoles = [...roleVotes.entries()].sort((a, b) => b[1] - a[1]);
for (let i = 1; i < sortedRoles.length; i++) {
const altName = _a.#ROLE_TO_LAYER[sortedRoles[i][0]] ?? sortedRoles[i][0];
if (!usedNames.has(altName)) {
layerName = altName;
resolved = true;
break;
}
}
}
// 仍冲突:使用位置描述而非数字后缀
if (!resolved && usedNames.has(layerName)) {
const pos = level.level <= maxLevel * 0.33
? 'Core'
: level.level >= maxLevel * 0.67
? 'App'
: 'Mid';
const qualifiedName = `${pos} ${layerName}`;
layerName = usedNames.has(qualifiedName) ? `${layerName} L${level.level}` : qualifiedName;
}
}
usedNames.add(layerName);
level.name = layerName;
}
}
/* ─── Call Flow Summary ─────────────────────────── */
async #computeCallFlowSummary() {
// 最频繁被调用的方法
const topCalled = await this.#edgeRepo.findTopCalledNodes(10);
// 入口点: 只有出度没有入度的方法
const entryPoints = await this.#edgeRepo.findEntryPoints(20);
// 数据生产者: data_flow outFlow >> inFlow
const dataProducers = await this.#edgeRepo.findTopDataFlowSources(10, 3);
// 数据消费者: data_flow inFlow >> outFlow
const dataConsumers = await this.#edgeRepo.findTopDataFlowSinks(10, 3);
return {
topCalledMethods: topCalled.map((r) => ({
id: r.toId,
callCount: r.callCount,
})),
entryPoints,
dataProducers,
dataConsumers,
};
}
}
_a = PanoramaAggregator;