UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

148 lines (147 loc) 6.1 kB
/** * CoverageAnalyzer — Guard 覆盖率矩阵 + Panorama 协同 * * 计算模块级 Rule 覆盖率,识别零覆盖和低覆盖模块。 * 与 PanoramaService 协同:利用模块划分 + gaps 数据做精准评估。 */ import Logger from '../../infrastructure/logging/Logger.js'; import { LanguageService } from '../../shared/LanguageService.js'; /* ────────────────────── 常量 ────────────────────── */ const LOW_COVERAGE_THRESHOLD = 50; /* ────────────────────── Class ────────────────────── */ export class CoverageAnalyzer { #knowledgeRepo; #guardViolationRepo; #ruleLearner; #logger = Logger.getInstance(); constructor(knowledgeRepo, guardViolationRepo, options = {}) { this.#knowledgeRepo = knowledgeRepo; this.#guardViolationRepo = guardViolationRepo; this.#ruleLearner = options.ruleLearner ?? null; } /** * 计算覆盖率矩阵 * @param moduleFiles 模块名 → 文件路径列表的映射 */ analyze(moduleFiles) { // 1. 加载所有 active rule recipes const rules = this.#loadActiveRules(); // 2. 构建 rule → 文件路径映射 (从 guard_violations 历史推断) const ruleFileMap = this.#buildRuleFileMap(); // 3. 加载规则健康数据 const allStats = this.#ruleLearner?.getAllStats() ?? {}; // 4. 对每个模块计算覆盖率 const modules = []; for (const [moduleName, files] of moduleFiles) { if (files.length === 0) { modules.push({ module: moduleName, ruleCount: 0, fpRate: 0, coverage: 0, level: 'zero', }); continue; } // 找出与该模块文件匹配的 rule const fileSet = new Set(files); const matchedRules = new Set(); // 方式 1: rule 的 guard_violations 历史曾命中该模块的文件 for (const [ruleId, rulePaths] of ruleFileMap) { for (const rp of rulePaths) { if (fileSet.has(rp)) { matchedRules.add(ruleId); break; } } } // 方式 2: rule 的 scope/language 与模块语言匹配 (基于文件扩展名推断) const moduleLanguages = this.#inferLanguages(files); for (const rule of rules) { if (rule.languages.length === 0 || rule.languages.some((l) => moduleLanguages.has(l))) { matchedRules.add(rule.id); } } const ruleCount = matchedRules.size; // 计算该模块关联规则的平均 FP 率 let fpRateSum = 0; let fpRuleCount = 0; for (const ruleId of matchedRules) { const stat = allStats[ruleId]; if (stat?.triggers && stat.triggers > 0) { const metrics = this.#ruleLearner?.getMetrics(ruleId); if (metrics) { fpRateSum += metrics.falsePositiveRate; fpRuleCount++; } } } const fpRate = fpRuleCount > 0 ? Math.round((fpRateSum / fpRuleCount) * 100) : 0; // 覆盖率 = 匹配到的规则数 / (模块文件数 × 权重因子) // 简化公式: 每个文件理论上应有 ≥1 条规则覆盖 const coverage = Math.min(100, Math.round((ruleCount / Math.max(1, files.length)) * 100)); const level = coverage === 0 ? 'zero' : coverage < LOW_COVERAGE_THRESHOLD ? 'low' : 'good'; modules.push({ module: moduleName, ruleCount, fpRate, coverage, level }); } // 5. 总体覆盖率 const totalModules = modules.length; const overallCoverage = totalModules > 0 ? Math.round(modules.reduce((sum, m) => sum + m.coverage, 0) / totalModules) : 0; const zeroModules = modules.filter((m) => m.level === 'zero').map((m) => m.module); const lowModules = modules.filter((m) => m.level === 'low').map((m) => m.module); return { modules, overallCoverage, zeroModules, lowModules }; } /* ── 内部 ── */ #loadActiveRules() { try { const rows = this.#knowledgeRepo.findActiveRuleIdsSync(); return rows.map((r) => ({ id: r.id, languages: r.language ? r.language.split(',').map((l) => LanguageService.normalize(l.trim())) : [], })); } catch { return []; } } #buildRuleFileMap() { const map = new Map(); try { const rows = this.#guardViolationRepo.findRecentViolationsJson(200); for (const row of rows) { try { const violations = JSON.parse(row.violationsJson || '[]'); const filePath = row.filePath; for (const v of violations) { if (!v.ruleId) { continue; } const list = map.get(v.ruleId) || []; list.push(filePath); map.set(v.ruleId, list); } } catch { // invalid json } } } catch { // table doesn't exist } return map; } #inferLanguages(files) { const langs = new Set(); for (const f of files) { const lang = LanguageService.inferLang(f); if (lang !== 'unknown') { langs.add(lang); } } return langs; } }