autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
398 lines (397 loc) • 17.1 kB
JavaScript
/**
* ComplianceReporter — 全项目 Guard 合规报告生成
*
* 依赖:
* - GuardCheckEngine.auditFiles() — 原始 violations 数据
* - ViolationsStore — 历史统计 & 趋势
* - RuleLearner — 规则 P/R/F1
* - ExclusionManager — 排除项(不计入合规分)
* - config.qualityGate — 阈值配置
*
* 输出:
* ComplianceReport { qualityGate, summary, topViolations, fileHotspots, ruleHealth, trend }
*/
import Logger from '../../infrastructure/logging/Logger.js';
import { COMPLIANCE_SCORING, QUALITY_GATE } from '../../shared/constants.js';
import { collectSourceFilesWithContent } from './SourceFileCollector.js';
/** Quality Gate 评分算法 */
function computeScore(summary, ruleHealth = []) {
let score = 100;
// 扣分:每个 error/warning/info 按常量权重扣分
score -= summary.errors * COMPLIANCE_SCORING.ERROR_PENALTY;
score -= summary.warnings * COMPLIANCE_SCORING.WARNING_PENALTY;
score -= (summary.infos || 0) * COMPLIANCE_SCORING.INFO_PENALTY;
// 加分:规则平均 F1 > 阈值加分
if (ruleHealth.length > 0) {
const avgF1 = ruleHealth.reduce((s, r) => s + (r.f1 || 0), 0) / ruleHealth.length;
if (avgF1 > COMPLIANCE_SCORING.HIGH_F1_THRESHOLD) {
score += COMPLIANCE_SCORING.HIGH_F1_BONUS;
}
}
// 扣分:高误报规则每条扣分
const problematic = ruleHealth.filter((r) => (r.precision || 1) < COMPLIANCE_SCORING.LOW_PRECISION_THRESHOLD);
score -= problematic.length * COMPLIANCE_SCORING.PROBLEMATIC_RULE_PENALTY;
return Math.max(0, Math.min(100, Math.round(score)));
}
/** 判定 Quality Gate 状态 */
function evaluateGate(summary, score, thresholds) {
const { maxErrors = QUALITY_GATE.MAX_ERRORS, maxWarnings = QUALITY_GATE.MAX_WARNINGS, minScore = QUALITY_GATE.MIN_SCORE, } = thresholds;
if (summary.errors > maxErrors) {
return 'FAIL';
}
if (score < minScore) {
return 'FAIL';
}
if (summary.warnings > maxWarnings) {
return 'WARN';
}
return 'PASS';
}
export class ComplianceReporter {
engine;
exclusionManager;
logger;
qualityGateConfig;
ruleLearner;
violationsStore;
/** 实时规则精度追踪(由 SignalBus 更新) */
#rulePrecision = new Map();
#recentViolationCount = 0;
/** @param qualityGateConfig { maxErrors, maxWarnings, minScore } */
constructor(guardCheckEngine, violationsStore, ruleLearner, exclusionManager, qualityGateConfig = {}, signalBus) {
this.engine = guardCheckEngine;
this.violationsStore = violationsStore;
this.ruleLearner = ruleLearner;
this.exclusionManager = exclusionManager;
this.qualityGateConfig = {
maxErrors: QUALITY_GATE.MAX_ERRORS,
maxWarnings: QUALITY_GATE.MAX_WARNINGS,
minScore: QUALITY_GATE.MIN_SCORE,
...qualityGateConfig,
};
this.logger = Logger.getInstance();
// Phase 2: 订阅 guard|quality 信号维护实时精度
if (signalBus) {
signalBus.subscribe('guard|quality', (signal) => {
if (signal.type === 'quality' && signal.source === 'RuleLearner' && signal.target) {
this.#rulePrecision.set(signal.target, signal.value);
}
if (signal.type === 'guard') {
this.#recentViolationCount++;
}
});
}
}
/**
* 确保 Enhancement Pack 规则已注入到 engine
* 与 MCP guard handler 的 _injectEnhancementGuardRules 逻辑一致
*/
async #ensureEnhancementPackRules() {
if (!this.engine.isEpInjected || this.engine.isEpInjected()) {
return;
}
try {
const { initEnhancementRegistry } = await import('#core/enhancement/index.js');
const enhReg = await initEnhancementRegistry();
// 仅注入无框架条件的通用 Pack(有框架条件的由 Bootstrap resolve() 精确注入)
const allPacks = enhReg.all().filter((pack) => {
const cond = pack.conditions;
return !cond?.frameworks?.length;
});
const allGuardRules = [];
for (const pack of allPacks) {
try {
const rules = pack.getGuardRules();
if (rules.length > 0) {
allGuardRules.push(...rules);
}
}
catch {
/* graceful degradation per pack */
}
}
if (allGuardRules.length > 0 && this.engine.injectExternalRules) {
this.engine.injectExternalRules(allGuardRules);
this.logger.info(`[ComplianceReporter] Injected ${allGuardRules.length} Enhancement Pack rules`);
}
this.engine.markEpInjected?.();
}
catch {
/* Enhancement registry not available — non-critical */
}
}
/**
* 确保 AST 语言插件已加载(Tree-sitter WASM)
* 未加载时 _runAstLayer2Checks 会静默跳过,导致 AST 类违规无法检测
*/
async #ensureAstPlugins() {
try {
const { loadPlugins } = await import('../../core/ast/index.js');
await loadPlugins();
}
catch {
/* AST not available — graceful degradation */
}
}
/**
* 生成全项目合规报告
* @param projectRoot 项目根目录
* @param [options.qualityGate] 覆盖默认的 Quality Gate 阈值
* @param [options.maxFiles] 最大扫描文件数
*/
async generate(projectRoot, options = {}) {
const thresholds = { ...this.qualityGateConfig, ...(options.qualityGate || {}) };
const maxFiles = options.maxFiles || 500;
// 0. 确保 Enhancement Pack 规则已注入(与 MCP guard handler 保持一致)
await this.#ensureEnhancementPackRules();
// 0b. 确保 AST 语言插件已加载(Tree-sitter WASM)
await this.#ensureAstPlugins();
// 1. 收集源文件
const files = await collectSourceFilesWithContent(projectRoot, { maxFiles });
this.logger.info(`[ComplianceReporter] Collected ${files.length} source files`);
// 2. 批量审计
const auditResult = this.engine.auditFiles(files, { scope: 'project' });
// 3. 通过 ExclusionManager 过滤被排除的项
const filteredFiles = [];
for (const fileResult of auditResult.files || []) {
if (this.exclusionManager?.isPathExcluded?.(fileResult.filePath)) {
continue;
}
const filteredViolations = fileResult.violations.filter((v) => {
// isRuleExcluded 内部已检查全局排除
if (this.exclusionManager?.isRuleExcluded?.(v.ruleId, fileResult.filePath)) {
return false;
}
return true;
});
filteredFiles.push({
...fileResult,
violations: filteredViolations,
summary: {
total: filteredViolations.length,
errors: filteredViolations.filter((v) => v.severity === 'error').length,
warnings: filteredViolations.filter((v) => v.severity === 'warning').length,
infos: filteredViolations.filter((v) => v.severity === 'info').length,
},
});
}
// 4. 汇总
const summary = {
filesScanned: files.length,
totalViolations: filteredFiles.reduce((s, f) => s + f.summary.total, 0),
errors: filteredFiles.reduce((s, f) => s + f.summary.errors, 0),
warnings: filteredFiles.reduce((s, f) => s + f.summary.warnings, 0),
infos: filteredFiles.reduce((s, f) => s + f.summary.infos, 0),
};
// 5. 按规则 ID 聚合 top violations
const ruleAgg = new Map();
for (const f of filteredFiles) {
for (const v of f.violations) {
const key = v.ruleId;
if (!ruleAgg.has(key)) {
ruleAgg.set(key, {
ruleId: key,
message: v.message,
severity: v.severity,
fileCount: new Set(),
occurrences: 0,
fixRecipeId: null,
fixRecipeTitle: null,
});
}
const agg = ruleAgg.get(key);
agg.fileCount.add(f.filePath);
agg.occurrences++;
if (v.fixSuggestion && !agg.fixRecipeId) {
agg.fixRecipeId = v.fixSuggestion.replace(/^recipe:/, '');
}
}
}
const topViolations = [...ruleAgg.values()]
.map((v) => ({ ...v, fileCount: v.fileCount.size }))
.sort((a, b) => b.occurrences - a.occurrences)
.slice(0, 20);
// 6. 文件热点
const fileHotspots = filteredFiles
.filter((f) => f.summary.total > 0)
.map((f) => ({
filePath: f.filePath,
violationCount: f.summary.total,
errorCount: f.summary.errors,
}))
.sort((a, b) => b.violationCount - a.violationCount)
.slice(0, 20);
// 7. 规则健康度(来自 RuleLearner)
let ruleHealth = [];
try {
if (this.ruleLearner?.getAllStats) {
const allStats = this.ruleLearner.getAllStats();
ruleHealth = Object.entries(allStats).map(([ruleId, stat]) => ({
ruleId,
precision: stat.metrics?.precision ?? 1,
recall: stat.metrics?.recall ?? 1,
f1: stat.metrics?.f1 ?? 1,
triggers: stat.triggers || 0,
warning: (stat.metrics?.precision ?? 1) < 0.5 ? '高误报' : null,
}));
}
}
catch {
// RuleLearner not available
}
// 8. 趋势
let trend = {
errorsChange: 0,
warningsChange: 0,
hasHistory: false,
};
try {
if (this.violationsStore?.getTrend) {
trend = this.violationsStore.getTrend();
}
}
catch {
// ViolationsStore not available
}
// 9. 评分 + Gate
const complianceScore = computeScore(summary, ruleHealth);
// 9b. 三维度评分: coverage + confidence(来自 capabilityReport)
const capabilityReport = auditResult.capabilityReport;
const coverageScore = capabilityReport?.checkCoverage ?? 100;
const totalChecks = summary.totalViolations + (capabilityReport?.uncertainResults.length ?? 0);
const uncertainCount = capabilityReport?.uncertainResults.length ?? 0;
const confidenceScore = totalChecks > 0
? Math.round((1 - uncertainCount / Math.max(1, totalChecks + filteredFiles.length)) * 100)
: 100;
const uncertainSummary = {
total: uncertainCount,
byLayer: {},
byReason: {},
};
if (capabilityReport) {
for (const u of capabilityReport.uncertainResults) {
uncertainSummary.byLayer[u.layer] = (uncertainSummary.byLayer[u.layer] || 0) + 1;
uncertainSummary.byReason[u.reason] = (uncertainSummary.byReason[u.reason] || 0) + 1;
}
}
const gateStatus = evaluateGate(summary, complianceScore, thresholds);
// 10. 写入 ViolationsStore(记录本次运行)
try {
if (this.violationsStore?.appendRun) {
const allViolations = filteredFiles.flatMap((f) => f.violations.map((v) => ({ ...v, filePath: f.filePath })));
this.violationsStore.appendRun({
filePath: projectRoot,
violations: allViolations,
summary: `Compliance scan: score=${complianceScore} ${gateStatus} | ${summary.errors}E ${summary.warnings}W | cov=${coverageScore} conf=${confidenceScore}`,
});
}
}
catch {
// Persist failure — non-critical
}
return {
timestamp: new Date().toISOString(),
projectRoot,
qualityGate: {
status: gateStatus,
score: complianceScore,
thresholds,
},
complianceScore,
coverageScore,
confidenceScore,
uncertainSummary,
boundaries: capabilityReport?.boundaries ?? [],
summary,
topViolations,
fileHotspots,
ruleHealth,
trend,
};
}
/**
* 终端格式化输出报告
* @param report generate() 产出的报告
* @param options { format: 'text' | 'markdown' | 'json' }
*/
printReport(report, options = {}) {
const { format = 'text' } = options;
if (format === 'json') {
return;
}
if (format === 'markdown') {
this._printMarkdown(report);
return;
}
// text format
this._printText(report);
}
_printText(report) {
const { qualityGate, summary, topViolations, fileHotspots, trend } = report;
const gateIcon = qualityGate.status === 'PASS' ? '✅' : qualityGate.status === 'WARN' ? '⚠️' : '❌';
const lines = [];
lines.push(`${gateIcon} Quality Gate: ${qualityGate.status} Score: ${qualityGate.score}/100`);
lines.push(` Files: ${summary.filesScanned} Errors: ${summary.errors} Warnings: ${summary.warnings} Infos: ${summary.infos || 0}`);
if (trend.hasHistory) {
const errTrend = trend.errorsChange > 0 ? `+${trend.errorsChange}` : `${trend.errorsChange}`;
const warnTrend = trend.warningsChange > 0 ? `+${trend.warningsChange}` : `${trend.warningsChange}`;
lines.push(` Trend: Errors ${errTrend} Warnings ${warnTrend}`);
}
if (topViolations.length > 0) {
lines.push('');
lines.push('Top Violations:');
for (const v of topViolations.slice(0, 10)) {
const fix = v.fixRecipeId ? ` → 🔧 recipe:${v.fixRecipeId}` : '';
const sev = v.severity === 'error' ? '🔴' : v.severity === 'warning' ? '🟡' : '🔵';
lines.push(` ${sev} ${v.ruleId} (${v.occurrences} hits, ${v.fileCount} files)${fix}`);
}
}
if (fileHotspots.length > 0) {
lines.push('');
lines.push('File Hotspots:');
for (const f of fileHotspots.slice(0, 10)) {
lines.push(` ${f.filePath} — ${f.violationCount} violations (${f.errorCount} errors)`);
}
}
this.logger.info(lines.join('\n'));
}
_printMarkdown(report) {
const { qualityGate, summary, topViolations, fileHotspots, trend } = report;
const lines = [];
lines.push('# Guard Compliance Report');
lines.push('');
lines.push(`| Metric | Value |`);
lines.push(`|--------|-------|`);
lines.push(`| Quality Gate | ${qualityGate.status} (Score: ${qualityGate.score}/100) |`);
lines.push(`| Files Scanned | ${summary.filesScanned} |`);
lines.push(`| Errors | ${summary.errors} |`);
lines.push(`| Warnings | ${summary.warnings} |`);
lines.push(`| Infos | ${summary.infos || 0} |`);
if (trend.hasHistory) {
lines.push(`| Errors Trend | ${trend.errorsChange > 0 ? '+' : ''}${trend.errorsChange} |`);
lines.push(`| Warnings Trend | ${trend.warningsChange > 0 ? '+' : ''}${trend.warningsChange} |`);
}
if (topViolations.length > 0) {
lines.push('');
lines.push('## Top Violations');
lines.push('');
lines.push('| Rule | Severity | Files | Hits | Fix |');
lines.push('|------|----------|-------|------|-----|');
for (const v of topViolations.slice(0, 20)) {
const fix = v.fixRecipeId ? `recipe:${v.fixRecipeId}` : '-';
lines.push(`| ${v.ruleId} | ${v.severity} | ${v.fileCount} | ${v.occurrences} | ${fix} |`);
}
}
if (fileHotspots.length > 0) {
lines.push('');
lines.push('## File Hotspots');
lines.push('');
lines.push('| File | Violations | Errors |');
lines.push('|------|-----------|--------|');
for (const f of fileHotspots.slice(0, 20)) {
lines.push(`| ${f.filePath} | ${f.violationCount} | ${f.errorCount} |`);
}
}
}
}
export default ComplianceReporter;