Files
boc/SIL/pr-analyzer.mjs
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- 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!
2026-07-02 17:34:19 +00:00

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);