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!
289 lines
8.7 KiB
JavaScript
289 lines
8.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// SIL Graph Version — Versionera kunskapsgrafen
|
|
// Erik-krav: "Ni versionerar kod, men jag skulle också versionera kunskapsgrafen"
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { createHash } from 'crypto';
|
|
import { execSync } from 'child_process';
|
|
|
|
const GRAPH_PATH = '/home/bernt/.openclaw/workspace/SYSTEM_GRAPH.json';
|
|
const VERSION_DIR = '/home/bernt/.openclaw/workspace/SIL/graph-versions';
|
|
const VERSION_LOG = '/home/bernt/.openclaw/workspace/SIL/graph-version-log.jsonl';
|
|
|
|
class GraphVersion {
|
|
constructor() {
|
|
if (!existsSync(VERSION_DIR)) {
|
|
mkdirSync(VERSION_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skapa en ny graf-version
|
|
*/
|
|
createVersion(options = {}) {
|
|
const graph = this.loadGraph();
|
|
const timestamp = new Date().toISOString();
|
|
const commitSha = this.getGitCommit();
|
|
const parentVersion = this.getLatestVersion();
|
|
|
|
// Beräkna hash av grafen
|
|
const graphHash = this.hashGraph(graph);
|
|
|
|
// Versions-ID: hash av (parent + timestamp + graph-hash)
|
|
const versionId = createHash('sha256')
|
|
.update(`${parentVersion?.id || 'genesis'}${timestamp}${graphHash}`)
|
|
.digest('hex')
|
|
.substring(0, 16);
|
|
|
|
const version = {
|
|
id: versionId,
|
|
parent: parentVersion?.id || null,
|
|
timestamp,
|
|
commitSha,
|
|
graphHash,
|
|
datasetVersion: options.datasetVersion || this.inferDatasetVersion(graph),
|
|
observerVersion: options.observerVersion || '0.1.0',
|
|
reasonerVersion: options.reasonerVersion || '0.1.0',
|
|
metadata: {
|
|
nodeCount: graph.nodes?.length || 0,
|
|
edgeCount: graph.edges?.length || 0,
|
|
changedBy: options.changedBy || 'unknown',
|
|
changeReason: options.changeReason || 'manual_update',
|
|
previousNodeCount: parentVersion?.metadata?.nodeCount || 0,
|
|
previousEdgeCount: parentVersion?.metadata?.edgeCount || 0
|
|
}
|
|
};
|
|
|
|
// Spara graf med versionsinfo
|
|
const versionedGraph = {
|
|
...graph,
|
|
_version: version
|
|
};
|
|
|
|
writeFileSync(
|
|
`${VERSION_DIR}/graph-${versionId}.json`,
|
|
JSON.stringify(versionedGraph, null, 2)
|
|
);
|
|
|
|
// Logga version
|
|
const logEntry = {
|
|
...version,
|
|
action: 'created'
|
|
};
|
|
writeFileSync(
|
|
VERSION_LOG,
|
|
JSON.stringify(logEntry) + '\n',
|
|
{ flag: 'a' }
|
|
);
|
|
|
|
console.log(`✅ Graf-version skapad: ${versionId}`);
|
|
console.log(` Parent: ${version.parent || 'genesis'}`);
|
|
console.log(` Commit: ${commitSha}`);
|
|
console.log(` Noder: ${version.metadata.nodeCount}`);
|
|
console.log(` Kanter: ${version.metadata.edgeCount}`);
|
|
|
|
return version;
|
|
}
|
|
|
|
/**
|
|
* Hämta senaste versionen
|
|
*/
|
|
getLatestVersion() {
|
|
if (!existsSync(VERSION_LOG)) return null;
|
|
|
|
const lines = readFileSync(VERSION_LOG, 'utf8')
|
|
.split('\n')
|
|
.filter(line => line.trim());
|
|
|
|
if (lines.length === 0) return null;
|
|
|
|
try {
|
|
return JSON.parse(lines[lines.length - 1]);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hämta specifik version
|
|
*/
|
|
getVersion(versionId) {
|
|
const path = `${VERSION_DIR}/graph-${versionId}.json`;
|
|
if (!existsSync(path)) return null;
|
|
|
|
try {
|
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lista alla versioner
|
|
*/
|
|
listVersions() {
|
|
if (!existsSync(VERSION_LOG)) return [];
|
|
|
|
return readFileSync(VERSION_LOG, 'utf8')
|
|
.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => {
|
|
try { return JSON.parse(line); } catch { return null; }
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Jämför två versioner
|
|
*/
|
|
compareVersions(versionId1, versionId2) {
|
|
const v1 = this.getVersion(versionId1);
|
|
const v2 = this.getVersion(versionId2);
|
|
|
|
if (!v1 || !v2) {
|
|
return { error: 'En eller båda versionerna hittades inte' };
|
|
}
|
|
|
|
const nodes1 = new Set(v1.nodes?.map(n => n.id) || []);
|
|
const nodes2 = new Set(v2.nodes?.map(n => n.id) || []);
|
|
|
|
const added = [...nodes2].filter(n => !nodes1.has(n));
|
|
const removed = [...nodes1].filter(n => !nodes2.has(n));
|
|
|
|
const edges1 = new Set(v1.edges?.map(e => `${e.from}-${e.to}`) || []);
|
|
const edges2 = new Set(v2.edges?.map(e => `${e.from}-${e.to}`) || []);
|
|
|
|
const edgesAdded = [...edges2].filter(e => !edges1.has(e));
|
|
const edgesRemoved = [...edges1].filter(e => !edges2.has(e));
|
|
|
|
return {
|
|
from: versionId1,
|
|
to: versionId2,
|
|
nodes: {
|
|
before: nodes1.size,
|
|
after: nodes2.size,
|
|
added: added.length,
|
|
removed: removed.length
|
|
},
|
|
edges: {
|
|
before: edges1.size,
|
|
after: edges2.size,
|
|
added: edgesAdded.length,
|
|
removed: edgesRemoved.length
|
|
},
|
|
churn: {
|
|
nodeChurn: added.length + removed.length,
|
|
edgeChurn: edgesAdded.length + edgesRemoved.length,
|
|
totalChurn: added.length + removed.length + edgesAdded.length + edgesRemoved.length
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Återskapa exakt den världsmodell som låg till grund för ett beslut
|
|
*/
|
|
reconstructForDecision(decisionTimestamp) {
|
|
const versions = this.listVersions();
|
|
|
|
// Hitta senaste versionen före beslutet
|
|
const relevantVersion = versions
|
|
.filter(v => new Date(v.timestamp) <= new Date(decisionTimestamp))
|
|
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0];
|
|
|
|
if (!relevantVersion) {
|
|
return { error: 'Ingen version hittad före beslutet' };
|
|
}
|
|
|
|
return {
|
|
decisionTimestamp,
|
|
version: relevantVersion,
|
|
graph: this.getVersion(relevantVersion.id)
|
|
};
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
loadGraph() {
|
|
try {
|
|
return JSON.parse(readFileSync(GRAPH_PATH, 'utf8'));
|
|
} catch {
|
|
return { nodes: [], edges: [] };
|
|
}
|
|
}
|
|
|
|
hashGraph(graph) {
|
|
return createHash('sha256')
|
|
.update(JSON.stringify(graph))
|
|
.digest('hex')
|
|
.substring(0, 16);
|
|
}
|
|
|
|
getGitCommit() {
|
|
try {
|
|
return execSync('git rev-parse --short HEAD', {
|
|
cwd: '/home/bernt/.openclaw/workspace',
|
|
encoding: 'utf8'
|
|
}).trim();
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
inferDatasetVersion(graph) {
|
|
// Använd senaste observer-timestamp som dataset-version
|
|
const timestamps = graph.nodes
|
|
?.map(n => n.lastObserved)
|
|
.filter(Boolean)
|
|
.sort();
|
|
|
|
return timestamps?.[timestamps.length - 1] || 'unknown';
|
|
}
|
|
}
|
|
|
|
// ── CLI ───────────────────────────────────────────────────────────────────
|
|
|
|
const gv = new GraphVersion();
|
|
const command = process.argv[2];
|
|
|
|
if (command === '--create') {
|
|
gv.createVersion({
|
|
changedBy: process.argv[3] || 'manual',
|
|
changeReason: process.argv[4] || 'explicit_versioning'
|
|
});
|
|
} else if (command === '--list') {
|
|
const versions = gv.listVersions();
|
|
console.log(`📊 ${versions.length} graf-versioner\n`);
|
|
for (const v of versions) {
|
|
console.log(` ${v.id} (${v.timestamp})`);
|
|
console.log(` Parent: ${v.parent || 'genesis'}`);
|
|
console.log(` Commit: ${v.commitSha}`);
|
|
console.log(` Noder: ${v.metadata.nodeCount}, Kanter: ${v.metadata.edgeCount}`);
|
|
console.log();
|
|
}
|
|
} else if (command === '--compare') {
|
|
const v1 = process.argv[3];
|
|
const v2 = process.argv[4];
|
|
if (!v1 || !v2) {
|
|
console.log('Användning: node graph-version.mjs --compare <v1> <v2>');
|
|
process.exit(1);
|
|
}
|
|
const result = gv.compareVersions(v1, v2);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} else if (command === '--reconstruct') {
|
|
const timestamp = process.argv[3];
|
|
if (!timestamp) {
|
|
console.log('Användning: node graph-version.mjs --reconstruct <iso-timestamp>');
|
|
process.exit(1);
|
|
}
|
|
const result = gv.reconstructForDecision(timestamp);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} else {
|
|
console.log('Användning:');
|
|
console.log(' node graph-version.mjs --create [changedBy] [reason]');
|
|
console.log(' node graph-version.mjs --list');
|
|
console.log(' node graph-version.mjs --compare <v1> <v2>');
|
|
console.log(' node graph-version.mjs --reconstruct <timestamp>');
|
|
}
|