333 lines
10 KiB
JavaScript
333 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 预测性审计引擎 (Predictive Audit)
|
|
*
|
|
* 挖掘 evolution-log.jsonl 模式:
|
|
* - 哪些标签/维度修复最频繁
|
|
* - 版本间修复密度趋势
|
|
* - 预测下次升级可能的风险点
|
|
*
|
|
* 用法:
|
|
* node scripts/predictive-audit.js # 文本报告
|
|
* node scripts/predictive-audit.js --json # JSON 输出
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
|
|
|
|
const CLAUDE_ROOT = detectClaudeRoot();
|
|
const JSON_MODE = process.argv.includes('--json');
|
|
|
|
// === 加载演进日志 ===
|
|
function loadEvolutionLog() {
|
|
const candidates = [
|
|
path.join(CLAUDE_ROOT, 'evolution-log.jsonl'),
|
|
path.join(CLAUDE_ROOT, 'debug', 'evolution-log.jsonl'),
|
|
path.join(CLAUDE_ROOT, 'projects', 'C--Users-janson9527us', 'memory', 'evolution-log.jsonl'),
|
|
];
|
|
|
|
const allEntries = [];
|
|
const seenSeqs = new Set();
|
|
for (const fp of candidates) {
|
|
if (!fs.existsSync(fp)) continue;
|
|
const lines = fs.readFileSync(fp, 'utf8').trim().split('\n');
|
|
for (const line of lines) {
|
|
try {
|
|
const entry = JSON.parse(line);
|
|
if (entry.seq != null && !seenSeqs.has(entry.seq)) {
|
|
seenSeqs.add(entry.seq);
|
|
allEntries.push(entry);
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
return allEntries.sort((a, b) => (a.seq || 0) - (b.seq || 0));
|
|
}
|
|
|
|
// === 分析 ===
|
|
function analyze(entries) {
|
|
if (entries.length === 0) return null;
|
|
|
|
// 1. 标签频率分析
|
|
const tagCounts = {};
|
|
for (const e of entries) {
|
|
for (const tag of (e.tags || [])) {
|
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
}
|
|
}
|
|
const topTags = Object.entries(tagCounts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([tag, count]) => ({ tag, count, pct: Math.round(count / entries.length * 100) }));
|
|
|
|
// 2. 修复密度趋势 (按版本)
|
|
const byVersion = {};
|
|
for (const e of entries) {
|
|
const v = e.version || 'unknown';
|
|
if (!byVersion[v]) byVersion[v] = { count: 0, totalFixes: 0, entries: [] };
|
|
byVersion[v].count++;
|
|
byVersion[v].totalFixes += (e.fix_count || 0);
|
|
byVersion[v].entries.push(e);
|
|
}
|
|
|
|
const versionTrend = Object.entries(byVersion)
|
|
.sort((a, b) => {
|
|
// 按版本号排序
|
|
const va = a[0].replace('v', '').split('.').map(Number);
|
|
const vb = b[0].replace('v', '').split('.').map(Number);
|
|
return (va[0] - vb[0]) || (va[1] - vb[1]);
|
|
})
|
|
.map(([version, data]) => ({
|
|
version,
|
|
entries: data.count,
|
|
totalFixes: data.totalFixes,
|
|
avgFixesPerEntry: data.count > 0 ? Math.round(data.totalFixes / data.count * 10) / 10 : 0,
|
|
}));
|
|
|
|
// 3. 触发源分析
|
|
const triggerCounts = {};
|
|
for (const e of entries) {
|
|
const trigger = e.trigger || 'unknown';
|
|
triggerCounts[trigger] = (triggerCounts[trigger] || 0) + 1;
|
|
}
|
|
|
|
// 4. 趋势分析 - 最近 5 个版本的复杂度变化
|
|
const recentVersions = versionTrend.slice(-5);
|
|
const complexityTrend = recentVersions.map(v => v.totalFixes);
|
|
const isIncreasing = complexityTrend.length >= 3 &&
|
|
complexityTrend[complexityTrend.length - 1] < complexityTrend[complexityTrend.length - 2];
|
|
|
|
// 5. 预测风险点
|
|
const predictions = [];
|
|
|
|
// 高频标签预测
|
|
if (topTags.length > 0) {
|
|
const topTag = topTags[0];
|
|
if (topTag.pct > 60) {
|
|
predictions.push({
|
|
risk: 'HIGH',
|
|
area: topTag.tag,
|
|
reason: `"${topTag.tag}" 出现在 ${topTag.pct}% 的版本变更中, 是最频繁的变更领域`,
|
|
suggestion: `下次升级重点关注 ${topTag.tag} 相关组件的回归测试`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 修复密度趋势预测
|
|
if (recentVersions.length >= 3) {
|
|
const recent = recentVersions.slice(-3);
|
|
const avgRecent = recent.reduce((s, v) => s + v.totalFixes, 0) / recent.length;
|
|
if (avgRecent > 10) {
|
|
predictions.push({
|
|
risk: 'MEDIUM',
|
|
area: 'complexity',
|
|
reason: `最近 3 个版本平均修复 ${Math.round(avgRecent)} 项, 系统复杂度快速增长`,
|
|
suggestion: '考虑放慢新功能节奏, 增加稳定性/重构投入',
|
|
});
|
|
}
|
|
}
|
|
|
|
// 版本/测试相关预测
|
|
const versionTag = topTags.find(t => t.tag === 'version');
|
|
const testingTag = topTags.find(t => t.tag === 'testing');
|
|
if (versionTag && testingTag) {
|
|
if (versionTag.count > testingTag.count * 1.5) {
|
|
predictions.push({
|
|
risk: 'MEDIUM',
|
|
area: 'testing-gap',
|
|
reason: `版本变更 (${versionTag.count}次) 远多于测试相关变更 (${testingTag.count}次)`,
|
|
suggestion: '增加自动化测试覆盖, 确保每次升级有充分测试',
|
|
});
|
|
}
|
|
}
|
|
|
|
// 安全标签频率
|
|
const securityTag = topTags.find(t => t.tag === 'security');
|
|
if (securityTag && securityTag.pct > 40) {
|
|
predictions.push({
|
|
risk: 'INFO',
|
|
area: 'security',
|
|
reason: `安全相关变更占 ${securityTag.pct}%, 系统安全投入较高`,
|
|
suggestion: '保持当前安全关注度, 考虑引入自动化安全扫描',
|
|
});
|
|
}
|
|
|
|
// 降低修复密度 = 好趋势
|
|
if (isIncreasing) {
|
|
predictions.push({
|
|
risk: 'INFO',
|
|
area: 'improving',
|
|
reason: '最近版本修复数呈下降趋势, 系统稳定性在改善',
|
|
suggestion: '继续保持, 可适当增加新功能投入',
|
|
});
|
|
}
|
|
|
|
// v5.1: 指数平滑预测
|
|
const fixCounts = versionTrend.map(v => v.totalFixes);
|
|
const smoothed = exponentialSmoothing(fixCounts);
|
|
|
|
// v5.1: 标签共现矩阵
|
|
const cooccurrence = buildCooccurrenceMatrix(entries);
|
|
|
|
// v5.1: 尖峰检测
|
|
const spikes = detectSpikes(fixCounts);
|
|
if (spikes.length > 0) {
|
|
predictions.push({
|
|
risk: 'INFO',
|
|
area: 'spike-detection',
|
|
reason: `检测到 ${spikes.length} 个修复密度尖峰`,
|
|
suggestion: '关注尖峰对应版本的变更质量',
|
|
});
|
|
}
|
|
|
|
return {
|
|
totalEntries: entries.length,
|
|
versionRange: `${entries[0].version} -> ${entries[entries.length - 1].version}`,
|
|
dateRange: `${entries[0].ts} ~ ${entries[entries.length - 1].ts}`,
|
|
topTags,
|
|
versionTrend,
|
|
triggers: Object.entries(triggerCounts).sort((a, b) => b[1] - a[1]),
|
|
predictions,
|
|
smoothedForecast: smoothed,
|
|
cooccurrence,
|
|
spikes,
|
|
};
|
|
}
|
|
|
|
// === v5.1: 指数平滑预测 ===
|
|
/**
|
|
* 简单指数平滑, 返回下一期预测值
|
|
* @param {number[]} data - 时间序列数据
|
|
* @param {number} alpha - 平滑系数 (默认 0.4)
|
|
* @returns {{ forecast: number, series: number[] }}
|
|
*/
|
|
function exponentialSmoothing(data, alpha = 0.4) {
|
|
if (data.length === 0) return { forecast: 0, series: [] };
|
|
|
|
const series = [data[0]];
|
|
for (let i = 1; i < data.length; i++) {
|
|
series.push(alpha * data[i] + (1 - alpha) * series[i - 1]);
|
|
}
|
|
|
|
// 下一期预测
|
|
const forecast = Math.round(series[series.length - 1] * 10) / 10;
|
|
return { forecast, series: series.map(v => Math.round(v * 10) / 10) };
|
|
}
|
|
|
|
// === v5.1: 标签共现矩阵 ===
|
|
/**
|
|
* 构建标签共现矩阵
|
|
* @param {Array} entries - evolution-log 条目
|
|
* @returns {Object} { "tagA:tagB" → count }
|
|
*/
|
|
function buildCooccurrenceMatrix(entries) {
|
|
const matrix = {};
|
|
|
|
for (const entry of entries) {
|
|
const tags = entry.tags || [];
|
|
for (let i = 0; i < tags.length; i++) {
|
|
for (let j = i + 1; j < tags.length; j++) {
|
|
const pair = [tags[i], tags[j]].sort().join(':');
|
|
matrix[pair] = (matrix[pair] || 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return matrix;
|
|
}
|
|
|
|
// === v5.1: 尖峰检测 ===
|
|
/**
|
|
* 检测时间序列中的异常尖峰
|
|
* @param {number[]} timeSeries - 时间序列数据
|
|
* @param {number} threshold - Z-score 阈值 (默认 2.0)
|
|
* @returns {Array<{ index: number, value: number, zScore: number }>}
|
|
*/
|
|
function detectSpikes(timeSeries, threshold = 2.0) {
|
|
if (timeSeries.length < 3) return [];
|
|
|
|
const mean = timeSeries.reduce((a, b) => a + b, 0) / timeSeries.length;
|
|
const variance = timeSeries.reduce((s, v) => s + (v - mean) ** 2, 0) / timeSeries.length;
|
|
const stddev = Math.sqrt(variance);
|
|
|
|
if (stddev === 0) return [];
|
|
|
|
const spikes = [];
|
|
for (let i = 0; i < timeSeries.length; i++) {
|
|
const zScore = Math.abs(timeSeries[i] - mean) / stddev;
|
|
if (zScore > threshold) {
|
|
spikes.push({ index: i, value: timeSeries[i], zScore: Math.round(zScore * 100) / 100 });
|
|
}
|
|
}
|
|
|
|
return spikes;
|
|
}
|
|
|
|
// === 输出 ===
|
|
function main() {
|
|
const entries = loadEvolutionLog();
|
|
if (entries.length === 0) {
|
|
console.log('无演进日志数据。');
|
|
return;
|
|
}
|
|
|
|
const report = analyze(entries);
|
|
|
|
if (JSON_MODE) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log('=== 预测性审计报告 ===');
|
|
console.log(`演进记录: ${report.totalEntries} 条 (${report.versionRange})`);
|
|
console.log(`时间跨度: ${report.dateRange}`);
|
|
console.log('');
|
|
|
|
// 标签热力图
|
|
console.log('标签频率 (Top 10):');
|
|
for (const t of report.topTags.slice(0, 10)) {
|
|
const bar = '#'.repeat(Math.min(t.count, 30));
|
|
console.log(` ${t.tag.padEnd(20)} ${String(t.count).padStart(3)} (${String(t.pct).padStart(3)}%) ${bar}`);
|
|
}
|
|
console.log('');
|
|
|
|
// 版本趋势
|
|
console.log('版本修复密度:');
|
|
for (const v of report.versionTrend) {
|
|
const bar = '*'.repeat(Math.min(v.totalFixes, 40));
|
|
console.log(` ${v.version.padEnd(8)} ${String(v.totalFixes).padStart(3)} fixes (${v.entries} entries) ${bar}`);
|
|
}
|
|
console.log('');
|
|
|
|
// 触发源
|
|
console.log('触发源分布:');
|
|
for (const [trigger, count] of report.triggers) {
|
|
console.log(` ${trigger.padEnd(20)} ${count}`);
|
|
}
|
|
console.log('');
|
|
|
|
// 预测
|
|
if (report.predictions.length > 0) {
|
|
console.log('风险预测:');
|
|
for (const p of report.predictions) {
|
|
console.log(` [${p.risk}] ${p.area}: ${p.reason}`);
|
|
console.log(` -> ${p.suggestion}`);
|
|
}
|
|
} else {
|
|
console.log('风险预测: 暂无显著风险信号');
|
|
}
|
|
}
|
|
|
|
// 模块导出 (供测试使用)
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = {
|
|
loadEvolutionLog, analyze, main,
|
|
exponentialSmoothing, buildCooccurrenceMatrix, detectSpikes,
|
|
};
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|