05ed037fe8
- 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!
286 lines
8.8 KiB
JavaScript
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]');
|
|
}
|
|
}
|