// 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 .
//
// 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]) =>
`
| ${name} | ` +
`${ok ? '✅ PASS' : '❌ FAIL'} | ` +
`${detail} |
`
).join('\n');
// Gesamtverdict: authoritative audit.summary + pageErrors
const pageErrors = audit.pageErrors?.length ?? 1;
auditPass = (audit.summary?.fail ?? -1) === 0 && pageErrors === 0;
} else {
auditRows = '| audit-result.json not found |
';
}
// ─── Perf Summary ─────────────────────────────────────────────────────────────
let perfRow = '';
let perfOk = false;
if (perf) {
perfOk = perf.pass === true;
perfRow = `| FPS | ` +
`${perf.fps} fps | ` +
`target ≥ ${perf.targetFps} · duration ${perf.durationMs}ms |
`;
if (perf.heap) perfRow +=
`| Heap | ${perf.heap.usedMB} MB | ` +
`total ${perf.heap.totalMB} MB |
`;
} else {
perfRow = '| perf-result.json not found |
';
}
// ─── Self-Play Summary ────────────────────────────────────────────────────────
let selfRows = '';
let selfOk = false;
if (selfplay) {
const s = selfplay.summary;
selfOk = s.winRate >= 50;
selfRows = `
| Episodes | ${s.total} | winRate ${s.winRate.toFixed(1)}% |
| Wins | ${s.wins} | Losses: ${s.losses} |
| Avg Kills | ${s.avgKills} | Avg Wave: ${s.avgWaves} |
`;
} else {
selfRows = '| selfplay-results.json not found (nightly only) |
';
}
const overallPass = auditPass && perfOk && (selfplay ? selfOk : true);
// ─── HTML ─────────────────────────────────────────────────────────────────────
const html = `
Aegis Labyrinth – CI Report
⚔️ Aegis Labyrinth – CI Report
Timestamp: ${new Date().toISOString()} · Branch: unknown
${overallPass ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}
Audits
| Check | Status | Detail |
${auditRows}
Performance
| Metric | Value | Info |
${perfRow}
Self-Play
| Metric | Value | Info |
${selfRows}
`;
const reportPath = path.join(ROOT, 'report.html');
fs.writeFileSync(reportPath, html);
console.log(`✅ Report generated → ${reportPath} (${overallPass ? 'ALL PASS' : 'SOME FAILURES'})`);