Files
boc/SIL/decision-engine.mjs
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- DNS: pilot.landvex.com -> 16.170.83.169
- TLS: Let's Encrypt certificate (expires 2026-09-30)
- Nginx: reverse proxy with SSL termination
- API: https://pilot.landvex.com/api/v1/missions
- UI: https://pilot.landvex.com/
- Upload: POST /api/v1/missions/import (multipart/form-data)

Verified:
 https://pilot.landvex.com/health
 https://pilot.landvex.com/version
 https://pilot.landvex.com/api/v1/missions (list)
 https://pilot.landvex.com/api/v1/missions/:id (get)
 POST /api/v1/missions/import (video upload)
 UI loads with title 'LandveX Intelligence Lab'

Next: Pilot 001 — Break the system!
2026-07-02 17:34:19 +00:00

519 lines
16 KiB
JavaScript

#!/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);');
}