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!
378 lines
12 KiB
JavaScript
Executable File
378 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// System Graph Reasoning Engine
|
|
// Traverserar kunskapsgrafen för att besvara frågor om systemet
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const graphPath = join(__dirname, 'SYSTEM_GRAPH.json');
|
|
|
|
let graph;
|
|
try {
|
|
graph = JSON.parse(readFileSync(graphPath, 'utf8'));
|
|
} catch (e) {
|
|
console.error('❌ Kunde inte ladda SYSTEM_GRAPH.json:', e.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// ── Hjälpfunktioner ───────────────────────────────────────────────────────
|
|
|
|
function getNode(id) {
|
|
return graph.nodes.find(n => n.id === id);
|
|
}
|
|
|
|
function getEdgesFrom(nodeId) {
|
|
return graph.edges.filter(e => e.from === nodeId);
|
|
}
|
|
|
|
function getEdgesTo(nodeId) {
|
|
return graph.edges.filter(e => e.to === nodeId);
|
|
}
|
|
|
|
function getRelatedNodes(nodeId, direction = 'both') {
|
|
const related = new Set();
|
|
if (direction === 'from' || direction === 'both') {
|
|
getEdgesFrom(nodeId).forEach(e => related.add(e.to));
|
|
}
|
|
if (direction === 'to' || direction === 'both') {
|
|
getEdgesTo(nodeId).forEach(e => related.add(e.from));
|
|
}
|
|
return Array.from(related).map(id => getNode(id)).filter(Boolean);
|
|
}
|
|
|
|
// ── Traversering ──────────────────────────────────────────────────────────
|
|
|
|
function traverse(nodeId, visited = new Set(), depth = 0, maxDepth = 10) {
|
|
if (depth > maxDepth || visited.has(nodeId)) return [];
|
|
visited.add(nodeId);
|
|
|
|
const node = getNode(nodeId);
|
|
if (!node) return [];
|
|
|
|
const edges = getEdgesFrom(nodeId);
|
|
const results = [{ node, depth, edges }];
|
|
|
|
for (const edge of edges) {
|
|
results.push(...traverse(edge.to, visited, depth + 1, maxDepth));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function reverseTraverse(nodeId, visited = new Set(), depth = 0, maxDepth = 10) {
|
|
if (depth > maxDepth || visited.has(nodeId)) return [];
|
|
visited.add(nodeId);
|
|
|
|
const node = getNode(nodeId);
|
|
if (!node) return [];
|
|
|
|
const edges = getEdgesTo(nodeId);
|
|
const results = [{ node, depth, edges }];
|
|
|
|
for (const edge of edges) {
|
|
results.push(...reverseTraverse(edge.from, visited, depth + 1, maxDepth));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ── Query-handlers ────────────────────────────────────────────────────────
|
|
|
|
function impactAnalysis(componentId) {
|
|
console.log(`\n🔍 IMPACT ANALYSIS: ${componentId}\n`);
|
|
|
|
const node = getNode(componentId);
|
|
if (!node) {
|
|
console.log(`❌ Komponent "${componentId}" hittades inte i grafen`);
|
|
return;
|
|
}
|
|
|
|
console.log(`📍 Komponent: ${node.label} (${node.type})`);
|
|
if (node.critical) console.log(`⚠️ KRITISK KOMPONENT`);
|
|
console.log('');
|
|
|
|
const impacted = traverse(componentId);
|
|
|
|
const byDepth = {};
|
|
impacted.forEach(({ node, depth }) => {
|
|
if (depth === 0) return; // Skip self
|
|
if (!byDepth[depth]) byDepth[depth] = [];
|
|
byDepth[depth].push(node);
|
|
});
|
|
|
|
console.log('📊 Påverkade komponenter:\n');
|
|
|
|
for (const [depth, nodes] of Object.entries(byDepth)) {
|
|
console.log(` Nivå ${depth} (direkt${depth > 1 ? ' + indirekt' : ''}):`);
|
|
nodes.forEach(n => {
|
|
const icon = n.critical ? '🔴' : n.type === 'actor' ? '👤' : n.type === 'service' ? '⚙️' : n.type === 'database' ? '🗄️' : '📦';
|
|
console.log(` ${icon} ${n.label} (${n.type})${n.critical ? ' [KRITISK]' : ''}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
// Affärsprocesser som påverkas
|
|
const businessProcesses = impacted.filter(({ node }) => node.type === 'business_process');
|
|
if (businessProcesses.length > 0) {
|
|
console.log('🏭 Affärsprocesser som påverkas:');
|
|
businessProcesses.forEach(({ node }) => {
|
|
console.log(` • ${node.label}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
// Tester som behövs
|
|
const services = impacted.filter(({ node }) => node.type === 'service');
|
|
if (services.length > 0) {
|
|
console.log('🧪 Tester som måste köras:');
|
|
services.forEach(({ node }) => {
|
|
if (node.endpoints) {
|
|
console.log(` • ${node.label}:`);
|
|
node.endpoints.forEach(ep => console.log(` - ${ep}`));
|
|
}
|
|
});
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
function failureAnalysis(componentId) {
|
|
console.log(`\n💥 FAILURE ANALYSIS: ${componentId}\n`);
|
|
|
|
const node = getNode(componentId);
|
|
if (!node) {
|
|
console.log(`❌ Komponent "${componentId}" hittades inte`);
|
|
return;
|
|
}
|
|
|
|
console.log(`📍 Komponent som fallerar: ${node.label}\n`);
|
|
|
|
// Hitta alla noder som ENDAST kan nås via denna komponent
|
|
const allNodes = graph.nodes.filter(n => n.id !== componentId);
|
|
const cutOff = [];
|
|
|
|
for (const target of allNodes) {
|
|
const paths = findAllPaths(target.id, componentId);
|
|
if (paths.length === 0) continue;
|
|
|
|
// Kolla om det finns vägar som INTE går via componentId
|
|
const alternativePaths = paths.filter(p => !p.includes(componentId));
|
|
if (alternativePaths.length === 0) {
|
|
cutOff.push(target);
|
|
}
|
|
}
|
|
|
|
if (cutOff.length === 0) {
|
|
console.log('✅ Ingen komponent är helt beroende av denna. Systemet har redundans.');
|
|
return;
|
|
}
|
|
|
|
console.log('❌ Komponenter som BLIR OTILLGÄNGLIGA:\n');
|
|
|
|
const byType = {};
|
|
cutOff.forEach(n => {
|
|
if (!byType[n.type]) byType[n.type] = [];
|
|
byType[n.type].push(n);
|
|
});
|
|
|
|
for (const [type, nodes] of Object.entries(byType)) {
|
|
const icon = type === 'actor' ? '👤' : type === 'service' ? '⚙️' : type === 'business_process' ? '🏭' : type === 'database' ? '🗄️' : '📦';
|
|
console.log(` ${icon} ${type.toUpperCase()}:`);
|
|
nodes.forEach(n => {
|
|
console.log(` • ${n.label}${n.critical ? ' [KRITISK]' : ''}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
// Affärspåverkan
|
|
const businessImpact = cutOff.filter(n => n.type === 'business_process');
|
|
if (businessImpact.length > 0) {
|
|
console.log('💰 AFFÄRSPÅVERKAN:\n');
|
|
businessImpact.forEach(n => {
|
|
console.log(` 🏭 ${n.label}`);
|
|
console.log(` Steg: ${n.steps?.join(' → ') || 'okänt'}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
function findAllPaths(targetId, excludeId = null, currentPath = [], visited = new Set()) {
|
|
if (currentPath.length > 10) return [];
|
|
|
|
const paths = [];
|
|
const edges = getEdgesTo(targetId);
|
|
|
|
for (const edge of edges) {
|
|
if (edge.from === excludeId) continue;
|
|
if (visited.has(edge.from)) continue;
|
|
|
|
const newPath = [edge.from, ...currentPath];
|
|
if (newPath.length > 1) {
|
|
paths.push(newPath);
|
|
}
|
|
|
|
visited.add(edge.from);
|
|
paths.push(...findAllPaths(edge.from, excludeId, newPath, visited));
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
function blastRadius(tableName) {
|
|
console.log(`\n💣 BLAST RADIUS: ${tableName}\n`);
|
|
|
|
const tableNode = graph.nodes.find(n => n.label === tableName && n.type === 'domain');
|
|
if (!tableNode) {
|
|
console.log(`❌ Tabell "${tableName}" hittades inte`);
|
|
return;
|
|
}
|
|
|
|
console.log(`📍 Tabell: ${tableNode.label}\n`);
|
|
|
|
const impacted = reverseTraverse(tableNode.id);
|
|
|
|
console.log('🔍 Komponenter som använder denna tabell:\n');
|
|
|
|
const services = [];
|
|
const processes = [];
|
|
|
|
impacted.forEach(({ node, depth }) => {
|
|
if (depth === 0) return;
|
|
if (node.type === 'service') services.push({ node, depth });
|
|
if (node.type === 'business_process') processes.push({ node, depth });
|
|
});
|
|
|
|
if (services.length > 0) {
|
|
console.log(' ⚙️ TJÄNSTER:');
|
|
services.forEach(({ node, depth }) => {
|
|
console.log(` ${' '.repeat(depth - 1)}• ${node.label} ${node.endpoints ? `(${node.endpoints.length} endpoints)` : ''}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
if (processes.length > 0) {
|
|
console.log(' 🏭 AFFÄRSPROCESSER:');
|
|
processes.forEach(({ node }) => {
|
|
console.log(` • ${node.label}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
// Förslag på tester
|
|
console.log('🧪 FÖRESLAGNA TESTER:\n');
|
|
services.forEach(({ node }) => {
|
|
if (node.endpoints) {
|
|
console.log(` ${node.label}:`);
|
|
node.endpoints.forEach(ep => {
|
|
console.log(` - GET/POST ${ep} (med ${tableName} data)`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Huvudmeny ─────────────────────────────────────────────────────────────
|
|
|
|
function printHelp() {
|
|
console.log(`
|
|
🧠 System Graph Reasoning Engine
|
|
|
|
Användning:
|
|
node graph-engine.mjs <kommando> [argument]
|
|
|
|
Kommandon:
|
|
impact <komponent> Visa vilka komponenter påverkas av en ändring
|
|
failure <komponent> Visa vad som går sönder om komponenten fallerar
|
|
blast <tabell> Visa vilka som använder en databastabell
|
|
list Lista alla komponenter i grafen
|
|
stats Visa statistik om grafen
|
|
|
|
Exempel:
|
|
node graph-engine.mjs impact wallet
|
|
node graph-engine.mjs failure stripe
|
|
node graph-engine.mjs blast missions
|
|
`);
|
|
}
|
|
|
|
function listComponents() {
|
|
console.log('\n📋 KOMPONENTER I SYSTEMET\n');
|
|
|
|
const byType = {};
|
|
graph.nodes.forEach(n => {
|
|
if (!byType[n.type]) byType[n.type] = [];
|
|
byType[n.type].push(n);
|
|
});
|
|
|
|
for (const [type, nodes] of Object.entries(byType)) {
|
|
const icon = type === 'actor' ? '👤' : type === 'service' ? '⚙️' : type === 'database' ? '🗄️' : type === 'frontend' ? '🖥️' : type === 'business_process' ? '🏭' : type === 'domain' ? '📊' : type === 'risk' ? '⚠️' : type === 'agent' ? '🤖' : '📦';
|
|
console.log(`${icon} ${type.toUpperCase()} (${nodes.length}):`);
|
|
nodes.forEach(n => {
|
|
const critical = n.critical ? ' 🔴' : '';
|
|
console.log(` • ${n.label}${critical}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
function stats() {
|
|
console.log('\n📊 GRAF-STATISTIK\n');
|
|
console.log(` Noder: ${graph.nodes.length}`);
|
|
console.log(` Kanter: ${graph.edges.length}`);
|
|
console.log(` Affärsprocesser: ${graph.nodes.filter(n => n.type === 'business_process').length}`);
|
|
console.log(` Risker: ${graph.nodes.filter(n => n.type === 'risk').length}`);
|
|
console.log(` Kritiska komponenter: ${graph.nodes.filter(n => n.critical).length}`);
|
|
console.log(` Senast uppdaterad: ${graph.last_updated}`);
|
|
console.log(` Uppdateringskälla: ${graph.update_source}`);
|
|
console.log('');
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
|
|
const [,, command, ...args] = process.argv;
|
|
|
|
if (!command || command === 'help') {
|
|
printHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
switch (command) {
|
|
case 'impact':
|
|
if (!args[0]) {
|
|
console.log('❌ Ange komponent: node graph-engine.mjs impact <komponent>');
|
|
process.exit(1);
|
|
}
|
|
impactAnalysis(args[0]);
|
|
break;
|
|
|
|
case 'failure':
|
|
if (!args[0]) {
|
|
console.log('❌ Ange komponent: node graph-engine.mjs failure <komponent>');
|
|
process.exit(1);
|
|
}
|
|
failureAnalysis(args[0]);
|
|
break;
|
|
|
|
case 'blast':
|
|
if (!args[0]) {
|
|
console.log('❌ Ange tabell: node graph-engine.mjs blast <tabellnamn>');
|
|
process.exit(1);
|
|
}
|
|
blastRadius(args[0]);
|
|
break;
|
|
|
|
case 'list':
|
|
listComponents();
|
|
break;
|
|
|
|
case 'stats':
|
|
stats();
|
|
break;
|
|
|
|
default:
|
|
console.log(`❌ Okänt kommando: ${command}`);
|
|
printHelp();
|
|
process.exit(1);
|
|
}
|