05ed037fe8
- 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!
519 lines
16 KiB
JavaScript
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);');
|
|
}
|