play.wenpai.net/scripts/trend-tracker.js
elementary-qa 6c8717a27a BackstopJS 稳定性修复 + 一键验收 + 安全基线过滤 + 趋势数据质量
- 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>
2026-02-19 21:51:35 +08:00

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]');
}