Initial project version
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// report.mjs – erzeugt report.html aus audit-result.json, perf-result.json,
|
||||
// selfplay-results.json (falls vorhanden).
|
||||
// Pass/Fail wird direkt aus den audit.assertions und audit.summary gelesen
|
||||
// (authoritative Quelle aus audit.mjs), nicht mit eigenen Heuristiken.
|
||||
//
|
||||
// Copyright (C) 2026 Lila-Kuh
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Usage: node report.mjs
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import url from 'url';
|
||||
|
||||
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, '..');
|
||||
|
||||
const auditPath = path.join(ROOT, 'audit-result.json');
|
||||
const perfPath = path.join(ROOT, 'perf-result.json');
|
||||
const selfPath = path.join(ROOT, 'selfplay-results.json');
|
||||
|
||||
function loadJson(p) {
|
||||
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
||||
}
|
||||
|
||||
const audit = loadJson(auditPath);
|
||||
const perf = loadJson(perfPath);
|
||||
const selfplay = loadJson(selfPath);
|
||||
|
||||
function statusColor(ok) { return ok ? '#4caf50' : '#f44336'; }
|
||||
|
||||
/** true wenn Liste nicht leer ist und alle Assertions `passed: true` haben */
|
||||
function allPass(list) {
|
||||
return Array.isArray(list) && list.length > 0 && list.every(a => a.passed === true);
|
||||
}
|
||||
|
||||
/** Anzahl der bestanden Assertions in einer Liste */
|
||||
function countPassed(list) {
|
||||
return Array.isArray(list) ? list.filter(a => a.passed === true).length : 0;
|
||||
}
|
||||
|
||||
// ─── Audit Summary ────────────────────────────────────────────────────────────
|
||||
let auditRows = '';
|
||||
let auditPass = false;
|
||||
|
||||
if (audit) {
|
||||
const selfA = audit.assertions?.self ?? [];
|
||||
const extA = audit.assertions?.extension ?? [];
|
||||
const redA = audit.assertions?.redesign ?? [];
|
||||
|
||||
// Assertions aus audit.mjs nach Themen gruppieren (Name-Prefix)
|
||||
const conn = selfA.filter(a => /^L\d+ /.test(a.name));
|
||||
const atk = selfA.filter(a => a.name.startsWith('tower '));
|
||||
const skl = selfA.filter(a => a.name.startsWith('skill '));
|
||||
const plc = selfA.filter(a => a.name.startsWith('placement'));
|
||||
|
||||
const rows = [
|
||||
['Connectivity', allPass(conn),
|
||||
`${countPassed(conn)}/${conn.length} level checks`],
|
||||
['Attacks', allPass(atk),
|
||||
`${countPassed(atk)}/${atk.length} tower checks (inkl. Support/Relay)`],
|
||||
['Skills', allPass(skl),
|
||||
`${countPassed(skl)}/${skl.length} skills · rank ≥ 1`],
|
||||
['Placement', allPass(plc),
|
||||
audit.self?.placement
|
||||
? `wall=${audit.self.placement.wallAccepted} path=${audit.self.placement.pathAccepted}`
|
||||
: 'N/A'],
|
||||
];
|
||||
|
||||
if (extA.length > 0) {
|
||||
rows.push(['Extension (Mortar/Tesla/Blocker)', allPass(extA),
|
||||
`${countPassed(extA)}/${extA.length} checks`]);
|
||||
}
|
||||
if (redA.length > 0) {
|
||||
rows.push(['Redesign (Kryo/Sniper/Quantum/Nano)', allPass(redA),
|
||||
`${countPassed(redA)}/${redA.length} checks`]);
|
||||
}
|
||||
|
||||
auditRows = rows.map(([name, ok, detail]) =>
|
||||
`<tr><td>${name}</td>` +
|
||||
`<td style="color:${statusColor(ok)};font-weight:bold">${ok ? '✅ PASS' : '❌ FAIL'}</td>` +
|
||||
`<td>${detail}</td></tr>`
|
||||
).join('\n');
|
||||
|
||||
// Gesamtverdict: authoritative audit.summary + pageErrors
|
||||
const pageErrors = audit.pageErrors?.length ?? 1;
|
||||
auditPass = (audit.summary?.fail ?? -1) === 0 && pageErrors === 0;
|
||||
} else {
|
||||
auditRows = '<tr><td colspan="3" style="color:#999">audit-result.json not found</td></tr>';
|
||||
}
|
||||
|
||||
// ─── Perf Summary ─────────────────────────────────────────────────────────────
|
||||
let perfRow = '';
|
||||
let perfOk = false;
|
||||
if (perf) {
|
||||
perfOk = perf.pass === true;
|
||||
perfRow = `<tr><td>FPS</td>` +
|
||||
`<td style="color:${statusColor(perfOk)};font-weight:bold">${perf.fps} fps</td>` +
|
||||
`<td>target ≥ ${perf.targetFps} · duration ${perf.durationMs}ms</td></tr>`;
|
||||
if (perf.heap) perfRow +=
|
||||
`<tr><td>Heap</td><td style="color:#4caf50">${perf.heap.usedMB} MB</td>` +
|
||||
`<td>total ${perf.heap.totalMB} MB</td></tr>`;
|
||||
} else {
|
||||
perfRow = '<tr><td colspan="3" style="color:#999">perf-result.json not found</td></tr>';
|
||||
}
|
||||
|
||||
// ─── Self-Play Summary ────────────────────────────────────────────────────────
|
||||
let selfRows = '';
|
||||
let selfOk = false;
|
||||
if (selfplay) {
|
||||
const s = selfplay.summary;
|
||||
selfOk = s.winRate >= 50;
|
||||
selfRows = `
|
||||
<tr><td>Episodes</td><td style="color:#4caf50">${s.total}</td><td>winRate ${s.winRate.toFixed(1)}%</td></tr>
|
||||
<tr><td>Wins</td><td style="color:${statusColor(selfOk)};font-weight:bold">${s.wins}</td><td>Losses: ${s.losses}</td></tr>
|
||||
<tr><td>Avg Kills</td><td style="color:#4caf50">${s.avgKills}</td><td>Avg Wave: ${s.avgWaves}</td></tr>
|
||||
`;
|
||||
} else {
|
||||
selfRows = '<tr><td colspan="3" style="color:#999">selfplay-results.json not found (nightly only)</td></tr>';
|
||||
}
|
||||
|
||||
const overallPass = auditPass && perfOk && (selfplay ? selfOk : true);
|
||||
|
||||
// ─── HTML ─────────────────────────────────────────────────────────────────────
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Aegis Labyrinth – CI Report</title>
|
||||
<style>
|
||||
body{font-family:'Segoe UI',system-ui,monospace;background:#0d1117;color:#c9d1d9;padding:30px;margin:0}
|
||||
h1{font-size:1.6em;margin-bottom:4px}
|
||||
.ts{color:#8b949e;font-size:.85em;margin-bottom:24px}
|
||||
.badge{display:inline-block;padding:4px 14px;border-radius:12px;font-weight:bold;font-size:.9em;margin-bottom:20px}
|
||||
.pass{background:#1a3a2a;color:#4caf50}.fail{background:#3a1a1a;color:#f44336}
|
||||
h2{font-size:1.1em;margin:24px 0 8px;color:#8b949e;text-transform:uppercase;letter-spacing:1px}
|
||||
table{border-collapse:collapse;width:100%;margin-bottom:20px}
|
||||
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #21262d}
|
||||
th{background:#161b22;color:#8b949e;font-size:.85em;text-transform:uppercase;letter-spacing:.5px}
|
||||
tr:hover{background:#161b22}
|
||||
.footer{color:#484f58;font-size:.8em;margin-top:40px;border-top:1px solid #21262d;padding-top:16px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⚔️ Aegis Labyrinth – CI Report</h1>
|
||||
<p class="ts">Timestamp: ${new Date().toISOString()} · Branch: <code>unknown</code></p>
|
||||
<span class="badge ${overallPass ? 'pass' : 'fail'}">${overallPass ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}</span>
|
||||
|
||||
<h2>Audits</h2>
|
||||
<table>
|
||||
<tr><th>Check</th><th>Status</th><th>Detail</th></tr>
|
||||
${auditRows}
|
||||
</table>
|
||||
|
||||
<h2>Performance</h2>
|
||||
<table>
|
||||
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
|
||||
${perfRow}
|
||||
</table>
|
||||
|
||||
<h2>Self-Play</h2>
|
||||
<table>
|
||||
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
|
||||
${selfRows}
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
Generated by <code>report.mjs</code> · Aegis Labyrinth v${audit ? '1.0.2' : '?'} · ${new Date().toLocaleString('de-DE')}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const reportPath = path.join(ROOT, 'report.html');
|
||||
fs.writeFileSync(reportPath, html);
|
||||
console.log(`✅ Report generated → ${reportPath} (${overallPass ? 'ALL PASS' : 'SOME FAILURES'})`);
|
||||
Reference in New Issue
Block a user