Files
boc/SIL/graph-version.mjs
T

289 lines
8.7 KiB
JavaScript
Raw Normal View History

#!/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>');
}