#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // Decision Replay — Spela upp beslut exakt // ═══════════════════════════════════════════════════════════════════════════ import { createHash } from 'crypto'; /** * Decision Replay System * * Varje beslut sparas med: * - Input (hashad för integritet) * - Canonical Operation * - Policy Version * - Evidence * - Decision * - Runtime Trace * * Kan återspelas för att verifiera reproducerbarhet. */ class DecisionReplay { constructor() { this.decisions = new Map(); this.replays = []; } /** * Registrera ett beslut */ recordDecision(input, canonicalResult, policyResult, trace) { const inputHash = this.hashInput(input); const timestamp = new Date().toISOString(); const decision = { id: `decision-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp, inputHash, input: this.sanitizeInput(input), canonicalOperation: { operation: canonicalResult.operation.id, confidence: canonicalResult.confidence, evidence: canonicalResult.evidence }, policyVersion: this.getPolicyVersion(), policyResult: { passed: policyResult.passed, policyId: policyResult.policyId, rule: policyResult.rule, reason: policyResult.reason, severity: policyResult.severity }, trace: trace.toJSON(), replayable: true }; this.decisions.set(decision.id, decision); return decision.id; } /** * Spela upp ett beslut */ replayDecision(decisionId) { const decision = this.decisions.get(decisionId); if (!decision) { return { error: 'Decision not found', replayable: false }; } // Återskapa beslutet const replay = { original: decision, replayed: { timestamp: new Date().toISOString(), inputHash: decision.inputHash, canonicalOperation: decision.canonicalOperation, policyVersion: this.getPolicyVersion(), policyResult: decision.policyResult }, comparison: this.compareDecisions(decision, decision.policyResult) }; this.replays.push(replay); return replay; } /** * Jämför två beslut */ compareDecisions(original, replayed) { const differences = []; if (original.canonicalOperation.operation !== replayed.canonicalOperation?.operation) { differences.push({ field: 'canonicalOperation', original: original.canonicalOperation.operation, replayed: replayed.canonicalOperation?.operation }); } if (original.policyResult.passed !== replayed.passed) { differences.push({ field: 'decision', original: original.policyResult.passed ? 'ALLOW' : 'BLOCK', replayed: replayed.passed ? 'ALLOW' : 'BLOCK' }); } if (original.policyVersion !== this.getPolicyVersion()) { differences.push({ field: 'policyVersion', original: original.policyVersion, replayed: this.getPolicyVersion() }); } return { identical: differences.length === 0, differences }; } /** * Hasha input för integritetskontroll */ hashInput(input) { const str = JSON.stringify(input); return createHash('sha256').update(str).digest('hex').substr(0, 16); } /** * Sanera input (ta bort känslig data) */ sanitizeInput(input) { const sanitized = { ...input }; // Ta bort filinnehåll (kan innehålla hemligheter) if (sanitized.files) { sanitized.files = sanitized.files.map(f => ({ path: f.path, hasContent: !!f.content, contentLength: f.content?.length || 0 })); } return sanitized; } /** * Hämta aktuell policy-version */ getPolicyVersion() { return '1.0.0'; // Ska hämtas från Policy Registry } /** * Generera replay-rapport */ generateReplayReport() { const totalDecisions = this.decisions.size; const totalReplays = this.replays.length; const identicalReplays = this.replays.filter(r => r.comparison.identical).length; return { totalDecisions, totalReplays, identicalReplays, divergentReplays: totalReplays - identicalReplays, reproducibility: totalReplays > 0 ? Math.round((identicalReplays / totalReplays) * 100) : 100 }; } /** * Exportera alla beslut */ exportDecisions() { return Array.from(this.decisions.values()); } } export { DecisionReplay };