UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

257 lines (256 loc) 9.89 kB
/** * DimensionAnalyzer — 多维度知识健康分析 * * **v2: 从统一维度注册表 (DimensionRegistry) 派生维度** * * 灵感来源: * - ISO/IEC 25010 质量模型 (8 大特性: 可靠性、安全性、可维护性…) * - ThoughtWorks Tech Radar (Adopt/Trial/Assess/Hold 四环) * - 雷达图/蛛网图可视化模型 * * 核心思路: 按「知识维度」衡量项目在各工程方向上的规范成熟度。 * 某维度 Recipe 为 0 → 该方向完全空白,标示为 gap。 * * @module DimensionAnalyzer */ import { classifyRecipeToDimension, DIMENSION_REGISTRY, resolveActiveDimensions, } from '#domain/dimension/index.js'; import { LanguageService } from '#shared/LanguageService.js'; import { COUNTABLE_LIFECYCLES } from '../../domain/knowledge/Lifecycle.js'; /* ═══ DimensionAnalyzer Class ═════════════════════════════ */ export class DimensionAnalyzer { #bootstrapRepo; #entityRepo; #knowledgeRepo; #projectRoot; constructor(bootstrapRepo, entityRepo, knowledgeRepo, projectRoot) { this.#bootstrapRepo = bootstrapRepo; this.#entityRepo = entityRepo; this.#knowledgeRepo = knowledgeRepo; this.#projectRoot = projectRoot; } /** * 分析项目知识健康雷达 * * @param moduleRoles — 项目中存在的模块角色 (用于 gap 优先级推断) */ async analyze(moduleRoles) { // 0. 按项目语言过滤活跃维度(排除无关语言/框架维度) const activeDims = await this.#resolveActiveDims(); // 1. 从 DB 获取所有活跃 recipe 的维度分类信息 const recipes = await this.#fetchRecipeMetadata(); // 2. 将每条 recipe 映射到维度 const dimensionCounts = new Map(); for (const def of activeDims) { dimensionCounts.set(def.id, { count: 0, titles: [] }); } let totalRecipes = 0; for (const recipe of recipes) { totalRecipes++; const dimId = this.#classifyRecipe(recipe); if (dimId) { const entry = dimensionCounts.get(dimId); entry.count++; if (entry.titles.length < 3) { entry.titles.push(recipe.title); } } } // 3. 计算各维度得分与状态 const dimensions = activeDims.map((def) => { const entry = dimensionCounts.get(def.id); return this.#scoreDimension(def, entry.count, entry.titles); }); // 4. 加权平均健康分 let weightedSum = 0; let weightTotal = 0; for (let i = 0; i < activeDims.length; i++) { weightedSum += dimensions[i].score * activeDims[i].weight; weightTotal += activeDims[i].weight; } const overallScore = weightTotal > 0 ? Math.round(weightedSum / weightTotal) : 0; // 5. 统计覆盖 const coveredDimensions = dimensions.filter((d) => d.recipeCount > 0).length; const totalDimensions = dimensions.length; const radar = { dimensions, overallScore, totalRecipes, coveredDimensions, totalDimensions, dimensionCoverage: totalDimensions > 0 ? coveredDimensions / totalDimensions : 0, }; // 6. 生成维度空白 (gaps) const roleSet = new Set(moduleRoles); const gaps = this.#detectDimensionGaps(dimensions, activeDims, roleSet); return { radar, gaps }; } /* ─── 按项目语言解析活跃维度 ───────────────────── */ async #resolveActiveDims() { // 1. 优先从 bootstrap_snapshots 获取 primary_lang try { const primaryLang = await this.#bootstrapRepo.getLatestPrimaryLang(this.#projectRoot); if (primaryLang) { return resolveActiveDimensions(primaryLang); } } catch { // 无 bootstrap 数据 → 继续尝试从 code_entities 推断 } // 2. 从 code_entities 文件扩展名推断主语言 const inferredLang = await this.#inferLanguageFromEntities(); if (inferredLang) { return resolveActiveDimensions(inferredLang); } return DIMENSION_REGISTRY; } /** * 从 code_entities 文件扩展名统计推断项目主语言 * * 当无 bootstrap_snapshots 时使用(如仅执行了 scan 但未 bootstrap 的项目) */ async #inferLanguageFromEntities() { try { const filePaths = await this.#entityRepo.findDistinctFilePaths(this.#projectRoot, 2000); if (filePaths.length === 0) { return null; } const langCounts = new Map(); for (const fp of filePaths) { if (!fp) { continue; } const dotIdx = fp.lastIndexOf('.'); if (dotIdx < 0) { continue; } const ext = fp.slice(dotIdx).toLowerCase(); const lang = LanguageService.langFromExt(ext); if (lang !== 'unknown') { langCounts.set(lang, (langCounts.get(lang) ?? 0) + 1); } } // .h 文件可能属于 Swift/ObjC 项目 — 如果同时存在 .swift 文件则优先 swift if (langCounts.has('swift') && langCounts.has('objectivec')) { const swiftCount = langCounts.get('swift'); const objcCount = langCounts.get('objectivec'); if (swiftCount >= objcCount * 0.2) { return 'swift'; } } // 选最多的语言 let bestLang = ''; let bestCount = 0; for (const [lang, _count] of langCounts) { if (_count > bestCount) { bestLang = lang; bestCount = _count; } } return bestLang || null; } catch { return null; } } /* ─── 从 DB 获取 recipe 元数据 ─────────────────── */ async #fetchRecipeMetadata() { try { return await this.#knowledgeRepo.findRecipeMetadata(COUNTABLE_LIFECYCLES); } catch { return []; } } /* ─── Recipe → 维度分类 ────────────────────────── */ /** * 将 recipe 分类到最匹配的维度 * * 委托给 DimensionRegistry.classifyRecipeToDimension() */ #classifyRecipe(recipe) { return classifyRecipeToDimension(recipe.topicHint, recipe.category); } /* ─── 维度评分 ─────────────────────────────────── */ #scoreDimension(def, recipeCount, titles) { // 得分: 每条 recipe 贡献 20 分, 上限 100 const score = Math.min(100, recipeCount * 20); // 状态阈值 let status; if (recipeCount === 0) { status = 'missing'; } else if (recipeCount === 1) { status = 'weak'; } else if (recipeCount <= 4) { status = 'adequate'; } else { status = 'strong'; } // 雷达环级 (对应 Tech Radar) let level; if (score >= 80) { level = 'adopt'; } else if (score >= 40) { level = 'trial'; } else if (score > 0) { level = 'assess'; } else { level = 'hold'; } return { id: def.id, name: def.label, description: def.qualityDescription, recipeCount, score, status, level, topRecipes: titles, }; } /* ─── 维度空白检测 ─────────────────────────────── */ #detectDimensionGaps(dimensions, activeDims, moduleRoles) { const gaps = []; for (let i = 0; i < dimensions.length; i++) { const dim = dimensions[i]; const def = activeDims[i]; if (dim.status !== 'missing' && dim.status !== 'weak') { continue; } // 优先级推断: 维度权重 × 是否有关联模块角色 const hasRelatedModules = def.relatedRoles.length === 0 || def.relatedRoles.some((r) => moduleRoles.has(r)); let priority; if (dim.status === 'missing' && def.weight >= 0.9) { priority = 'high'; } else if (dim.status === 'missing' && hasRelatedModules) { priority = 'high'; } else if (dim.status === 'missing') { priority = 'medium'; } else { // weak priority = hasRelatedModules && def.weight >= 0.9 ? 'medium' : 'low'; } const affectedRoles = def.relatedRoles.filter((r) => moduleRoles.has(r)); gaps.push({ dimension: def.id, dimensionName: def.label, recipeCount: dim.recipeCount, status: dim.status, priority, suggestedTopics: [...def.suggestedTopics], affectedRoles, }); } // 按优先级排序 const priorityOrder = { high: 0, medium: 1, low: 2 }; return gaps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); } }