#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // SIL Decision Engine — Beslutsunderlag för PR:er // Erik-krav: "Inte en kodgenerator. En beslutsmotor." // ═══════════════════════════════════════════════════════════════════════════ import { readFileSync, existsSync } from 'fs'; const GRAPH_PATH = '/home/bernt/.openclaw/workspace/SYSTEM_GRAPH.json'; export class DecisionEngine { constructor() { this.graph = this.loadGraph(); } loadGraph() { try { return JSON.parse(readFileSync(GRAPH_PATH, 'utf8')); } catch { return { nodes: [], edges: [] }; } } /** * Generera beslutsunderlag för en PR */ generateDecision(analysis) { const riskScore = this.calculateRiskScore(analysis); const blastRadius = this.calculateBlastRadius(analysis); const confidence = this.calculateOverallConfidence(analysis); const requiredTests = this.identifyRequiredTests(analysis); const deployment = this.assessDeployment(analysis); const rollback = this.assessRollback(analysis); const decision = { timestamp: new Date().toISOString(), analysisId: analysis.analysisId, // Sammanfattning summary: { riskScore: `${riskScore}/100`, riskLevel: this.riskLevel(riskScore), blastRadius, confidence: `${Math.round(confidence * 100)}%`, recommendation: this.generateRecommendation(riskScore, confidence, blastRadius) }, // Detaljer details: { risk: this.explainRisk(analysis, riskScore), blastRadius: this.explainBlastRadius(blastRadius), confidence: this.explainConfidence(analysis, confidence), tests: requiredTests, deployment, rollback }, // Åtgärder actions: this.generateActions(riskScore, confidence, requiredTests) }; return decision; } /** * Beräkna risk-score (0-100) */ calculateRiskScore(analysis) { let score = 0; // Risker från analysen const risks = analysis.risks || []; for (const risk of risks) { if (risk.level === 'high') score += 25; else if (risk.level === 'medium') score += 10; else score += 5; } // Breaking changes const breaking = analysis.breakingChanges || []; score += breaking.length * 15; // Kritiska komponenter const critical = (analysis.changedComponents || []).filter(c => c.critical); score += critical.length * 20; // Databasändringar const dbChanges = (analysis.impact?.databases || []).length; score += dbChanges * 10; // Externa beroenden const external = (analysis.impact?.external || []).length; score += external * 5; // Osäkerhet (från uncertainty-modulen) const unknownCount = analysis.uncertaintySummary?.unknown || 0; score += unknownCount * 10; return Math.min(100, score); } riskLevel(score) { if (score >= 70) return 'HIGH'; if (score >= 40) return 'MEDIUM'; if (score >= 20) return 'LOW'; return 'MINIMAL'; } /** * Beräkna blast radius */ calculateBlastRadius(analysis) { const impact = analysis.impact || {}; return { services: (impact.services || []).length, databases: (impact.databases || []).length, external: (impact.external || []).length, businessProcesses: (analysis.businessImpact || []).length, total: (impact.services || []).length + (impact.databases || []).length + (impact.external || []).length }; } /** * Beräkna övergripande confidence */ calculateOverallConfidence(analysis) { const evidence = analysis.evidence || {}; const baseConfidence = evidence.confidence || 0.5; // Justera för osäkerhet const uncertainty = analysis.uncertaintySummary || {}; const total = uncertainty.total || 1; const certain = uncertainty.certain || 0; if (total > 0) { return baseConfidence * (certain / total); } return baseConfidence; } /** * Identifiera nödvändiga tester */ identifyRequiredTests(analysis) { const tests = []; const impact = analysis.impact || {}; // Tester för påverkade services for (const service of impact.services || []) { tests.push({ type: 'integration', target: service.label, priority: service.critical ? 'high' : 'medium', reason: `Service ${service.label} är direkt påverkad` }); if (service.critical) { tests.push({ type: 'regression', target: service.label, priority: 'high', reason: 'Kritisk service — kräver regressionstest' }); } } // Databastester if (impact.databases?.length > 0) { tests.push({ type: 'migration', target: impact.databases.map(d => d.label).join(', '), priority: 'high', reason: 'Databasändring kräver migrationstest' }); } // Externa integrationer for (const ext of impact.external || []) { tests.push({ type: 'integration', target: ext.label, priority: 'medium', reason: `Extern integration ${ext.label} kan påverkas` }); } // Affärsflödestester for (const bi of analysis.businessImpact || []) { tests.push({ type: 'e2e', target: bi.process, priority: 'medium', reason: `Affärsprocess ${bi.process} påverkas` }); } return tests; } /** * Bedöm deployment-strategi */ assessDeployment(analysis) { const riskScore = this.calculateRiskScore(analysis); if (riskScore >= 70) { return { strategy: 'REJECT', reason: 'För hög risk — kräver omarbetning', canary: false, featureFlag: false }; } if (riskScore >= 40) { return { strategy: 'CANARY', reason: 'Medium risk — kanary-deployment rekommenderas', canary: true, featureFlag: true }; } if (riskScore >= 20) { return { strategy: 'FEATURE_FLAG', reason: 'Låg risk — feature flag tillräcklig', canary: false, featureFlag: true }; } return { strategy: 'DIRECT', reason: 'Minimal risk — direkt deployment OK', canary: false, featureFlag: false }; } /** * Bedöm rollback-möjligheter */ assessRollback(analysis) { const hasDbChanges = (analysis.impact?.databases || []).length > 0; const hasBreaking = (analysis.breakingChanges || []).length > 0; if (hasDbChanges && hasBreaking) { return { available: false, reason: 'Databasändring + breaking changes = komplex rollback', complexity: 'HIGH', estimatedTime: '30-60 min' }; } if (hasDbChanges) { return { available: true, reason: 'Rollback-script krävs för databas', complexity: 'MEDIUM', estimatedTime: '10-20 min' }; } if (hasBreaking) { return { available: true, reason: 'Feature flag eller revert av commit', complexity: 'LOW', estimatedTime: '2-5 min' }; } return { available: true, reason: 'Enkel revert av commit', complexity: 'LOW', estimatedTime: '< 2 min' }; } /** * Generera rekommendation */ generateRecommendation(riskScore, confidence, blastRadius) { if (riskScore >= 70) { return 'REJECT — Risken är för hög. Omarbeta eller dela upp PR:en.'; } if (confidence < 0.5) { return 'REQUIRE_REVIEW — För låg confidence för auto-approve.'; } if (blastRadius.total >= 5) { return 'APPROVE_AFTER_TESTS — Stor påverkan, kräver full test-suite.'; } if (riskScore >= 40) { return 'APPROVE_WITH_CAUTION — Medium risk, kanary-deployment.'; } return 'APPROVE — Låg risk, kan deployas direkt.'; } /** * Generera åtgärder */ generateActions(riskScore, confidence, tests) { const actions = []; if (riskScore >= 70) { actions.push({ type: 'BLOCK', message: 'Blockera merge — risken är för hög' }); } if (confidence < 0.7) { actions.push({ type: 'REQUIRE_REVIEW', message: 'Kräv manuell review — confidence är för låg' }); } for (const test of tests.filter(t => t.priority === 'high')) { actions.push({ type: 'REQUIRE_TEST', message: `Kräv test: ${test.type} — ${test.target}` }); } if (tests.length > 0) { actions.push({ type: 'RUN_TESTS', message: `Kör ${tests.length} tester innan merge` }); } return actions; } // ── Explainers ────────────────────────────────────────────────────────── explainRisk(analysis, score) { const level = this.riskLevel(score); const factors = []; if (analysis.risks?.length > 0) { factors.push(`${analysis.risks.length} risker identifierade`); } if (analysis.breakingChanges?.length > 0) { factors.push(`${analysis.breakingChanges.length} breaking changes`); } if (analysis.impact?.databases?.length > 0) { factors.push(`${analysis.impact.databases.length} databaser påverkas`); } return { score, level, factors, explanation: `Risknivå: ${level}. ${factors.join(', ')}.` }; } explainBlastRadius(blastRadius) { if (blastRadius.total === 0) { return 'Ingen indirekt påverkan — isolerad ändring.'; } const parts = []; if (blastRadius.services > 0) parts.push(`${blastRadius.services} tjänster`); if (blastRadius.databases > 0) parts.push(`${blastRadius.databases} databaser`); if (blastRadius.external > 0) parts.push(`${blastRadius.external} externa integrationer`); return `Påverkar: ${parts.join(', ')}.`; } explainConfidence(analysis, confidence) { const uncertain = analysis.uncertaintySummary; if (!uncertain || uncertain.total === 0) { return `Confidence: ${Math.round(confidence * 100)}% — inga osäkerheter rapporterade.`; } return `Confidence: ${Math.round(confidence * 100)}%. ` + `${uncertain.certain}/${uncertain.total} slutsatser är säkra. ` + `${uncertain.unknown} är okända, ${uncertain.uncertain} är osäkra.`; } // ── Output ────────────────────────────────────────────────────────────── formatDecision(decision) { const lines = []; lines.push('╔═══════════════════════════════════════════════════════════════╗'); lines.push('║ SIL DECISION ENGINE ║'); lines.push('║ Beslutsunderlag för PR ║'); lines.push('╚═══════════════════════════════════════════════════════════════╝'); lines.push(''); // Sammanfattning lines.push('📊 SAMMANFATTNING'); lines.push(''); lines.push(` Risk Score: ${decision.summary.riskScore} (${decision.summary.riskLevel})`); lines.push(` Blast Radius: ${decision.summary.blastRadius.total} komponenter`); lines.push(` Confidence: ${decision.summary.confidence}`); lines.push(''); lines.push(` ✅ REKOMMENDATION: ${decision.summary.recommendation}`); lines.push(''); // Risk lines.push('⚠️ RISKANALYS'); lines.push(''); lines.push(` ${decision.details.risk.explanation}`); lines.push(''); // Blast Radius lines.push('💥 BLAST RADIUS'); lines.push(''); lines.push(` ${decision.details.blastRadius}`); lines.push(''); // Confidence lines.push('🎯 CONFIDENCE'); lines.push(''); lines.push(` ${decision.details.confidence}`); lines.push(''); // Tester if (decision.details.tests.length > 0) { lines.push('🧪 KRÄVDA TESTER'); lines.push(''); for (const test of decision.details.tests) { const icon = test.priority === 'high' ? '🔴' : '🟡'; lines.push(` ${icon} ${test.type.toUpperCase()}: ${test.target}`); lines.push(` ${test.reason}`); } lines.push(''); } // Deployment lines.push('🚀 DEPLOYMENT'); lines.push(''); lines.push(` Strategi: ${decision.details.deployment.strategy}`); lines.push(` ${decision.details.deployment.reason}`); lines.push(''); // Rollback lines.push('↩️ ROLLBACK'); lines.push(''); lines.push(` Tillgänglig: ${decision.details.rollback.available ? 'Ja' : 'Nej'}`); lines.push(` ${decision.details.rollback.reason}`); lines.push(` Uppskattad tid: ${decision.details.rollback.estimatedTime}`); lines.push(''); // Åtgärder if (decision.actions.length > 0) { lines.push('⚡ ÅTGÄRDER'); lines.push(''); for (const action of decision.actions) { const icon = action.type === 'BLOCK' ? '🔴' : action.type === 'REQUIRE_REVIEW' ? '🟡' : '🔵'; lines.push(` ${icon} ${action.message}`); } lines.push(''); } lines.push('═══════════════════════════════════════════════════════════════'); lines.push(''); return lines.join('\n'); } } // ── Integration med PRAnalyzer ──────────────────────────────────────────── export function withDecisionEngine(AnalyzerClass) { return class extends AnalyzerClass { constructor(...args) { super(...args); this.decisionEngine = new DecisionEngine(); } analyzePR(repoPath, baseBranch = 'main', headBranch = 'HEAD') { const report = super.analyzePR(repoPath, baseBranch, headBranch); // Generera beslutsunderlag const decision = this.decisionEngine.generateDecision(report); report.decision = decision; // Skriv ut beslutsunderlag console.log(this.decisionEngine.formatDecision(decision)); return report; } }; } // ── CLI ─────────────────────────────────────────────────────────────────── if (import.meta.url === `file://${process.argv[1]}`) { console.log('🎯 SIL Decision Engine'); console.log(''); console.log('Integreras med pr-analyzer.mjs:'); console.log(' import { withDecisionEngine } from "./decision-engine.mjs"'); console.log(' const EnhancedAnalyzer = withDecisionEngine(PRAnalyzer);'); console.log(''); console.log('Eller använd standalone:'); console.log(' import { DecisionEngine } from "./decision-engine.mjs"'); console.log(' const engine = new DecisionEngine();'); console.log(' const decision = engine.generateDecision(analysis);'); }