- Justfile: Playwright 和 BackstopJS 之间加 sleep+pkill 避免 Chromium 资源竞争 - Justfile: 新增 fetch-and-test recipe(Forgejo 拉取→Playground→验收→趋势导入) - generate-report.js: 安全扫描过滤 WordPress 核心已知信息泄露(readme.html等) - trend-tracker.js: null 值正确存为 SQL NULL,compare 跳过未测维度 - acceptance-criteria.json: 新增 knownWpCoreLeaks 忽略列表 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
5.3 KiB
JavaScript
119 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// 验收结果趋势追踪 — SQLite 存储 + 查询
|
|
// 用法:
|
|
// node scripts/trend-tracker.js init 初始化数据库
|
|
// node scripts/trend-tracker.js import <report.json> 导入验收报告
|
|
// node scripts/trend-tracker.js query <plugin> [limit] 查询趋势
|
|
// node scripts/trend-tracker.js compare <plugin> 对比最近两次
|
|
|
|
const { execFileSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DB_PATH = path.join(process.env.HOME, 'data', 'acceptance-trends.db');
|
|
const DB_DIR = path.dirname(DB_PATH);
|
|
|
|
function sql(query, opts = []) {
|
|
return execFileSync('sqlite3', [...opts, DB_PATH, query], { encoding: 'utf-8' }).trim();
|
|
}
|
|
|
|
function init() {
|
|
if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
|
|
sql(`CREATE TABLE IF NOT EXISTS runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plugin TEXT NOT NULL,
|
|
version TEXT,
|
|
run_date TEXT DEFAULT (datetime('now','localtime')),
|
|
lh_performance INTEGER,
|
|
lh_accessibility INTEGER,
|
|
lh_best_practices INTEGER,
|
|
lh_seo INTEGER,
|
|
security_high INTEGER DEFAULT 0,
|
|
security_medium INTEGER DEFAULT 0,
|
|
security_low INTEGER DEFAULT 0,
|
|
a11y_violations INTEGER,
|
|
i18n_coverage REAL,
|
|
i18n_overflow INTEGER,
|
|
html_errors INTEGER,
|
|
broken_links INTEGER,
|
|
visual_diff_pct REAL,
|
|
overall_pass INTEGER,
|
|
overall_warn INTEGER,
|
|
overall_fail INTEGER,
|
|
report_path TEXT,
|
|
notes TEXT
|
|
)`);
|
|
console.log('数据库已初始化:', DB_PATH);
|
|
}
|
|
|
|
function importReport(reportPath) {
|
|
const raw = fs.readFileSync(reportPath, 'utf-8');
|
|
const data = JSON.parse(raw);
|
|
const p = data.plugin || path.basename(reportPath, '.json');
|
|
const v = data.version || 'unknown';
|
|
const lh = data.lighthouse || {};
|
|
const sec = data.security || {};
|
|
const i18n = data.i18n || {};
|
|
const vals = [
|
|
p, v,
|
|
lh.performance ?? null, lh.accessibility ?? null, lh.bestPractices ?? null, lh.seo ?? null,
|
|
sec.high ?? 0, sec.medium ?? 0, sec.low ?? 0,
|
|
data.a11yViolations ?? null, i18n.coverage ?? null, i18n.overflow ?? null,
|
|
data.htmlErrors ?? null, data.brokenLinks ?? null, data.visualDiffPct ?? null,
|
|
data.pass ?? null, data.warn ?? null, data.fail ?? null,
|
|
reportPath, data.notes || ''
|
|
];
|
|
const placeholders = vals.map(v =>
|
|
v === null ? 'NULL' : `'${String(v).replace(/'/g, "''")}'`
|
|
).join(',');
|
|
sql(`INSERT INTO runs (plugin,version,lh_performance,lh_accessibility,lh_best_practices,lh_seo,security_high,security_medium,security_low,a11y_violations,i18n_coverage,i18n_overflow,html_errors,broken_links,visual_diff_pct,overall_pass,overall_warn,overall_fail,report_path,notes) VALUES (${placeholders})`);
|
|
console.log(`已导入: ${p} ${v}`);
|
|
}
|
|
|
|
function query(plugin, limit = 10) {
|
|
const rows = sql(`SELECT version, run_date, lh_performance, lh_accessibility, lh_best_practices, lh_seo, security_high, i18n_coverage, overall_pass, overall_warn, overall_fail FROM runs WHERE plugin='${plugin.replace(/'/g, "''")}' ORDER BY run_date DESC LIMIT ${parseInt(limit)}`);
|
|
if (!rows) { console.log('无记录'); return; }
|
|
console.log('版本 | 日期 | Perf | A11y | BP | SEO | 高危 | i18n | 通过/警告/失败');
|
|
console.log('-'.repeat(85));
|
|
rows.split('\n').forEach(row => {
|
|
const [ver, date, perf, a11y, bp, seo, sec, i18n, pass, warn, fail] = row.split('|');
|
|
console.log(`${(ver||'?').padEnd(8)} | ${date} | ${(perf||'-').padStart(4)} | ${(a11y||'-').padStart(4)} | ${(bp||'-').padStart(4)} | ${(seo||'-').padStart(4)} | ${(sec||'0').padStart(4)} | ${(i18n||'-').padStart(5)} | ${pass||0}/${warn||0}/${fail||0}`);
|
|
});
|
|
}
|
|
|
|
function compare(plugin) {
|
|
const rows = sql(`SELECT * FROM runs WHERE plugin='${plugin.replace(/'/g, "''")}' ORDER BY run_date DESC LIMIT 2`);
|
|
if (!rows) { console.log('不足两次记录,无法对比'); return; }
|
|
const lines = rows.split('\n').filter(Boolean);
|
|
if (lines.length < 2) { console.log('仅一次记录,无法对比'); return; }
|
|
const cols = 'id|plugin|version|run_date|lh_perf|lh_a11y|lh_bp|lh_seo|sec_h|sec_m|sec_l|a11y|i18n_cov|i18n_of|html_err|links|vis_diff|pass|warn|fail|path|notes'.split('|');
|
|
const latest = lines[0].split('|');
|
|
const prev = lines[1].split('|');
|
|
console.log(`对比: ${latest[2]} (${latest[3]}) vs ${prev[2]} (${prev[3]})\n`);
|
|
const numCols = [4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19];
|
|
for (const i of numCols) {
|
|
const aRaw = latest[i], bRaw = prev[i];
|
|
// 跳过 NULL/空值(未测维度不参与对比)
|
|
if (!aRaw && !bRaw) continue;
|
|
if (!aRaw) { console.log(` ${cols[i]}: ${bRaw} → (未测)`); continue; }
|
|
if (!bRaw) { console.log(` ${cols[i]}: (未测) → ${aRaw}`); continue; }
|
|
const a = parseFloat(aRaw), b = parseFloat(bRaw);
|
|
if (isNaN(a) || isNaN(b)) continue;
|
|
const diff = a - b;
|
|
if (diff !== 0) {
|
|
const arrow = diff > 0 ? '↑' : '↓';
|
|
console.log(` ${cols[i]}: ${b} → ${a} (${arrow}${Math.abs(diff).toFixed(1)})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// CLI
|
|
const [,, cmd, ...args] = process.argv;
|
|
switch (cmd) {
|
|
case 'init': init(); break;
|
|
case 'import': importReport(args[0]); break;
|
|
case 'query': query(args[0], args[1]); break;
|
|
case 'compare': compare(args[0]); break;
|
|
default:
|
|
console.log('用法: trend-tracker.js <init|import|query|compare> [args]');
|
|
}
|