Files
boc/SIL/provenance.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

286 lines
8.8 KiB
JavaScript

#!/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 <trace-file> [conclusion-index]');
}
}