#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // SIL Uncertainty — Osäkerhet som förstklassig signal // Erik-krav: "Jag skulle också vilja att agenten ibland kan säga: 'Jag vet inte.'" // ═══════════════════════════════════════════════════════════════════════════ /** * Uncertainty-aware analysis — när SIL är osäker säger den det tydligt */ export class UncertaintyEngine { constructor() { this.thresholds = { // När ska vi säga "Jag vet inte"? lowConfidence: 0.5, // Under 50% — osäker mediumConfidence: 0.7, // 50-70% — kvalificerad gissning highConfidence: 0.9 // Över 90% — säker }; } /** * Utvärdera osäkerhet för en slutsats */ evaluateUncertainty(conclusion) { const confidence = conclusion.confidence || 0; const evidence = conclusion.evidence || []; const verified = conclusion.verified || false; // Beräkna olika osäkerhetsdimensioner const dimensions = { // 1. Confidence-osäkerhet confidenceUncertainty: this.calculateConfidenceUncertainty(confidence), // 2. Evidence-osäkerhet (hur mycket evidence har vi?) evidenceUncertainty: this.calculateEvidenceUncertainty(evidence), // 3. Verifierings-osäkerhet (är det verifierat eller infererat?) verificationUncertainty: verified ? 0 : 0.3, // 4. Kontext-osäkerhet (saknas kontext?) contextUncertainty: this.calculateContextUncertainty(conclusion) }; // Total osäkerhet (max av dimensionerna — vi är osäkra om NÅGON dimension är osäker) const totalUncertainty = Math.max(...Object.values(dimensions)); // Bestäm signal let signal; let explanation; if (totalUncertainty > 0.7) { signal = 'UNKNOWN'; explanation = this.generateUnknownExplanation(dimensions, conclusion); } else if (totalUncertainty > 0.4) { signal = 'UNCERTAIN'; explanation = this.generateUncertainExplanation(dimensions, conclusion); } else { signal = 'CERTAIN'; explanation = null; } return { conclusion: conclusion.text || conclusion.conclusion, confidence, signal, // UNKNOWN, UNCERTAIN, CERTAIN totalUncertainty: Math.round(totalUncertainty * 100) + '%', dimensions: { confidence: Math.round(dimensions.confidenceUncertainty * 100) + '%', evidence: Math.round(dimensions.evidenceUncertainty * 100) + '%', verification: Math.round(dimensions.verificationUncertainty * 100) + '%', context: Math.round(dimensions.contextUncertainty * 100) + '%' }, explanation, // Rekommendation baserat på osäkerhet recommendation: this.generateRecommendation(signal, conclusion) }; } calculateConfidenceUncertainty(confidence) { // Lägre confidence = högre osäkerhet if (confidence >= this.thresholds.highConfidence) return 0.1; if (confidence >= this.thresholds.mediumConfidence) return 0.3; if (confidence >= this.thresholds.lowConfidence) return 0.6; return 0.9; } calculateEvidenceUncertainty(evidence) { if (!evidence || evidence.length === 0) return 0.9; if (evidence.length === 1) return 0.5; if (evidence.length >= 3) return 0.1; return 0.3; } calculateContextUncertainty(conclusion) { // Om vi saknar kontext (t.ex. runtime-data) är vi osäkra const needsRuntime = conclusion.text?.match(/runtime|production|live|actual/i); const hasRuntime = conclusion.evidence?.some(e => e.source?.match(/runtime|trace|metric|log/i) ); if (needsRuntime && !hasRuntime) return 0.7; return 0.1; } generateUnknownExplanation(dimensions, conclusion) { const reasons = []; if (parseFloat(dimensions.confidenceUncertainty) > 0.5) { reasons.push(`Confidence är för låg (${Math.round((conclusion.confidence || 0) * 100)}%)`); } if (parseFloat(dimensions.evidenceUncertainty) > 0.5) { reasons.push('För lite evidence — behöver fler källor'); } if (parseFloat(dimensions.verificationUncertainty) > 0.2) { reasons.push('Slutsatsen är inte verifierad — endast inferens'); } if (parseFloat(dimensions.contextUncertainty) > 0.5) { reasons.push('Saknar runtime-kontext — kan inte verifiera i produktion'); } return { status: 'JAG VET INTE', reasons, whatWeKnow: this.extractWhatWeKnow(conclusion), whatWeNeed: this.extractWhatWeNeed(dimensions, conclusion) }; } generateUncertainExplanation(dimensions, conclusion) { const reasons = []; if (parseFloat(dimensions.confidenceUncertainty) > 0.3) { reasons.push(`Confidence är medel (${Math.round((conclusion.confidence || 0) * 100)}%)`); } if (parseFloat(dimensions.evidenceUncertainty) > 0.3) { reasons.push('Begränsat evidence'); } return { status: 'OSÄKER', reasons, caveat: 'Slutsatsen kan vara korrekt men bör verifieras manuellt' }; } extractWhatWeKnow(conclusion) { const known = []; if (conclusion.evidence) { for (const ev of conclusion.evidence) { if (ev.verified) { known.push(ev.description || ev.type); } } } return known.length > 0 ? known : ['Inget är säkert känt']; } extractWhatWeNeed(dimensions, conclusion) { const needed = []; if (parseFloat(dimensions.confidenceUncertainty) > 0.5) { needed.push('Mer data för att höja confidence'); } if (parseFloat(dimensions.verificationUncertainty) > 0.2) { needed.push('Runtime-verifiering (traces, metrics, logs)'); } if (parseFloat(dimensions.contextUncertainty) > 0.5) { needed.push('Produktionsdata för att validera'); } return needed; } generateRecommendation(signal, conclusion) { switch (signal) { case 'UNKNOWN': return { action: 'REQUIRE_MANUAL_REVIEW', message: 'SIL kan inte avgöra — kräver mänsklig granskning', autoApprove: false, blockMerge: true }; case 'UNCERTAIN': return { action: 'SUGGEST_VERIFICATION', message: 'SIL är osäker — föreslå verifiering innan merge', autoApprove: false, blockMerge: false }; case 'CERTAIN': return { action: 'AUTO_APPROVE', message: 'SIL är säker — kan godkännas automatiskt', autoApprove: true, blockMerge: false }; } } /** * Formatera osäkerhet för mänsklig läsning */ formatUncertainty(uncertaintyResult) { const lines = []; lines.push(`\n${'='.repeat(60)}`); lines.push('OSÄKERHETSANALYS'); lines.push(`${'='.repeat(60)}\n`); lines.push(`Slutsats: ${uncertaintyResult.conclusion}`); lines.push(`Confidence: ${Math.round((uncertaintyResult.confidence || 0) * 100)}%`); lines.push(`Signal: ${uncertaintyResult.signal}`); lines.push(`Total osäkerhet: ${uncertaintyResult.totalUncertainty}\n`); lines.push('Dimensioner:'); for (const [dim, value] of Object.entries(uncertaintyResult.dimensions)) { lines.push(` ${dim}: ${value}`); } if (uncertaintyResult.explanation) { lines.push(`\n${uncertaintyResult.explanation.status}`); if (uncertaintyResult.explanation.reasons) { lines.push('Anledningar:'); for (const reason of uncertaintyResult.explanation.reasons) { lines.push(` • ${reason}`); } } if (uncertaintyResult.explanation.whatWeKnow) { lines.push('\nVad vi vet:'); for (const known of uncertaintyResult.explanation.whatWeKnow) { lines.push(` ✓ ${known}`); } } if (uncertaintyResult.explanation.whatWeNeed) { lines.push('\nVad vi behöver:'); for (const need of uncertaintyResult.explanation.whatWeNeed) { lines.push(` ? ${need}`); } } } lines.push(`\nRekommendation: ${uncertaintyResult.recommendation.message}`); lines.push(`Åtgärd: ${uncertaintyResult.recommendation.action}`); lines.push(`Blockera merge: ${uncertaintyResult.recommendation.blockMerge ? 'Ja' : 'Nej'}`); lines.push(`${'='.repeat(60)}\n`); return lines.join('\n'); } } // ── Integration med PRAnalyzer ──────────────────────────────────────────── export function withUncertainty(AnalyzerClass) { return class extends AnalyzerClass { constructor(...args) { super(...args); this.uncertainty = new UncertaintyEngine(); } analyzePR(repoPath, baseBranch = 'main', headBranch = 'HEAD') { const report = super.analyzePR(repoPath, baseBranch, headBranch); // Utvärdera osäkerhet för varje slutsats if (report.provenance && report.provenance.trace) { const trace = JSON.parse(report.provenance.trace); for (const conclusion of trace.conclusions || []) { const uncertainty = this.uncertainty.evaluateUncertainty(conclusion); // Lägg till osäkerhet i rapporten if (!report.uncertainty) report.uncertainty = []; report.uncertainty.push(uncertainty); } } // Lägg till osäkerhetssammanfattning if (report.uncertainty) { const unknownCount = report.uncertainty.filter(u => u.signal === 'UNKNOWN').length; const uncertainCount = report.uncertainty.filter(u => u.signal === 'UNCERTAIN').length; const certainCount = report.uncertainty.filter(u => u.signal === 'CERTAIN').length; report.uncertaintySummary = { total: report.uncertainty.length, unknown: unknownCount, uncertain: uncertainCount, certain: certainCount, // Om NÅGON slutsats är UNKNOWN, blockera merge shouldBlockMerge: unknownCount > 0, // Om fler än 30% är osäkra, föreslå manuell review suggestManualReview: (unknownCount + uncertainCount) / report.uncertainty.length > 0.3 }; } return report; } }; } // ── CLI ─────────────────────────────────────────────────────────────────── if (import.meta.url === `file://${process.argv[1]}`) { const engine = new UncertaintyEngine(); // Demo: olika typer av osäkerhet const examples = [ { text: 'Wallet Service påverkar Stripe', confidence: 0.95, verified: true, evidence: [ { type: 'git_diff', verified: true, source: 'git' }, { type: 'ast_analysis', verified: true, source: 'AST' } ] }, { text: 'Auth Service påverkar KYC-flödet', confidence: 0.42, verified: false, evidence: [ { type: 'readme_reference', verified: false, source: 'README' } ] }, { text: 'Mission Service påverkar produktionsdatabasen', confidence: 0.65, verified: false, evidence: [ { type: 'code_reference', verified: false, source: 'AST' } ] } ]; console.log('🎯 OSÄKERHETS-DEMO\n'); for (const example of examples) { const result = engine.evaluateUncertainty(example); console.log(engine.formatUncertainty(result)); } }