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!
344 lines
12 KiB
JavaScript
344 lines
12 KiB
JavaScript
#!/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));
|
|
}
|
|
}
|