721 lines
23 KiB
JavaScript
721 lines
23 KiB
JavaScript
|
|
#!/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);
|