#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // SIL Provenance — Revisionsspår för varje slutsats // Erik-krav: "För varje slutsats vill jag kunna svara på: // - Vilka observationer användes? // - Vilka regler aktiverades? // - Vilka grafnoder traverserades? // - Vilka relationer användes? // - Vilka confidence-värden påverkade slutsatsen? // - Vilken del var verifierad och vilken del var inferens?" // ═══════════════════════════════════════════════════════════════════════════ import { readFileSync } from 'fs'; /** * ProvenanceTracker — Spårar hur varje slutsats i SIL nåddes */ export class ProvenanceTracker { constructor(graph) { this.graph = graph; this.traces = []; } /** * Starta en ny spårning för en analys */ startTrace(analysisId, context) { const trace = { id: analysisId, timestamp: new Date().toISOString(), context, steps: [], conclusions: [] }; this.traces.push(trace); return trace; } /** * Logga ett steg i resonemangskedjan */ logStep(trace, step) { const entry = { order: trace.steps.length + 1, type: step.type, // 'observation', 'rule', 'traversal', 'inference', 'verification' description: step.description, inputs: step.inputs || [], output: step.output, confidence: step.confidence || 1.0, verified: step.verified || false, source: step.source || 'unknown', timestamp: new Date().toISOString() }; trace.steps.push(entry); return entry; } /** * Logga en slutsats */ logConclusion(trace, conclusion) { const entry = { order: trace.conclusions.length + 1, conclusion: conclusion.text, confidence: conclusion.confidence, basis: conclusion.basis || [], // vilka steg som ledde hit verified: conclusion.verified || false, verificationMethod: conclusion.verificationMethod || null, inferred: conclusion.inferred || false, timestamp: new Date().toISOString() }; trace.conclusions.push(entry); return entry; } /** * Hämta komplett spår för en analys */ getTrace(analysisId) { return this.traces.find(t => t.id === analysisId); } /** * Generera mänskligt läsbar förklaring av hur en slutsats nåddes */ explainConclusion(analysisId, conclusionIndex = 0) { const trace = this.getTrace(analysisId); if (!trace) return null; const conclusion = trace.conclusions[conclusionIndex]; if (!conclusion) return null; const explanation = { conclusion: conclusion.conclusion, confidence: conclusion.confidence, how: [], verified: conclusion.verified, inferred: conclusion.inferred }; // För varje basis-steg, förklara for (const stepId of conclusion.basis) { const step = trace.steps.find(s => s.order === stepId); if (!step) continue; explanation.how.push({ type: step.type, description: step.description, confidence: step.confidence, verified: step.verified, source: step.source }); } return explanation; } /** * Exportera spår i JSONL-format för persistent lagring */ exportTrace(trace) { return JSON.stringify(trace); } } /** * Integrera med PRAnalyzer — lägg till provenance-spårning */ export function withProvenance(AnalyzerClass) { return class extends AnalyzerClass { constructor(...args) { super(...args); this.provenance = new ProvenanceTracker(this.graph); } analyzePR(repoPath, baseBranch = 'main', headBranch = 'HEAD') { const analysisId = `pr-${Date.now()}`; const trace = this.provenance.startTrace(analysisId, { repo: repoPath, baseBranch, headBranch }); // Steg 1: Observation — hämta diff this.provenance.logStep(trace, { type: 'observation', description: 'Hämta git diff för PR', inputs: [`${baseBranch}...${headBranch}`], output: 'diff_files_list', confidence: 0.99, verified: true, source: 'git' }); const diffFiles = this.getDiffFiles(repoPath, baseBranch, headBranch); // Steg 2: Observation — identifiera ändrade filer this.provenance.logStep(trace, { type: 'observation', description: 'Identifiera ändrade filer från diff', inputs: diffFiles, output: `${diffFiles.length} filer identifierade`, confidence: 0.99, verified: true, source: 'git' }); // Steg 3: Regel — mappa filer till komponenter const changedComponents = this.identifyChangedComponents(diffFiles, repoPath); for (const comp of changedComponents) { this.provenance.logStep(trace, { type: 'rule', description: `Mappa fil ${comp.file} till komponent ${comp.name}`, inputs: [comp.file], output: comp.id, confidence: comp.confidence, verified: comp.source === 'AST' || comp.source === 'DB_MIGRATION', source: comp.source }); } // Steg 4: Traversal — traversera grafen const impact = this.analyzeImpact(changedComponents); for (const indirect of impact.indirect) { const edge = this.graph.edges.find(e => e.from === changedComponents.find(c => c.name === indirect.from)?.id && e.to === this.graph.nodes.find(n => n.label === indirect.to)?.id ); this.provenance.logStep(trace, { type: 'traversal', description: `Traversera kant: ${indirect.from} → ${indirect.to}`, inputs: [indirect.from], output: indirect.to, confidence: edge?.confidence || indirect.confidence, verified: false, // kräver runtime-verifiering source: 'graph_traversal' }); } // Steg 5: Inferens — affärspåverkan const businessImpact = this.analyzeBusinessImpact(impact); for (const bi of businessImpact) { this.provenance.logStep(trace, { type: 'inference', description: `Härled affärspåverkan: ${bi.process} påverkas`, inputs: impact.indirect.map(i => i.to), output: bi.process, confidence: 0.75, // inferens har lägre confidence verified: false, source: 'business_rule' }); } // Steg 6: Slutsatser const report = super.analyzePR(repoPath, baseBranch, headBranch); // Logga slutsatser med provenance for (const comp of changedComponents) { this.provenance.logConclusion(trace, { text: `${comp.name} är en ändrad komponent`, confidence: comp.confidence, basis: trace.steps .filter(s => s.output === comp.id || s.inputs.includes(comp.file)) .map(s => s.order), verified: true, verificationMethod: 'git_diff', inferred: false }); } for (const indirect of impact.indirect) { this.provenance.logConclusion(trace, { text: `${indirect.to} påverkas indirekt av ändring i ${indirect.from}`, confidence: indirect.confidence, basis: trace.steps .filter(s => s.type === 'traversal' && s.output === indirect.to) .map(s => s.order), verified: false, verificationMethod: null, inferred: true }); } // Lägg till provenance i rapporten report.provenance = { analysisId, trace: this.provenance.exportTrace(trace), explanation: this.provenance.explainConclusion(analysisId, 0) }; return report; } }; } // ── CLI för att inspektera provenance ───────────────────────────────────── if (import.meta.url === `file://${process.argv[1]}`) { const command = process.argv[2]; if (command === '--explain') { const traceFile = process.argv[3]; const conclusionIndex = parseInt(process.argv[4]) || 0; try { const trace = JSON.parse(readFileSync(traceFile, 'utf8')); const tracker = new ProvenanceTracker({}); tracker.traces.push(trace); const explanation = tracker.explainConclusion(trace.id, conclusionIndex); console.log(JSON.stringify(explanation, null, 2)); } catch (e) { console.error('Fel:', e.message); } } else { console.log('Användning:'); console.log(' node provenance.mjs --explain [conclusion-index]'); } }