#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // SIL PR Analyzer — Production Version // Analyserar Pull Requests mot kunskapsgrafen // Kör i skuggläge: jämför med verklighet och loggar avvikelser // ═══════════════════════════════════════════════════════════════════════════ import { execSync } from 'child_process'; import { readFileSync, writeFileSync, appendFileSync, existsSync } from 'fs'; import { globSync } from 'glob'; import { ProvenanceTracker } from './provenance.mjs'; import { UncertaintyEngine, withUncertainty } from './uncertainty.mjs'; import { DecisionEngine, withDecisionEngine } from './decision-engine.mjs'; class PRAnalyzer { constructor(graphPath) { this.graph = JSON.parse(readFileSync(graphPath, 'utf8')); this.findings = []; this.validationLog = '/home/bernt/.openclaw/workspace/SIL/shadow-log.jsonl'; this.provenance = new ProvenanceTracker(this.graph); } analyzePR(repoPath, baseBranch = 'main', headBranch = 'HEAD') { const analysisId = `pr-${Date.now()}`; console.log(`🔍 Analyserar PR: ${headBranch} → ${baseBranch} [${analysisId}]\n`); // Starta provenance-spårning const trace = this.provenance.startTrace(analysisId, { repo: repoPath, baseBranch, headBranch, tool: 'pr-analyzer' }); // 1. Hämta diff this.provenance.logStep(trace, { type: 'observation', description: 'Hämta git diff för PR', inputs: [`${baseBranch}...${headBranch}`], output: 'diff_data', confidence: 0.99, verified: true, source: 'git' }); const diff = this.getDiff(repoPath, baseBranch, headBranch); const diffFiles = this.getDiffFiles(repoPath, baseBranch, headBranch); this.provenance.logStep(trace, { type: 'observation', description: 'Identifiera ändrade filer', inputs: diffFiles, output: `${diffFiles.length} filer ändrade`, confidence: 0.99, verified: true, source: 'git' }); // 2. Identifiera ändrade komponenter const changedComponents = this.identifyChangedComponents(diffFiles, repoPath); for (const comp of changedComponents) { this.provenance.logStep(trace, { type: 'rule', description: `Mappa fil till komponent: ${comp.file} → ${comp.name}`, inputs: [comp.file], output: comp.id, confidence: comp.confidence, verified: comp.source === 'AST' || comp.source === 'DB_MIGRATION', source: comp.source }); } // 3. Analysera påverkan på grafen const impact = this.analyzeImpact(changedComponents); for (const indirect of impact.indirect) { this.provenance.logStep(trace, { type: 'traversal', description: `Traversera graf: ${indirect.from} → ${indirect.to}`, inputs: [indirect.from], output: indirect.to, confidence: indirect.confidence, verified: false, source: 'graph' }); } // 4. Kontrollera mot affärsflöden const businessImpact = this.analyzeBusinessImpact(impact); for (const bi of businessImpact) { this.provenance.logStep(trace, { type: 'inference', description: `Härled affärspåverkan: ${bi.process}`, inputs: impact.indirect.map(i => i.to), output: bi.process, confidence: 0.75, verified: false, source: 'business_rule' }); } // 5. Identifiera tester const requiredTests = this.identifyRequiredTests(impact); // 6. Riskbedömning const risks = this.assessRisks(impact, changedComponents); // 7. Kontrollera breaking changes const breakingChanges = this.checkBreakingChanges(diff, impact); // 8. Evidence const evidence = this.gatherEvidence(diffFiles, repoPath); // 9. Jämför med faktisk verklighet (skuggläge) const shadowValidation = this.validateAgainstReality(changedComponents, impact); // Logga slutsatser for (const comp of changedComponents) { this.provenance.logConclusion(trace, { text: `${comp.name} är ändrad`, confidence: comp.confidence, basis: trace.steps.filter(s => s.output === comp.id).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 ${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 }); } // Generera rapport return this.generateReport({ analysisId, trace, diff, diffFiles, changedComponents, impact, businessImpact, requiredTests, risks, breakingChanges, evidence, shadowValidation }); } getDiff(repoPath, baseBranch, headBranch) { try { return execSync( `git diff ${baseBranch}...${headBranch} --stat`, { cwd: repoPath, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 } ); } catch (e) { return ''; } } getDiffFiles(repoPath, baseBranch, headBranch) { try { const output = execSync( `git diff --name-only ${baseBranch}...${headBranch}`, { cwd: repoPath, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 } ); return output.trim().split('\n').filter(f => f); } catch (e) { return []; } } identifyChangedComponents(diffFiles, repoPath) { const components = []; for (const file of diffFiles) { // Auth-komponenter if (file.match(/auth|login|logout|register|password/i)) { components.push({ id: 'auth', name: 'Auth Service', file, confidence: 0.95, source: 'AST', evidence: [`Fil: ${file}`] }); } // Wallet-komponenter if (file.match(/wallet|payout|payment|stripe/i)) { components.push({ id: 'wallet', name: 'Wallet Service', file, confidence: 0.95, source: 'AST', evidence: [`Fil: ${file}`] }); } // Mission-komponenter if (file.match(/mission|claim|submission/i)) { components.push({ id: 'mission', name: 'Mission Service', file, confidence: 0.95, source: 'AST', evidence: [`Fil: ${file}`] }); } // KYC-komponenter if (file.match(/kyc|identity|verify|document/i)) { components.push({ id: 'kyc', name: 'KYC Service', file, confidence: 0.90, source: 'AST', evidence: [`Fil: ${file}`] }); } // Databas if (file.match(/migration|schema|\.sql$/i)) { components.push({ id: 'db', name: 'Database', file, confidence: 0.97, source: 'DB_MIGRATION', evidence: [`Migration: ${file}`] }); } // Frontend if (file.match(/\.tsx$|\.ts$|\.jsx$|\.js$/) && file.includes('src/')) { components.push({ id: 'app', name: 'quiXzoom App', file, confidence: 0.90, source: 'AST', evidence: [`Frontend-fil: ${file}`] }); } } // Deduplicera const seen = new Set(); return components.filter(c => { if (seen.has(c.id)) return false; seen.add(c.id); return true; }); } analyzeImpact(changedComponents) { const impact = { direct: [], indirect: [], services: [], databases: [], external: [] }; for (const component of changedComponents) { impact.direct.push(component); const node = this.graph.nodes.find(n => n.id === component.id); if (!node) continue; // Traversera kanter const edges = this.graph.edges.filter(e => e.from === component.id); for (const edge of edges) { const targetNode = this.graph.nodes.find(n => n.id === edge.to); if (!targetNode) continue; impact.indirect.push({ from: component.name, to: targetNode.label, relation: edge.relation, confidence: edge.confidence || 0.80 }); if (targetNode.type === 'service') impact.services.push(targetNode); if (targetNode.type === 'database' || targetNode.type === 'domain') impact.databases.push(targetNode); if (targetNode.type === 'external') impact.external.push(targetNode); } } return impact; } analyzeBusinessImpact(impact) { const affectedProcesses = []; const processNodes = this.graph.nodes.filter(n => n.type === 'business_process'); for (const process of processNodes) { const processEdges = this.graph.edges.filter(e => e.from === process.id); for (const edge of processEdges) { const usesComponent = impact.direct.find(c => c.id === edge.to) || impact.indirect.find(i => i.to === edge.to); if (usesComponent) { affectedProcesses.push({ process: process.label, step: edge.relation, impact: usesComponent.name || usesComponent.to }); } } } return affectedProcesses; } identifyRequiredTests(impact) { const tests = []; for (const service of impact.services) { if (service.endpoints) { tests.push({ service: service.label, type: 'integration', endpoints: service.endpoints, priority: service.critical ? 'high' : 'medium' }); } tests.push({ service: service.label, type: 'unit', priority: 'high' }); } if (impact.databases.length > 0) { tests.push({ type: 'migration', tables: impact.databases.map(d => d.label), priority: 'high' }); } return tests; } assessRisks(impact, changedComponents) { const risks = []; for (const component of changedComponents) { const node = this.graph.nodes.find(n => n.id === component.id); if (node && node.critical) { risks.push({ level: 'high', category: 'critical_component', description: `Ändring i kritisk komponent: ${node.label}`, mitigation: 'Kräver extra granskning och tester' }); } } for (const ext of impact.external) { risks.push({ level: 'medium', category: 'external_dependency', description: `Påverkar extern tjänst: ${ext.label}`, mitigation: 'Verifiera med extern leverantör' }); } if (impact.databases.length > 0) { risks.push({ level: 'high', category: 'data_model', description: 'Databasändring kan påverka data integritet', mitigation: 'Kör migrationstester och backup' }); } return risks; } checkBreakingChanges(diff, impact) { const breaking = []; if (diff.includes('DELETE') || diff.includes('REMOVE')) { breaking.push({ type: 'api_removal', description: 'API-endpoint eller funktion kan ha tagits bort', action: 'Verifiera bakåtkompatibilitet' }); } if (diff.includes('DROP') || diff.includes('ALTER')) { breaking.push({ type: 'schema_change', description: 'Databasschema ändrat', action: 'Kontrollera att existerande data migreras korrekt' }); } return breaking; } gatherEvidence(diffFiles, repoPath) { const evidence = { static: [], dynamic: [], confidence: 0 }; for (const file of diffFiles) { if (file.match(/\.(ts|js|tsx|jsx|rs)$/)) { evidence.static.push({ type: 'source_code', file, confidence: 0.99 }); } if (file.match(/migration|schema|\.sql$/)) { evidence.static.push({ type: 'database_migration', file, confidence: 0.97 }); } if (file.match(/openapi|swagger|\.yaml$|\.yml$/)) { evidence.static.push({ type: 'api_contract', file, confidence: 0.96 }); } } // Beräkna genomsnittlig confidence if (evidence.static.length > 0) { evidence.confidence = evidence.static.reduce((a, e) => a + e.confidence, 0) / evidence.static.length; } return evidence; } validateAgainstReality(changedComponents, impact) { const validation = { predictions: [], canVerify: [], cannotVerify: [] }; // För varje förutsägelse, avgör om vi kan verifiera den for (const component of changedComponents) { // Kan vi verifiera att denna komponent faktiskt ändrades? validation.canVerify.push({ prediction: `${component.name} ändrades`, verificationMethod: 'git_diff', confidence: component.confidence }); } // Kan vi verifiera indirekt påverkan? for (const indirect of impact.indirect) { validation.cannotVerify.push({ prediction: `${indirect.to} påverkas av ändring i ${indirect.from}`, reason: 'Kräver runtime-analys eller manuell review', confidence: indirect.confidence }); } return validation; } generateReport(analysis) { const timestamp = new Date().toISOString(); const report = { timestamp, analysisId: analysis.analysisId, provenance: { traceId: analysis.trace.id, steps: analysis.trace.steps.length, conclusions: analysis.trace.conclusions.length, explanation: this.provenance.explainConclusion(analysis.analysisId, 0) }, summary: { changedComponents: analysis.changedComponents.length, directImpact: analysis.impact.direct.length, indirectImpact: analysis.impact.indirect.length, businessProcessesAffected: analysis.businessImpact.length, risksIdentified: analysis.risks.length, breakingChanges: analysis.breakingChanges.length, evidenceConfidence: analysis.evidence.confidence }, changedComponents: analysis.changedComponents, impact: analysis.impact, businessImpact: analysis.businessImpact, requiredTests: analysis.requiredTests, risks: analysis.risks, breakingChanges: analysis.breakingChanges, evidence: analysis.evidence, shadowValidation: analysis.shadowValidation }; // Spara till validation log this.logValidation(report, repoPath, baseBranch, headBranch); // Skriv ut rapport this.printReport(report); return report; } logValidation(report, repoPath, baseBranch, headBranch) { // Get commit info let commit = 'unknown'; let changedFiles = 0; try { commit = execSync('git rev-parse --short HEAD', { cwd: repoPath, encoding: 'utf8' }).trim(); changedFiles = parseInt( execSync(`git diff --name-only ${baseBranch}...${headBranch} | wc -l`, { cwd: repoPath, encoding: 'utf8' }).trim(), 10 ) || 0; } catch (e) {} const entry = { timestamp: report.timestamp, repo: repoPath.split('/').pop(), from: baseBranch, to: headBranch, commit, changedFiles, components: report.summary.changedComponents, risks: report.summary.risksIdentified, breakingChanges: report.summary.breakingChanges, confidence: report.summary.evidenceConfidence, predictions: report.shadowValidation.canVerify.map(v => ({ prediction: v.prediction, confidence: v.confidence, verified: null })), unverifiable: report.shadowValidation.cannotVerify.map(v => ({ prediction: v.prediction, reason: v.reason, confidence: v.confidence })) }; appendFileSync( this.validationLog, JSON.stringify(entry) + '\n', 'utf8' ); } printReport(report) { console.log('═══════════════════════════════════════════════════════════════'); console.log(' SIL PR ANALYSIS REPORT'); console.log(' (Skuggläge — jämför med verklighet)'); console.log('═══════════════════════════════════════════════════════════════\n'); // Sammanfattning console.log('📊 SAMMANFATTNING\n'); console.log(` Ändrade komponenter: ${report.summary.changedComponents}`); console.log(` Direkt påverkan: ${report.summary.directImpact}`); console.log(` Indirekt påverkan: ${report.summary.indirectImpact}`); console.log(` Affärsprocesser påverkade: ${report.summary.businessProcessesAffected}`); console.log(` Risker identifierade: ${report.summary.risksIdentified}`); console.log(` Breaking changes: ${report.summary.breakingChanges}`); console.log(` Evidence confidence: ${report.summary.evidenceConfidence.toFixed(2)}\n`); // Ändrade komponenter console.log('🔧 ÄNDRADE KOMPONENTER\n'); for (const comp of report.changedComponents) { console.log(` ✅ ${comp.name}`); console.log(` Fil: ${comp.file}`); console.log(` Confidence: ${comp.confidence} (${comp.source})`); console.log(` Evidence: ${comp.evidence.join(', ')}`); console.log(); } // Evidence console.log('📋 EVIDENCE\n'); console.log(` Statiska bevis: ${report.evidence.static.length}`); console.log(` Dynamiska bevis: ${report.evidence.dynamic.length}`); console.log(` Genomsnittlig confidence: ${report.evidence.confidence.toFixed(2)}`); for (const ev of report.evidence.static) { console.log(` • ${ev.type}: ${ev.file} (${ev.confidence})`); } console.log(); // Påverkan if (report.impact.indirect.length > 0) { console.log('🌐 INDIREKT PÅVERKAN\n'); for (const impact of report.impact.indirect) { console.log(` ${impact.from} → ${impact.to}`); console.log(` Relation: ${impact.relation}`); console.log(` Confidence: ${impact.confidence}`); console.log(); } } // Affärsprocesser if (report.businessImpact.length > 0) { console.log('🏭 AFFÄRSPROCESSER PÅVERKADE\n'); for (const bi of report.businessImpact) { console.log(` • ${bi.process}`); console.log(` Steg: ${bi.step}`); console.log(` Påverkan: ${bi.impact}`); console.log(); } } // Tester if (report.requiredTests.length > 0) { console.log('🧪 TESTER SOM KRÄVS\n'); for (const test of report.requiredTests) { const icon = test.priority === 'high' ? '🔴' : '🟡'; console.log(` ${icon} ${test.type.toUpperCase()}`); if (test.service) console.log(` Service: ${test.service}`); if (test.endpoints) console.log(` Endpoints: ${test.endpoints.join(', ')}`); if (test.tables) console.log(` Tabeller: ${test.tables.join(', ')}`); console.log(); } } // Risker if (report.risks.length > 0) { console.log('⚠️ RISKER\n'); for (const risk of report.risks) { const icon = risk.level === 'high' ? '🔴' : risk.level === 'medium' ? '🟡' : '🟢'; console.log(` ${icon} ${risk.category.toUpperCase()}`); console.log(` ${risk.description}`); console.log(` Åtgärd: ${risk.mitigation}`); console.log(); } } // Skuggläge console.log('👥 SKUGGLÄGE (Validering)\n'); console.log(` Kan verifiera: ${report.shadowValidation.canVerify.length} förutsägelser`); console.log(` Kan INTE verifiera: ${report.shadowValidation.cannotVerify.length} förutsägelser`); console.log(); if (report.shadowValidation.cannotVerify.length > 0) { console.log(' Förutsägelser som kräver manuell review:'); for (const v of report.shadowValidation.cannotVerify) { console.log(` • ${v.prediction}`); console.log(` Anledning: ${v.reason}`); console.log(` Confidence: ${v.confidence}`); console.log(); } } // Provenance if (report.provenance) { console.log('📋 PROVENANCE (Revisionsspår)\n'); console.log(` Analys-ID: ${report.analysisId}`); console.log(` Steg: ${report.provenance.steps}`); console.log(` Slutsatser: ${report.provenance.conclusions}`); console.log(); if (report.provenance.explanation) { console.log(' Exempel — hur en slutsats nåddes:'); const exp = report.provenance.explanation; console.log(` Slutsats: "${exp.conclusion}"`); console.log(` Confidence: ${exp.confidence}`); console.log(` Verifierad: ${exp.verified ? 'Ja' : 'Nej'}`); console.log(` Infererad: ${exp.inferred ? 'Ja' : 'Nej'}`); console.log(' Kedja:'); for (const step of exp.how) { const icon = step.verified ? '✅' : '⚠️'; console.log(` ${icon} [${step.type}] ${step.description}`); console.log(` confidence=${step.confidence} source=${step.source}`); } console.log(); } } console.log('💡 REKOMMENDATIONER\n'); console.log(' 1. Kör alla föreslagna tester innan merge'); console.log(' 2. Verifiera att inga breaking changes påverkar produktion'); console.log(' 3. Uppdatera dokumentation om API:er ändrats'); console.log(' 4. JÄMFÖR denna analys med verklighet efter deploy'); console.log(' 5. GRANSKA provenance om en slutsats verkar felaktig'); console.log(); console.log('═══════════════════════════════════════════════════════════════\n'); } } // ── Main ────────────────────────────────────────────────────────────────── // Bygg kedja av mixins: Provenance → Uncertainty → Decision Engine const UncertaintyAnalyzer = withUncertainty(PRAnalyzer); const FullAnalyzer = withDecisionEngine(UncertaintyAnalyzer); const analyzer = new FullAnalyzer('/home/bernt/.openclaw/workspace/SYSTEM_GRAPH.json'); const repoPath = process.argv[2] || '/home/bernt/repos/quixzoom.com'; const baseBranch = process.argv[3] || 'HEAD~1'; const headBranch = process.argv[4] || 'HEAD'; console.log('🚀 SIL PR Analyzer (Skuggläge — Experiment-fas)\n'); console.log('⚠️ SIL är INTE aktiverad för merge-beslut ännu\n'); console.log(' Kör "node SIL/metrics.mjs" för diagnostiska metriker\n'); analyzer.analyzePR(repoPath, baseBranch, headBranch);