Initial project version
Aegis CI / audit (push) Failing after 1m7s
Aegis CI / selfplay (push) Skipped
Aegis CI / perf (push) Failing after 3s
Aegis CI / report (push) Failing after 2s

This commit is contained in:
2026-08-29 01:45:19 +02:00
commit ea2a2f2cbf
21 changed files with 2830 additions and 0 deletions
+187
View File
@@ -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'})`);