opentology 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/context.js +14 -0
- package/dist/commands/drop.js +2 -0
- package/dist/commands/rollback.d.ts +2 -0
- package/dist/commands/rollback.js +75 -0
- package/dist/index.js +2 -0
- package/dist/lib/snapshot.d.ts +40 -0
- package/dist/lib/snapshot.js +136 -0
- package/dist/mcp/server.js +80 -2
- package/package.json +1 -1
package/dist/commands/context.js
CHANGED
|
@@ -217,6 +217,20 @@ export function registerContext(program) {
|
|
|
217
217
|
}
|
|
218
218
|
settings.hooks = hooks;
|
|
219
219
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
220
|
+
// Ensure .opentology/snapshots/ is in .gitignore
|
|
221
|
+
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
222
|
+
const snapshotIgnore = '.opentology/snapshots/';
|
|
223
|
+
if (existsSync(gitignorePath)) {
|
|
224
|
+
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
225
|
+
if (!gitignoreContent.includes(snapshotIgnore)) {
|
|
226
|
+
writeFileSync(gitignorePath, gitignoreContent.trimEnd() + '\n' + snapshotIgnore + '\n', 'utf-8');
|
|
227
|
+
console.log(pc.green(' Added .opentology/snapshots/ to .gitignore'));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
writeFileSync(gitignorePath, snapshotIgnore + '\n', 'utf-8');
|
|
232
|
+
console.log(pc.green(' Created .gitignore with .opentology/snapshots/'));
|
|
233
|
+
}
|
|
220
234
|
console.log('');
|
|
221
235
|
console.log(pc.dim('Consider adding .opentology/hooks/ to version control so team members share the hook.'));
|
|
222
236
|
}
|
package/dist/commands/drop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { loadConfig, resolveGraphUri, saveConfig } from '../lib/config.js';
|
|
3
3
|
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
4
|
+
import { snapshotGraph } from '../lib/snapshot.js';
|
|
4
5
|
export function registerDrop(program) {
|
|
5
6
|
program
|
|
6
7
|
.command('drop')
|
|
@@ -23,6 +24,7 @@ export function registerDrop(program) {
|
|
|
23
24
|
}
|
|
24
25
|
try {
|
|
25
26
|
const adapter = await createReadyAdapter(config);
|
|
27
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
26
28
|
await adapter.dropGraph(graphUri);
|
|
27
29
|
// In embedded mode, clear tracked files for this graph
|
|
28
30
|
if (config.mode === 'embedded') {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { loadConfig, resolveGraphUri } from '../lib/config.js';
|
|
3
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
4
|
+
import { listSnapshots, restoreSnapshot } from '../lib/snapshot.js';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
function ask(question) {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
rl.question(question, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer.trim().toLowerCase());
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function registerRollback(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('rollback')
|
|
18
|
+
.description('List or restore graph snapshots')
|
|
19
|
+
.option('--list', 'List available snapshots')
|
|
20
|
+
.option('--to <timestamp>', 'Restore to a specific snapshot timestamp')
|
|
21
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
let config;
|
|
24
|
+
try {
|
|
25
|
+
config = loadConfig();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error(`Error: ${err.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
|
|
32
|
+
if (opts.list) {
|
|
33
|
+
const snapshots = listSnapshots(graphUri);
|
|
34
|
+
if (snapshots.length === 0) {
|
|
35
|
+
console.log(pc.dim('No snapshots found for this graph.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(pc.bold(`Snapshots for ${graphUri}:`));
|
|
39
|
+
console.log('');
|
|
40
|
+
for (const s of snapshots) {
|
|
41
|
+
const sizeKb = (s.sizeBytes / 1024).toFixed(1);
|
|
42
|
+
const label = s.isPreRollback ? pc.yellow(' (pre-rollback)') : '';
|
|
43
|
+
console.log(` ${pc.dim(s.timestamp)} ${sizeKb} KB${label}`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const adapter = await createReadyAdapter(config);
|
|
49
|
+
if (opts.to) {
|
|
50
|
+
await restoreSnapshot(adapter, config, graphUri, opts.to);
|
|
51
|
+
console.log(pc.green(`Restored graph to snapshot: ${opts.to}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// No flags: restore latest snapshot with confirmation
|
|
55
|
+
const snapshots = listSnapshots(graphUri).filter((s) => !s.isPreRollback);
|
|
56
|
+
if (snapshots.length === 0) {
|
|
57
|
+
console.log(pc.dim('No snapshots found for this graph.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const latest = snapshots[0];
|
|
61
|
+
const answer = await ask(`Restore to latest snapshot ${pc.bold(latest.timestamp)}? [y/N] `);
|
|
62
|
+
if (answer === 'y' || answer === 'yes') {
|
|
63
|
+
await restoreSnapshot(adapter, config, graphUri, latest.timestamp);
|
|
64
|
+
console.log(pc.green(`Restored graph to snapshot: ${latest.timestamp}`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(pc.dim('Rollback cancelled.'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerPrefix } from './commands/prefix.js';
|
|
|
17
17
|
import { registerContext } from './commands/context.js';
|
|
18
18
|
import { registerViz } from './commands/viz.js';
|
|
19
19
|
import { registerDoctor } from './commands/doctor.js';
|
|
20
|
+
import { registerRollback } from './commands/rollback.js';
|
|
20
21
|
const program = new Command();
|
|
21
22
|
program
|
|
22
23
|
.name('opentology')
|
|
@@ -39,4 +40,5 @@ registerPrefix(program);
|
|
|
39
40
|
registerContext(program);
|
|
40
41
|
registerViz(program);
|
|
41
42
|
registerDoctor(program);
|
|
43
|
+
registerRollback(program);
|
|
42
44
|
program.parse(process.argv);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StoreAdapter } from './store-adapter.js';
|
|
2
|
+
import type { OpenTologyConfig } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Derive a filesystem-safe slug from a graph URI.
|
|
5
|
+
* Reuses the same pattern as persistGraph().
|
|
6
|
+
*/
|
|
7
|
+
export declare function graphSlug(graphUri: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Generate an ISO timestamp suitable for filenames.
|
|
10
|
+
* Format: 2026-04-05T14-30-22-123Z (colons replaced with dashes, ms included).
|
|
11
|
+
*/
|
|
12
|
+
export declare function toFilenameTimestamp(date?: Date): string;
|
|
13
|
+
/**
|
|
14
|
+
* Snapshot a named graph before a destructive operation.
|
|
15
|
+
* No-op if not embedded mode or if the graph is empty.
|
|
16
|
+
* Automatically prunes old snapshots after saving.
|
|
17
|
+
*/
|
|
18
|
+
export declare function snapshotGraph(adapter: StoreAdapter, config: OpenTologyConfig, graphUri: string, retention?: number): Promise<string | null>;
|
|
19
|
+
export interface SnapshotInfo {
|
|
20
|
+
timestamp: string;
|
|
21
|
+
path: string;
|
|
22
|
+
sizeBytes: number;
|
|
23
|
+
isPreRollback: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* List snapshots for a graph, sorted by timestamp descending (newest first).
|
|
27
|
+
*/
|
|
28
|
+
export declare function listSnapshots(graphUri: string): SnapshotInfo[];
|
|
29
|
+
/**
|
|
30
|
+
* Restore a graph to a specific snapshot.
|
|
31
|
+
* 1. Saves current state as {timestamp}_pre-rollback.ttl (snapshot-before-rollback)
|
|
32
|
+
* 2. Drops the graph and loads the snapshot
|
|
33
|
+
* 3. Persists the restored state to .opentology/data/
|
|
34
|
+
*/
|
|
35
|
+
export declare function restoreSnapshot(adapter: StoreAdapter, config: OpenTologyConfig, graphUri: string, timestamp: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Prune old snapshots, keeping only the most recent `keep` count.
|
|
38
|
+
* Pre-rollback snapshots are pruned separately (keep 2).
|
|
39
|
+
*/
|
|
40
|
+
export declare function pruneSnapshots(graphUri: string, keep?: number): Promise<number>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { persistGraph } from '../mcp/server.js';
|
|
4
|
+
const DEFAULT_RETENTION = 5;
|
|
5
|
+
/**
|
|
6
|
+
* Derive a filesystem-safe slug from a graph URI.
|
|
7
|
+
* Reuses the same pattern as persistGraph().
|
|
8
|
+
*/
|
|
9
|
+
export function graphSlug(graphUri) {
|
|
10
|
+
return graphUri.replace(/[^a-zA-Z0-9-]/g, '_').replace(/_+/g, '_');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate an ISO timestamp suitable for filenames.
|
|
14
|
+
* Format: 2026-04-05T14-30-22-123Z (colons replaced with dashes, ms included).
|
|
15
|
+
*/
|
|
16
|
+
export function toFilenameTimestamp(date = new Date()) {
|
|
17
|
+
return date.toISOString().replace(/:/g, '-').replace(/\./g, '-');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse a filename timestamp back to a Date.
|
|
21
|
+
*/
|
|
22
|
+
function parseFilenameTimestamp(filename) {
|
|
23
|
+
// Remove .ttl suffix and _pre-rollback suffix
|
|
24
|
+
const base = filename.replace(/\.ttl$/, '').replace(/_pre-rollback$/, '');
|
|
25
|
+
// Reverse the filename encoding: dashes back to colons/dots
|
|
26
|
+
// Format: 2026-04-05T14-30-22-123Z → 2026-04-05T14:30:22.123Z
|
|
27
|
+
const parts = base.match(/^(\d{4}-\d{2}-\d{2}T)(\d{2})-(\d{2})-(\d{2})-(\d{3}Z)$/);
|
|
28
|
+
if (!parts)
|
|
29
|
+
return null;
|
|
30
|
+
const iso = `${parts[1]}${parts[2]}:${parts[3]}:${parts[4]}.${parts[5]}`;
|
|
31
|
+
const d = new Date(iso);
|
|
32
|
+
return isNaN(d.getTime()) ? null : d;
|
|
33
|
+
}
|
|
34
|
+
function snapshotDir(graphUri) {
|
|
35
|
+
return join(process.cwd(), '.opentology', 'snapshots', graphSlug(graphUri));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot a named graph before a destructive operation.
|
|
39
|
+
* No-op if not embedded mode or if the graph is empty.
|
|
40
|
+
* Automatically prunes old snapshots after saving.
|
|
41
|
+
*/
|
|
42
|
+
export async function snapshotGraph(adapter, config, graphUri, retention = DEFAULT_RETENTION) {
|
|
43
|
+
if (config.mode !== 'embedded')
|
|
44
|
+
return null;
|
|
45
|
+
const exported = await adapter.exportGraph(graphUri);
|
|
46
|
+
if (!exported.trim())
|
|
47
|
+
return null;
|
|
48
|
+
const dir = snapshotDir(graphUri);
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const timestamp = toFilenameTimestamp();
|
|
51
|
+
const filename = `${timestamp}.ttl`;
|
|
52
|
+
const filePath = join(dir, filename);
|
|
53
|
+
writeFileSync(filePath, exported, 'utf-8');
|
|
54
|
+
// Verify the file was written
|
|
55
|
+
if (!existsSync(filePath)) {
|
|
56
|
+
throw new Error(`Snapshot write failed: ${filePath}`);
|
|
57
|
+
}
|
|
58
|
+
// Prune old snapshots (create → verify → prune order)
|
|
59
|
+
await pruneSnapshots(graphUri, retention);
|
|
60
|
+
return filePath;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List snapshots for a graph, sorted by timestamp descending (newest first).
|
|
64
|
+
*/
|
|
65
|
+
export function listSnapshots(graphUri) {
|
|
66
|
+
const dir = snapshotDir(graphUri);
|
|
67
|
+
if (!existsSync(dir))
|
|
68
|
+
return [];
|
|
69
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.ttl'));
|
|
70
|
+
return files
|
|
71
|
+
.map((f) => {
|
|
72
|
+
const fullPath = join(dir, f);
|
|
73
|
+
const stat = statSync(fullPath);
|
|
74
|
+
return {
|
|
75
|
+
timestamp: f.replace(/\.ttl$/, ''),
|
|
76
|
+
path: fullPath,
|
|
77
|
+
sizeBytes: stat.size,
|
|
78
|
+
isPreRollback: f.includes('_pre-rollback'),
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Restore a graph to a specific snapshot.
|
|
85
|
+
* 1. Saves current state as {timestamp}_pre-rollback.ttl (snapshot-before-rollback)
|
|
86
|
+
* 2. Drops the graph and loads the snapshot
|
|
87
|
+
* 3. Persists the restored state to .opentology/data/
|
|
88
|
+
*/
|
|
89
|
+
export async function restoreSnapshot(adapter, config, graphUri, timestamp) {
|
|
90
|
+
const dir = snapshotDir(graphUri);
|
|
91
|
+
const snapshotPath = join(dir, `${timestamp}.ttl`);
|
|
92
|
+
if (!existsSync(snapshotPath)) {
|
|
93
|
+
throw new Error(`Snapshot not found: ${timestamp}`);
|
|
94
|
+
}
|
|
95
|
+
// Step 1: Snapshot-before-rollback — save current state
|
|
96
|
+
const currentExport = await adapter.exportGraph(graphUri);
|
|
97
|
+
if (currentExport.trim()) {
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
const preRollbackTimestamp = toFilenameTimestamp();
|
|
100
|
+
const preRollbackPath = join(dir, `${preRollbackTimestamp}_pre-rollback.ttl`);
|
|
101
|
+
writeFileSync(preRollbackPath, currentExport, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
// Step 2: Drop and restore
|
|
104
|
+
const snapshotTurtle = readFileSync(snapshotPath, 'utf-8');
|
|
105
|
+
await adapter.dropGraph(graphUri);
|
|
106
|
+
await adapter.insertTurtle(graphUri, snapshotTurtle);
|
|
107
|
+
// Step 3: Persist restored state
|
|
108
|
+
await persistGraph(adapter, config, graphUri);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Prune old snapshots, keeping only the most recent `keep` count.
|
|
112
|
+
* Pre-rollback snapshots are pruned separately (keep 2).
|
|
113
|
+
*/
|
|
114
|
+
export async function pruneSnapshots(graphUri, keep = DEFAULT_RETENTION) {
|
|
115
|
+
const dir = snapshotDir(graphUri);
|
|
116
|
+
if (!existsSync(dir))
|
|
117
|
+
return 0;
|
|
118
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.ttl'));
|
|
119
|
+
// Separate regular snapshots and pre-rollback snapshots
|
|
120
|
+
const regular = files.filter((f) => !f.includes('_pre-rollback')).sort();
|
|
121
|
+
const preRollback = files.filter((f) => f.includes('_pre-rollback')).sort();
|
|
122
|
+
let deleted = 0;
|
|
123
|
+
// Prune regular snapshots
|
|
124
|
+
while (regular.length > keep) {
|
|
125
|
+
const oldest = regular.shift();
|
|
126
|
+
unlinkSync(join(dir, oldest));
|
|
127
|
+
deleted++;
|
|
128
|
+
}
|
|
129
|
+
// Prune pre-rollback snapshots (keep max 2)
|
|
130
|
+
while (preRollback.length > 2) {
|
|
131
|
+
const oldest = preRollback.shift();
|
|
132
|
+
unlinkSync(join(dir, oldest));
|
|
133
|
+
deleted++;
|
|
134
|
+
}
|
|
135
|
+
return deleted;
|
|
136
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { deepScan } from '../lib/deep-scanner.js';
|
|
|
6
6
|
import { pushSymbolTriples } from '../lib/deep-scan-triples.js';
|
|
7
7
|
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
8
8
|
import { normalizeModuleUri } from '../lib/module-uri.js';
|
|
9
|
+
import { snapshotGraph, listSnapshots, restoreSnapshot } from '../lib/snapshot.js';
|
|
9
10
|
import { hasGraphScope, autoScopeQuery, getInferenceGraphUri } from '../lib/sparql-utils.js';
|
|
10
11
|
import { validateTurtle } from '../lib/validator.js';
|
|
11
12
|
import { discoverShapes, validateWithShacl, hasShapes } from '../lib/shacl.js';
|
|
@@ -120,6 +121,7 @@ async function handlePush(args) {
|
|
|
120
121
|
});
|
|
121
122
|
const adapter = await createReadyAdapter(config);
|
|
122
123
|
if (replace) {
|
|
124
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
123
125
|
await adapter.dropGraph(graphUri);
|
|
124
126
|
}
|
|
125
127
|
await adapter.insertTurtle(graphUri, content);
|
|
@@ -220,6 +222,7 @@ async function handleDrop(args) {
|
|
|
220
222
|
graph: args.graph,
|
|
221
223
|
});
|
|
222
224
|
const adapter = await createReadyAdapter(config);
|
|
225
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
223
226
|
await adapter.dropGraph(graphUri);
|
|
224
227
|
await persistGraph(adapter, config, graphUri);
|
|
225
228
|
return { success: true, graphUri };
|
|
@@ -235,10 +238,32 @@ async function handleDelete(args) {
|
|
|
235
238
|
graph: args.graph,
|
|
236
239
|
});
|
|
237
240
|
const adapter = await createReadyAdapter(config);
|
|
241
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
238
242
|
await adapter.deleteTriples(graphUri, { turtle: content, where });
|
|
239
243
|
await persistGraph(adapter, config, graphUri);
|
|
240
244
|
return { success: true };
|
|
241
245
|
}
|
|
246
|
+
async function handleRollback(args) {
|
|
247
|
+
const action = args.action;
|
|
248
|
+
const { config, graphUri } = resolveConfig({
|
|
249
|
+
graphUri: args.graphUri,
|
|
250
|
+
graph: args.graph,
|
|
251
|
+
});
|
|
252
|
+
if (action === 'list') {
|
|
253
|
+
const snapshots = listSnapshots(graphUri);
|
|
254
|
+
return { graphUri, snapshots };
|
|
255
|
+
}
|
|
256
|
+
if (action === 'restore') {
|
|
257
|
+
const to = args.to;
|
|
258
|
+
if (!to) {
|
|
259
|
+
throw new Error('timestamp (to) is required for restore action');
|
|
260
|
+
}
|
|
261
|
+
const adapter = await createReadyAdapter(config);
|
|
262
|
+
await restoreSnapshot(adapter, config, graphUri, to);
|
|
263
|
+
return { success: true, graphUri, restoredTo: to };
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Unknown rollback action: ${action}. Use 'list' or 'restore'.`);
|
|
266
|
+
}
|
|
242
267
|
async function handleDiff(args) {
|
|
243
268
|
const content = args.content;
|
|
244
269
|
if (!content) {
|
|
@@ -310,6 +335,7 @@ async function handleGraphDrop(args) {
|
|
|
310
335
|
const config = loadConfig();
|
|
311
336
|
const graphUri = resolveGraphUri(config, name);
|
|
312
337
|
const adapter = await createReadyAdapter(config);
|
|
338
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
313
339
|
await adapter.dropGraph(graphUri);
|
|
314
340
|
await persistGraph(adapter, config, graphUri);
|
|
315
341
|
const graphs = config.graphs ?? {};
|
|
@@ -326,6 +352,7 @@ async function handleInfer(args) {
|
|
|
326
352
|
});
|
|
327
353
|
const adapter = await createReadyAdapter(config);
|
|
328
354
|
if (clear) {
|
|
355
|
+
await snapshotGraph(adapter, config, graphUri);
|
|
329
356
|
await clearInferences(adapter, graphUri);
|
|
330
357
|
return { success: true, cleared: true };
|
|
331
358
|
}
|
|
@@ -376,6 +403,8 @@ async function handleContextScan(args) {
|
|
|
376
403
|
const adapter = await createReadyAdapter(config);
|
|
377
404
|
if (snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0) {
|
|
378
405
|
const dg = snapshot.dependencyGraph;
|
|
406
|
+
// Snapshot before destructive scan
|
|
407
|
+
await snapshotGraph(adapter, config, contextUri);
|
|
379
408
|
// Scoped delete: clear stale module triples before re-insert
|
|
380
409
|
await adapter.sparqlUpdate(`DELETE WHERE { GRAPH <${contextUri}> { ?m a <https://opentology.dev/vocab#Module> . ?m ?p ?o } }`);
|
|
381
410
|
// Insert fresh triples
|
|
@@ -525,6 +554,20 @@ async function handleContextInit(args) {
|
|
|
525
554
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
526
555
|
actions.push('Auto-registered hooks in .claude/settings.json');
|
|
527
556
|
}
|
|
557
|
+
// Ensure .opentology/snapshots/ is in .gitignore
|
|
558
|
+
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
559
|
+
const snapshotIgnore = '.opentology/snapshots/';
|
|
560
|
+
if (existsSync(gitignorePath)) {
|
|
561
|
+
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
562
|
+
if (!gitignoreContent.includes(snapshotIgnore)) {
|
|
563
|
+
writeFileSync(gitignorePath, gitignoreContent.trimEnd() + '\n' + snapshotIgnore + '\n', 'utf-8');
|
|
564
|
+
actions.push('Added .opentology/snapshots/ to .gitignore');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
writeFileSync(gitignorePath, snapshotIgnore + '\n', 'utf-8');
|
|
569
|
+
actions.push('Created .gitignore with .opentology/snapshots/');
|
|
570
|
+
}
|
|
528
571
|
// Auto-push Module triples from dependency graph
|
|
529
572
|
let moduleStats = null;
|
|
530
573
|
try {
|
|
@@ -1262,6 +1305,33 @@ export async function startMcpServer() {
|
|
|
1262
1305
|
properties: {},
|
|
1263
1306
|
},
|
|
1264
1307
|
},
|
|
1308
|
+
{
|
|
1309
|
+
name: 'rollback',
|
|
1310
|
+
description: 'List or restore graph snapshots. Snapshots are automatically created before destructive operations (drop, delete, scan, etc.). Use action=list to see available snapshots, action=restore with a timestamp to restore.',
|
|
1311
|
+
inputSchema: {
|
|
1312
|
+
type: 'object',
|
|
1313
|
+
properties: {
|
|
1314
|
+
action: {
|
|
1315
|
+
type: 'string',
|
|
1316
|
+
enum: ['list', 'restore'],
|
|
1317
|
+
description: 'Action: list available snapshots or restore a specific one',
|
|
1318
|
+
},
|
|
1319
|
+
to: {
|
|
1320
|
+
type: 'string',
|
|
1321
|
+
description: 'Timestamp of the snapshot to restore (required for action=restore)',
|
|
1322
|
+
},
|
|
1323
|
+
graph: {
|
|
1324
|
+
type: 'string',
|
|
1325
|
+
description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
|
|
1326
|
+
},
|
|
1327
|
+
graphUri: {
|
|
1328
|
+
type: 'string',
|
|
1329
|
+
description: 'Named graph URI (uses config default if omitted)',
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
required: ['action'],
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1265
1335
|
],
|
|
1266
1336
|
}));
|
|
1267
1337
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1335,8 +1405,16 @@ export async function startMcpServer() {
|
|
|
1335
1405
|
case 'context_impact':
|
|
1336
1406
|
result = await handleContextImpact(args);
|
|
1337
1407
|
break;
|
|
1338
|
-
case 'context_sync':
|
|
1339
|
-
|
|
1408
|
+
case 'context_sync': {
|
|
1409
|
+
const syncConfig = loadConfig();
|
|
1410
|
+
const syncContextUri = `${syncConfig.graphUri}/context`;
|
|
1411
|
+
const syncAdapter = await createReadyAdapter(syncConfig);
|
|
1412
|
+
await snapshotGraph(syncAdapter, syncConfig, syncContextUri);
|
|
1413
|
+
result = await syncContext(syncConfig, process.cwd());
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
case 'rollback':
|
|
1417
|
+
result = await handleRollback(args);
|
|
1340
1418
|
break;
|
|
1341
1419
|
case 'doctor':
|
|
1342
1420
|
result = await runDoctor();
|