// 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

${auditRows}
CheckStatusDetail

Performance

${perfRow}
MetricValueInfo

Self-Play

${selfRows}
MetricValueInfo
`; const reportPath = path.join(ROOT, 'report.html'); fs.writeFileSync(reportPath, html); console.log(`✅ Report generated → ${reportPath} (${overallPass ? 'ALL PASS' : 'SOME FAILURES'})`);