198 lines
7.9 KiB
JavaScript
198 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* audit-all.mjs — Three-Layer Protocol Audit Orchestrator
|
|
*
|
|
* Runs all three protocol audits sequentially:
|
|
* 1. AGENTS.md — Execution Protocol
|
|
* 2. GOVERNANCE.md — Execution Governance
|
|
* 3. PORTFOLIO.md — Portfolio Management
|
|
*
|
|
* Outputs:
|
|
* - Console: colorized summary with PASS / WARN / FAIL
|
|
* - File: audit-report.md (generated in workspace root)
|
|
*
|
|
* Usage:
|
|
* node scripts/audit-all.mjs
|
|
*
|
|
* npm script (add to package.json):
|
|
* "audit": "node scripts/audit-all.mjs"
|
|
*/
|
|
|
|
import { fileURLToPath } from 'url';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const WORKSPACE = path.resolve(__dirname, '..');
|
|
|
|
// Dynamic imports for the three audit modules
|
|
const [agentsMod, govMod, portMod] = await Promise.all([
|
|
import(path.join(__dirname, 'audit-agents.mjs')),
|
|
import(path.join(__dirname, 'audit-governance.mjs')),
|
|
import(path.join(__dirname, 'audit-portfolio.mjs')),
|
|
]);
|
|
|
|
/** @returns {{ layer: string, checks: import('./audit-agents.mjs').CheckResult[] }[]} */
|
|
function runAll() {
|
|
const layers = [
|
|
{ layer: 'Execution Protocol (AGENTS.md)', fn: () => agentsMod.runAgentAudit(WORKSPACE) },
|
|
{ layer: 'Execution Governance (GOVERNANCE.md)', fn: () => govMod.runGovernanceAudit(WORKSPACE) },
|
|
{ layer: 'Portfolio Management (PORTFOLIO.md)', fn: () => portMod.runPortfolioAudit(WORKSPACE) },
|
|
];
|
|
return layers.map(({ layer, fn }) => ({ layer, checks: fn() }));
|
|
}
|
|
|
|
/** Colorized console output */
|
|
function printConsole(results) {
|
|
let totalPass = 0, totalWarn = 0, totalFail = 0, totalChecks = 0;
|
|
|
|
for (const { layer, checks } of results) {
|
|
// Skip summary rows for individual counts
|
|
const realChecks = checks.filter(c => !c.id.endsWith('-SUMMARY'));
|
|
const pass = realChecks.filter(c => c.status === 'PASS').length;
|
|
const warn = realChecks.filter(c => c.status === 'WARN').length;
|
|
const fail = realChecks.filter(c => c.status === 'FAIL').length;
|
|
totalPass += pass;
|
|
totalWarn += warn;
|
|
totalFail += fail;
|
|
totalChecks += realChecks.length;
|
|
|
|
const overall = fail > 0 ? '\x1b[31mFAIL\x1b[0m' : warn > 0 ? '\x1b[33mWARN\x1b[0m' : '\x1b[32mPASS\x1b[0m';
|
|
console.log(`\n\x1b[1m${'='.repeat(60)}\x1b[0m`);
|
|
console.log(`\x1b[1m ${layer}\x1b[0m ${overall}`);
|
|
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
|
|
|
|
for (const r of checks) {
|
|
if (r.id.endsWith('-SUMMARY')) continue;
|
|
const icon = r.status === 'PASS' ? '\x1b[32m✅\x1b[0m' : r.status === 'WARN' ? '\x1b[33m⚠️\x1b[0m' : '\x1b[31m❌\x1b[0m';
|
|
const statusTag = r.status === 'PASS' ? '\x1b[32mPASS\x1b[0m' : r.status === 'WARN' ? '\x1b[33mWARN\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
console.log(` ${icon} [${statusTag}] [${r.id}] ${r.name}`);
|
|
console.log(` ${r.evidence}`);
|
|
if (r.suggestion) console.log(` \x1b[90m→ ${r.suggestion}\x1b[0m`);
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
// Final summary
|
|
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
|
|
console.log(`\x1b[1m THREE-LAYER AUDIT COMPLETE\x1b[0m`);
|
|
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
|
|
console.log(` Total checks: ${totalChecks}`);
|
|
console.log(` \x1b[32mPASS: ${totalPass}\x1b[0m`);
|
|
console.log(` \x1b[33mWARN: ${totalWarn}\x1b[0m`);
|
|
console.log(` \x1b[31mFAIL: ${totalFail}\x1b[0m`);
|
|
|
|
const verdict = totalFail > 0 ? '\x1b[31mFAIL' : totalWarn > 0 ? '\x1b[33mWARN' : '\x1b[32mPASS';
|
|
console.log(`\n VERDICT: ${verdict}\x1b[0m`);
|
|
console.log();
|
|
}
|
|
|
|
/** Generate audit-report.md */
|
|
function writeReport(results) {
|
|
const reportPath = path.join(WORKSPACE, 'audit-report.md');
|
|
|
|
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' +08:00';
|
|
let md = `# Audit Report — ${timestamp}\n\n`;
|
|
md += `> Generated by \`scripts/audit-all.mjs\`\n\n`;
|
|
md += `## Global Summary\n\n`;
|
|
|
|
let totalPass = 0, totalWarn = 0, totalFail = 0, totalChecks = 0;
|
|
|
|
// First pass: aggregate all checks across layers
|
|
const allChecksFlat = [];
|
|
for (const { layer, checks } of results) {
|
|
for (const r of checks) {
|
|
if (r.id.endsWith('-SUMMARY')) continue;
|
|
allChecksFlat.push({ ...r, layer });
|
|
if (r.status === 'PASS') totalPass++;
|
|
else if (r.status === 'WARN') totalWarn++;
|
|
else if (r.status === 'FAIL') totalFail++;
|
|
totalChecks++;
|
|
}
|
|
}
|
|
|
|
md += `| Metric | Value |\n|--------|-------|\n`;
|
|
md += `| Total Checks | ${totalChecks} |\n`;
|
|
md += `| ✅ PASS | ${totalPass} |\n`;
|
|
md += `| ⚠️ WARN | ${totalWarn} |\n`;
|
|
md += `| ❌ FAIL | ${totalFail} |\n`;
|
|
md += `| **Verdict** | **${totalFail > 0 ? '❌ FAIL' : totalWarn > 0 ? '⚠️ WARN' : '✅ PASS'}** |\n\n`;
|
|
|
|
const verdictOverall = totalFail > 0 ? 'FAIL' : totalWarn > 0 ? 'WARN' : 'PASS';
|
|
md += `**Verdict: ${verdictOverall}**\n\n`;
|
|
|
|
// Layer-by-layer tables
|
|
for (const { layer, checks } of results) {
|
|
const real = checks.filter(c => !c.id.endsWith('-SUMMARY'));
|
|
const lp = real.filter(c => c.status === 'PASS').length;
|
|
const lw = real.filter(c => c.status === 'WARN').length;
|
|
const lf = real.filter(c => c.status === 'FAIL').length;
|
|
const lv = lf > 0 ? '❌ FAIL' : lw > 0 ? '⚠️ WARN' : '✅ PASS';
|
|
md += `## ${layer} — ${lv}\n\n`;
|
|
md += `| ID | Name | Status | Evidence | Suggestion |\n`;
|
|
md += `|----|------|--------|----------|------------|\n`;
|
|
|
|
for (const r of real) {
|
|
const icon = r.status === 'PASS' ? '✅' : r.status === 'WARN' ? '⚠️' : '❌';
|
|
const sug = r.suggestion ? r.suggestion.replace(/\|/g, '\\|') : '';
|
|
const ev = r.evidence.replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 120);
|
|
md += `| ${r.id} | ${r.name} | ${icon} ${r.status} | ${ev} | ${sug} |\n`;
|
|
}
|
|
|
|
md += `| **Layer Summary** | **${lp} PASS / ${lw} WARN / ${lf} FAIL** | **${lv}** | | |\n\n`;
|
|
}
|
|
|
|
// Fails list
|
|
const fails = allChecksFlat.filter(c => c.status === 'FAIL');
|
|
if (fails.length > 0) {
|
|
md += `## ❌ Failures (repair required)\n\n`;
|
|
for (const f of fails) {
|
|
md += `- **[${f.id}]** ${f.name} — ${f.evidence}\n`;
|
|
if (f.suggestion) md += ` - *Repair:* ${f.suggestion}\n`;
|
|
}
|
|
md += '\n';
|
|
}
|
|
|
|
// Warnings list
|
|
const warns = allChecksFlat.filter(c => c.status === 'WARN');
|
|
if (warns.length > 0) {
|
|
md += `## ⚠️ Warnings (improvement recommended)\n\n`;
|
|
for (const w of warns) {
|
|
md += `- **[${w.id}]** ${w.name} — ${w.evidence}\n`;
|
|
if (w.suggestion) md += ` - *Suggestion:* ${w.suggestion}\n`;
|
|
}
|
|
md += '\n';
|
|
}
|
|
|
|
// Coverage summary
|
|
md += `## Coverage\n\n`;
|
|
md += `| Layer | Sections | Checks |\n|-------|----------|--------|\n`;
|
|
const agentsReal = results[0].checks.filter(c => !c.id.endsWith('-SUMMARY'));
|
|
const govReal = results[1].checks.filter(c => !c.id.endsWith('-SUMMARY'));
|
|
const portReal = results[2].checks.filter(c => !c.id.endsWith('-SUMMARY'));
|
|
md += `| AGENTS.md | §0-§7, Learning Loop, Context Budget | ${agentsReal.length} |\n`;
|
|
md += `| GOVERNANCE.md | §1-§12 | ${govReal.length} |\n`;
|
|
md += `| PORTFOLIO.md | §1-§14 | ${portReal.length} |\n`;
|
|
md += `| **Total** | **3 layers** | **${totalChecks}** |\n\n`;
|
|
|
|
md += `---\n*Audit complete. ${timestamp}*\n`;
|
|
|
|
fs.writeFileSync(reportPath, md, 'utf8');
|
|
console.log(`\n 📄 Report written to: audit-report.md`);
|
|
return reportPath;
|
|
}
|
|
|
|
// ---- Main ----
|
|
console.log(`\x1b[1m\x1b[36m`);
|
|
console.log(` ╔══════════════════════════════════════════════════╗`);
|
|
console.log(` ║ THREE-LAYER PROTOCOL AUDIT ║`);
|
|
console.log(` ║ AGENTS.md · GOVERNANCE.md · PORTFOLIO.md ║`);
|
|
console.log(` ╚══════════════════════════════════════════════════╝`);
|
|
console.log(`\x1b[0m`);
|
|
|
|
const results = runAll();
|
|
printConsole(results);
|
|
writeReport(results);
|