#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // EOS Engineering Readiness v3 — Viktade områden // Erik-krav: "En hög poäng i ett mindre kritiskt område ska inte kompensera för saknad deployprocess" // ═══════════════════════════════════════════════════════════════════════════ import { execSync } from 'child_process'; import { existsSync, writeFileSync } from 'fs'; const READINESS_PATH = '/home/bernt/.openclaw/workspace/EOS/readiness-report-v3.json'; /** * Viktade områden — kritiska områden väger tyngre */ const READINESS_AREAS = [ { id: 'git', name: 'Git Discipline', weight: 0.20, // 20% target: 90, rationale: 'Allt börjar med Git. Utan versionshantering finns ingen spårbarhet.', measures: [ { name: 'Ocommitade filer', check: () => { try { const status = execSync('git status --short', { cwd: '/home/bernt/repos/quixzoom.com', encoding: 'utf8' }); const uncommitted = status.trim().split('\n').filter(l => l.trim()).length; const score = Math.max(0, 100 - uncommitted * 2); return { score, evidence: `${uncommitted} ocommitade filer`, blocker: uncommitted > 10, action: uncommitted > 0 ? `Commita eller .gitignore ${uncommitted} filer` : null }; } catch { return { score: 0, evidence: 'Kunde inte köra git status', blocker: true, action: 'Kontrollera git-konfiguration' }; } } }, { name: 'Git-konfiguration', check: () => { const hasGit = existsSync('/home/bernt/repos/quixzoom.com/.git'); return { score: hasGit ? 100 : 0, evidence: hasGit ? 'Git-repo hittat' : 'Inget Git-repo', blocker: !hasGit, action: !hasGit ? 'Initiera git-repo' : null }; } }, { name: 'Branch-skydd', check: () => { return { score: 0, evidence: 'Branch-skydd ej verifierat', blocker: true, action: 'Konfigurera branch-skydd på GitHub/GitLab' }; } } ] }, { id: 'cicd', name: 'CI/CD', weight: 0.20, // 20% target: 90, rationale: 'Utan automatiserad pipeline är deployment felbenägen och ospårbar.', measures: [ { name: 'CI/CD pipeline', check: () => { const hasCI = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') || existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml'); return { score: hasCI ? 100 : 0, evidence: hasCI ? 'CI/CD hittad' : 'Ingen CI/CD', blocker: !hasCI, action: !hasCI ? 'Sätt upp GitHub Actions eller GitLab CI' : null }; } }, { name: 'Automatiserade tester i pipeline', check: () => { return { score: 0, evidence: 'Ej verifierat', blocker: true, action: 'Lägg till test-steg i CI-pipeline' }; } } ] }, { id: 'backup', name: 'Backup & Rollback', weight: 0.15, // 15% target: 80, rationale: 'Förmågan att återställa vid fel är avgörande för produktionssäkerhet.', measures: [ { name: 'Backup-procedur', check: () => { const hasBackup = existsSync('/home/bernt/repos/quixzoom.com/scripts/backup.sh'); return { score: hasBackup ? 100 : 0, evidence: hasBackup ? 'Backup-script hittat' : 'Inget backup-script', blocker: !hasBackup, action: !hasBackup ? 'Skapa backup-script för databas' : null }; } }, { name: 'Rollback-procedur', check: () => { const hasRollback = existsSync('/home/bernt/repos/quixzoom.com/scripts/rollback.sh'); return { score: hasRollback ? 100 : 0, evidence: hasRollback ? 'Rollback-script hittat' : 'Inget rollback-script', blocker: !hasRollback, action: !hasRollback ? 'Skapa rollback-script' : null }; } } ] }, { id: 'infrastructure', name: 'Infrastructure as Code', weight: 0.15, // 15% target: 80, rationale: 'Manuell infrastruktur är ospårbar och kan inte reproduceras.', measures: [ { name: 'IaC-verktyg', check: () => { const hasIaC = existsSync('/home/bernt/repos/quixzoom.com/terraform') || existsSync('/home/bernt/repos/quixzoom.com/cloudformation') || existsSync('/home/bernt/repos/quixzoom.com/cdk'); return { score: hasIaC ? 100 : 0, evidence: hasIaC ? 'IaC hittad' : 'Ingen IaC', blocker: !hasIaC, action: !hasIaC ? 'Skapa Terraform/CloudFormation för AWS-resurser' : null }; } }, { name: 'Miljökonfiguration', check: () => { const hasEnv = existsSync('/home/bernt/repos/quixzoom.com/.env.example'); return { score: hasEnv ? 100 : 0, evidence: hasEnv ? '.env.example hittad' : 'Ingen .env.example', blocker: false, action: !hasEnv ? 'Skapa .env.example' : null }; } } ] }, { id: 'testing', name: 'Testing', weight: 0.10, // 10% target: 80, rationale: 'Tester ger förtroende för att ändringar inte bryter befintlig funktionalitet.', measures: [ { name: 'Tester', check: () => { const hasTests = existsSync('/home/bernt/repos/quixzoom.com/tests') || existsSync('/home/bernt/repos/quixzoom.com/__tests__'); return { score: hasTests ? 100 : 0, evidence: hasTests ? 'Tester hittade' : 'Inga tester', blocker: !hasTests, action: !hasTests ? 'Skapa första testerna' : null }; } }, { name: 'Test-konfiguration', check: () => { const hasConfig = existsSync('/home/bernt/repos/quixzoom.com/jest.config.js') || existsSync('/home/bernt/repos/quixzoom.com/vitest.config.ts'); return { score: hasConfig ? 100 : 0, evidence: hasConfig ? 'Test-config hittad' : 'Ingen test-config', blocker: !hasConfig, action: !hasConfig ? 'Konfigurera Jest/Vitest' : null }; } } ] }, { id: 'security', name: 'Security', weight: 0.10, // 10% target: 70, rationale: 'Säkerhet är inte en feature utan en förutsättning.', measures: [ { name: 'Dependency scanning', check: () => { return { score: 0, evidence: 'Ej verifierat', blocker: false, action: 'Lägg till npm audit i CI' }; } }, { name: 'Secret-hantering', check: () => { const hasEnv = existsSync('/home/bernt/repos/quixzoom.com/.env.example'); return { score: hasEnv ? 50 : 0, evidence: hasEnv ? '.env.example finns' : 'Ingen secret-hantering', blocker: false, action: 'Använd AWS Secrets Manager eller liknande' }; } } ] }, { id: 'api-contracts', name: 'API Contracts', weight: 0.05, // 5% target: 80, rationale: 'API-kontrakt dokumenterar gränssnitt och förhindrar brytande ändringar.', measures: [ { name: 'OpenAPI-spec', check: () => { const hasOpenAPI = existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') || existsSync('/home/bernt/repos/quixzoom.com/openapi.json'); return { score: hasOpenAPI ? 100 : 0, evidence: hasOpenAPI ? 'OpenAPI hittad' : 'Ingen OpenAPI', blocker: !hasOpenAPI, action: !hasOpenAPI ? 'Skapa OpenAPI-spec för alla endpoints' : null }; } }, { name: 'API-dokumentation', check: () => { const hasDocs = existsSync('/home/bernt/repos/quixzoom.com/API.md') || existsSync('/home/bernt/repos/quixzoom.com/docs/api.md'); return { score: hasDocs ? 100 : 0, evidence: hasDocs ? 'API-docs hittad' : 'Ingen API-dokumentation', blocker: false, action: !hasDocs ? 'Skapa API-dokumentation' : null }; } } ] }, { id: 'database', name: 'DB Migrations', weight: 0.03, // 3% target: 90, rationale: 'Migrationer säkerställer att databasändringar är reproducerbara.', measures: [ { name: 'Migrationer', check: () => { const hasMigrations = existsSync('/home/bernt/repos/quixzoom.com/migrations') || existsSync('/home/bernt/repos/quixzoom.com/prisma'); return { score: hasMigrations ? 100 : 0, evidence: hasMigrations ? 'Migrationer hittade' : 'Inga migrationer', blocker: !hasMigrations, action: !hasMigrations ? 'Sätt upp Prisma eller liknande migrationsverktyg' : null }; } }, { name: 'Migration i CI', check: () => { return { score: 0, evidence: 'Ej verifierat', blocker: true, action: 'Lägg till migration-steg i CI-pipeline' }; } } ] }, { id: 'observability', name: 'Observability', weight: 0.01, // 1% target: 70, rationale: 'Observerbarhet hjälper till att diagnostisera problem i produktion.', measures: [ { name: 'Loggning', check: () => { const hasLogging = existsSync('/home/bernt/repos/quixzoom.com/src/lib/logger.ts') || existsSync('/home/bernt/repos/quixzoom.com/src/utils/logger.js'); return { score: hasLogging ? 100 : 0, evidence: hasLogging ? 'Logger hittad' : 'Ingen logger', blocker: false, action: !hasLogging ? 'Skapa central logger' : null }; } }, { name: 'Health checks', check: () => { const hasHealth = existsSync('/home/bernt/repos/quixzoom.com/src/routes/health.ts') || existsSync('/home/bernt/repos/quixzoom.com/src/api/health.js'); return { score: hasHealth ? 100 : 0, evidence: hasHealth ? 'Health endpoint hittad' : 'Ingen health endpoint', blocker: false, action: !hasHealth ? 'Skapa /health endpoint' : null }; } } ] }, { id: 'agent-safety', name: 'Agent Safety', weight: 0.01, // 1% target: 100, rationale: 'EOS måste skydda sig själv. Agent Safety är metasäkerhet.', measures: [ { name: 'EOS-kontrakt', check: () => { const hasContract = existsSync('/home/bernt/.openclaw/workspace/EOS/engineering-contract.mjs'); return { score: hasContract ? 100 : 0, evidence: hasContract ? 'EOS-kontrakt hittat' : 'Inget EOS-kontrakt', blocker: !hasContract, action: !hasContract ? 'Skapa EOS-kontrakt' : null }; } }, { name: 'Immutable Truth', check: () => { const hasTruth = existsSync('/home/bernt/.openclaw/workspace/EOS/immutable-truth.mjs'); return { score: hasTruth ? 100 : 0, evidence: hasTruth ? 'Immutable Truth hittad' : 'Ingen Immutable Truth', blocker: !hasTruth, action: !hasTruth ? 'Skapa Immutable Truth-validering' : null }; } } ] } ]; class EngineeringReadinessV3 { constructor() { this.areas = READINESS_AREAS; } calculate() { console.log('📊 Beräknar Engineering Readiness v3 (viktad)...\n'); const results = []; let totalScore = 0; let totalWeight = 0; let totalBlockers = 0; for (const area of this.areas) { const areaResult = this.calculateArea(area); results.push(areaResult); totalScore += areaResult.score * area.weight; totalWeight += area.weight; totalBlockers += areaResult.blockers.length; } const overallScore = Math.round(totalScore / totalWeight); const report = { timestamp: new Date().toISOString(), overall: overallScore, totalBlockers, areas: results, thresholds: { critical: 60, warning: 80, ready: 90 }, status: overallScore >= 90 ? 'READY' : overallScore >= 60 ? 'WARNING' : 'CRITICAL' }; this.saveReport(report); this.printReport(report); return report; } calculateArea(area) { let areaScore = 0; const measures = []; const blockers = []; const actions = []; for (const measure of area.measures) { const result = measure.check(); measures.push({ name: measure.name, score: result.score, evidence: result.evidence, blocker: result.blocker, action: result.action }); areaScore += result.score; if (result.blocker) { blockers.push({ measure: measure.name, evidence: result.evidence, action: result.action }); } if (result.action) { actions.push(result.action); } } const avgScore = Math.round(areaScore / area.measures.length); return { id: area.id, name: area.name, weight: area.weight, target: area.target, score: avgScore, gap: area.target - avgScore, rationale: area.rationale, measures, blockers, actions, status: avgScore >= area.target ? 'READY' : avgScore >= 60 ? 'WARNING' : 'CRITICAL' }; } saveReport(report) { writeFileSync(READINESS_PATH, JSON.stringify(report, null, 2)); } printReport(report) { console.log('╔═══════════════════════════════════════════════════════════════╗'); console.log('║ ENGINEERING READINESS v3 ║'); console.log('║ Viktad — kritiska områden väger tyngre ║'); console.log('╚═══════════════════════════════════════════════════════════════╝'); console.log(); const overallIcon = report.status === 'READY' ? '✅' : report.status === 'WARNING' ? '⚠️' : '🔴'; console.log(`${overallIcon} ÖVERGRIPANDE: ${report.overall}%`); console.log(` Status: ${report.status}`); console.log(` Blockerare: ${report.totalBlockers}`); console.log(); console.log('📋 PER OMRÅDE (sorterat efter vikt)\n'); // Sortera efter vikt (högst först) const sortedAreas = [...report.areas].sort((a, b) => b.weight - a.weight); for (const area of sortedAreas) { const icon = area.status === 'READY' ? '✅' : area.status === 'WARNING' ? '⚠️' : '🔴'; const bar = '█'.repeat(area.score / 5) + '░'.repeat(20 - area.score / 5); const weightPct = Math.round(area.weight * 100); console.log(`${icon} ${area.name} (${weightPct}%)`); console.log(` [${bar}] ${area.score}% (mål: ${area.target}%)`); console.log(` ${area.rationale}`); if (area.blockers.length > 0) { console.log(` 🔴 Blockerare:`); for (const blocker of area.blockers) { console.log(` • ${blocker.measure}: ${blocker.evidence}`); console.log(` Åtgärd: ${blocker.action}`); } } console.log(); } console.log('🎯 TRÖSKLAR\n'); console.log(` Critical: <${report.thresholds.critical}%`); console.log(` Warning: <${report.thresholds.warning}%`); console.log(` Ready: ≥${report.thresholds.ready}%`); console.log(); console.log('💡 REKOMMENDATION\n'); if (report.status === 'CRITICAL') { console.log(' 🔴 STOP — Agenter får INTE utföra ändringar'); console.log(` Åtgärda ${report.totalBlockers} blockerare innan fortsatt arbete`); } else if (report.status === 'WARNING') { console.log(' ⚠️ WARNING — Begränsad agent-aktivitet'); } else { console.log(' ✅ READY — Agenter kan arbeta normalt'); } console.log(); console.log('═══════════════════════════════════════════════════════════════\n'); } } // ── Main ────────────────────────────────────────────────────────────────── const readiness = new EngineeringReadinessV3(); readiness.calculate();