@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGC Runtime Freeze-Restore - Universal Checkpointing and Reconstruction
|
|
3
|
+
*
|
|
4
|
+
* Provides nanosecond-precision state snapshots with BLAKE3 integrity verification.
|
|
5
|
+
* Supports both Git-backed and filesystem-based storage strategies.
|
|
6
|
+
*
|
|
7
|
+
* @module freeze-restore
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { blake3 } from 'hash-wasm';
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { dirname } from 'path';
|
|
15
|
+
import {
|
|
16
|
+
compressFile,
|
|
17
|
+
readCompressed,
|
|
18
|
+
computeDelta,
|
|
19
|
+
applyDelta,
|
|
20
|
+
} from './storage-optimization.mjs';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
/** @typedef {Object} SnapshotManifest
|
|
26
|
+
* @property {string} timestamp_ns - Nanosecond precision timestamp (as string for BigInt)
|
|
27
|
+
* @property {string} o_hash - BLAKE3 hash of canonical state
|
|
28
|
+
* @property {number} file_count - Number of files in snapshot
|
|
29
|
+
* @property {number} total_bytes - Total bytes in snapshot
|
|
30
|
+
* @property {string} created_at - ISO 8601 timestamp
|
|
31
|
+
* @property {boolean} [compressed] - Whether snapshot is compressed
|
|
32
|
+
* @property {number} [original_size] - Original size before compression
|
|
33
|
+
* @property {number} [compressed_size] - Size after compression
|
|
34
|
+
* @property {boolean} [incremental] - Whether snapshot is incremental
|
|
35
|
+
* @property {string} [base_snapshot] - Base snapshot timestamp for incremental
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** @typedef {Object} UniverseState
|
|
39
|
+
* @property {Object} data - Arbitrary universe state data
|
|
40
|
+
* @property {bigint} [timestamp] - Optional timestamp
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** @typedef {Object} Snapshot
|
|
44
|
+
* @property {SnapshotManifest} manifest - Snapshot metadata
|
|
45
|
+
* @property {string} path - Filesystem path to snapshot
|
|
46
|
+
* @property {UniverseState} state - Reconstructed state
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get default snapshot directory path
|
|
51
|
+
* @returns {string} Absolute path to snapshots directory
|
|
52
|
+
*/
|
|
53
|
+
function getSnapshotDir() {
|
|
54
|
+
// Navigate up from src/ to package root, then to var/kgc/snapshots
|
|
55
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
56
|
+
return path.join(packageRoot, 'var', 'kgc', 'snapshots');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize and canonicalize state for deterministic hashing
|
|
61
|
+
* @param {UniverseState} state - Universe state object
|
|
62
|
+
* @returns {string} Canonical JSON representation
|
|
63
|
+
*/
|
|
64
|
+
function canonicalizeState(state) {
|
|
65
|
+
// Deep sort keys recursively for deterministic ordering
|
|
66
|
+
function sortKeys(obj) {
|
|
67
|
+
if (obj === null || typeof obj !== 'object') {
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(obj)) {
|
|
71
|
+
return obj.map(sortKeys);
|
|
72
|
+
}
|
|
73
|
+
const sorted = {};
|
|
74
|
+
const keys = Object.keys(obj).sort();
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
sorted[key] = sortKeys(obj[key]);
|
|
77
|
+
}
|
|
78
|
+
return sorted;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Convert BigInt to string for JSON serialization
|
|
82
|
+
function replacer(key, value) {
|
|
83
|
+
if (typeof value === 'bigint') {
|
|
84
|
+
return value.toString() + 'n';
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalized = sortKeys(state);
|
|
90
|
+
return JSON.stringify(normalized, replacer, 0); // No whitespace for determinism
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Freeze universe to snapshot with BLAKE3 integrity hash
|
|
95
|
+
*
|
|
96
|
+
* Creates a point-in-time snapshot of universe state with:
|
|
97
|
+
* - Nanosecond-precision timestamp (BigInt)
|
|
98
|
+
* - BLAKE3 hash for integrity verification
|
|
99
|
+
* - Manifest with metadata
|
|
100
|
+
* - Compressed JSON serialization
|
|
101
|
+
*
|
|
102
|
+
* @param {UniverseState} O - Universe state to freeze
|
|
103
|
+
* @param {Object} [options] - Freeze options
|
|
104
|
+
* @param {string} [options.snapshotDir] - Custom snapshot directory
|
|
105
|
+
* @param {boolean} [options.useGit=false] - Use Git backbone if available
|
|
106
|
+
* @param {boolean} [options.compress=true] - Compress snapshot with gzip
|
|
107
|
+
* @param {boolean} [options.incremental=false] - Create incremental snapshot
|
|
108
|
+
* @returns {Promise<SnapshotManifest>} Snapshot manifest with hash and metadata
|
|
109
|
+
* @throws {TypeError} If O is not an object
|
|
110
|
+
* @throws {Error} If freeze operation fails
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* const universe = { entities: [...], timestamp: 1234567890123456789n };
|
|
114
|
+
* const manifest = await freezeUniverse(universe);
|
|
115
|
+
* console.assert(manifest.o_hash, 'Has BLAKE3 hash');
|
|
116
|
+
* console.assert(manifest.timestamp_ns, 'Has nanosecond timestamp');
|
|
117
|
+
*/
|
|
118
|
+
export async function freezeUniverse(O, options = {}) {
|
|
119
|
+
// Validation
|
|
120
|
+
if (!O || typeof O !== 'object') {
|
|
121
|
+
throw new TypeError('freezeUniverse: O must be an object');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// 1. Generate nanosecond timestamp
|
|
126
|
+
const timestamp_ns = process.hrtime.bigint();
|
|
127
|
+
|
|
128
|
+
// 2. Canonicalize state for deterministic hashing
|
|
129
|
+
const canonical_state = canonicalizeState(O);
|
|
130
|
+
|
|
131
|
+
// 3. Compute BLAKE3 hash
|
|
132
|
+
const o_hash = await blake3(canonical_state);
|
|
133
|
+
|
|
134
|
+
// 4. Create snapshot directory
|
|
135
|
+
const snapshotDir = options.snapshotDir || getSnapshotDir();
|
|
136
|
+
const snapshotPath = path.join(snapshotDir, timestamp_ns.toString());
|
|
137
|
+
await fs.mkdir(snapshotPath, { recursive: true });
|
|
138
|
+
|
|
139
|
+
// 5. Handle incremental snapshots
|
|
140
|
+
let stateToWrite = canonical_state;
|
|
141
|
+
let isIncremental = false;
|
|
142
|
+
let baseSnapshot = null;
|
|
143
|
+
|
|
144
|
+
if (options.incremental) {
|
|
145
|
+
// Get previous snapshot for delta computation
|
|
146
|
+
const snapshots = await getSnapshotList({ snapshotDir });
|
|
147
|
+
if (snapshots.length > 0) {
|
|
148
|
+
const prevSnapshot = snapshots[0];
|
|
149
|
+
const prevStatePath = path.join(
|
|
150
|
+
prevSnapshot.path,
|
|
151
|
+
prevSnapshot.manifest.compressed ? 'state.json.gz' : 'state.json'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
let prevState;
|
|
156
|
+
if (prevSnapshot.manifest.compressed) {
|
|
157
|
+
prevState = await readCompressed(prevStatePath);
|
|
158
|
+
} else {
|
|
159
|
+
prevState = await fs.readFile(prevStatePath, 'utf-8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const prevObj = JSON.parse(prevState);
|
|
163
|
+
const currObj = JSON.parse(canonical_state);
|
|
164
|
+
const delta = computeDelta(prevObj, currObj);
|
|
165
|
+
|
|
166
|
+
// Only use delta if it's significantly smaller
|
|
167
|
+
const deltaStr = JSON.stringify(delta);
|
|
168
|
+
if (deltaStr.length < canonical_state.length * 0.5) {
|
|
169
|
+
stateToWrite = deltaStr;
|
|
170
|
+
isIncremental = true;
|
|
171
|
+
baseSnapshot = prevSnapshot.manifest.timestamp_ns;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Fall back to full snapshot if delta fails
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 6. Write state file
|
|
180
|
+
const statePath = path.join(snapshotPath, 'state.json');
|
|
181
|
+
await fs.writeFile(statePath, stateToWrite, 'utf-8');
|
|
182
|
+
|
|
183
|
+
// 7. Compress if requested (default: true)
|
|
184
|
+
const shouldCompress = options.compress !== false;
|
|
185
|
+
let finalSize = 0;
|
|
186
|
+
let originalSize = 0;
|
|
187
|
+
let compressed = false;
|
|
188
|
+
|
|
189
|
+
if (shouldCompress) {
|
|
190
|
+
const compressedPath = statePath + '.gz';
|
|
191
|
+
const compressionResult = await compressFile(statePath, compressedPath);
|
|
192
|
+
originalSize = compressionResult.original_size;
|
|
193
|
+
finalSize = compressionResult.compressed_size;
|
|
194
|
+
compressed = true;
|
|
195
|
+
|
|
196
|
+
// Remove uncompressed file
|
|
197
|
+
await fs.unlink(statePath);
|
|
198
|
+
} else {
|
|
199
|
+
const stats = await fs.stat(statePath);
|
|
200
|
+
finalSize = stats.size;
|
|
201
|
+
originalSize = stats.size;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 8. Create manifest
|
|
205
|
+
const manifest = {
|
|
206
|
+
timestamp_ns: timestamp_ns.toString(),
|
|
207
|
+
o_hash,
|
|
208
|
+
file_count: 1,
|
|
209
|
+
total_bytes: finalSize,
|
|
210
|
+
created_at: new Date().toISOString(),
|
|
211
|
+
compressed,
|
|
212
|
+
...(compressed && { original_size: originalSize, compressed_size: finalSize }),
|
|
213
|
+
...(isIncremental && { incremental: true, base_snapshot: baseSnapshot }),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 9. Write manifest
|
|
217
|
+
const manifestPath = path.join(snapshotPath, 'manifest.json');
|
|
218
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
219
|
+
|
|
220
|
+
// 10. Optional: Git commit if requested
|
|
221
|
+
if (options.useGit) {
|
|
222
|
+
try {
|
|
223
|
+
const gitDir = path.resolve(snapshotDir, '../../..');
|
|
224
|
+
const gitExists = await fs.access(path.join(gitDir, '.git'))
|
|
225
|
+
.then(() => true)
|
|
226
|
+
.catch(() => false);
|
|
227
|
+
|
|
228
|
+
if (gitExists) {
|
|
229
|
+
// Git operations would go here - simplified for now
|
|
230
|
+
// This would use isomorphic-git to commit the snapshot
|
|
231
|
+
}
|
|
232
|
+
} catch (gitError) {
|
|
233
|
+
// Git operations are optional - continue without them
|
|
234
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
235
|
+
console.warn(`[KGC Freeze] Git commit skipped: ${gitError.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return manifest;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error(`Failed to freeze universe: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Verify snapshot integrity by recomputing hash
|
|
248
|
+
*
|
|
249
|
+
* Validates that snapshot data matches stored hash, ensuring:
|
|
250
|
+
* - Data integrity (no corruption)
|
|
251
|
+
* - Authenticity (matches original freeze)
|
|
252
|
+
* - Completeness (all files present)
|
|
253
|
+
*
|
|
254
|
+
* @param {SnapshotManifest|string} snapshot - Snapshot manifest or timestamp string
|
|
255
|
+
* @param {Object} [options] - Verification options
|
|
256
|
+
* @param {string} [options.snapshotDir] - Custom snapshot directory
|
|
257
|
+
* @returns {Promise<boolean>} True if snapshot is valid
|
|
258
|
+
* @throws {TypeError} If snapshot parameter is invalid
|
|
259
|
+
* @throws {Error} If snapshot not found
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* const valid = await verifyFreeze(manifest);
|
|
263
|
+
* console.assert(valid === true, 'Snapshot is valid');
|
|
264
|
+
*/
|
|
265
|
+
export async function verifyFreeze(snapshot, options = {}) {
|
|
266
|
+
// Handle both manifest objects and timestamp strings
|
|
267
|
+
let timestamp_ns;
|
|
268
|
+
let expected_hash;
|
|
269
|
+
|
|
270
|
+
if (typeof snapshot === 'string') {
|
|
271
|
+
timestamp_ns = snapshot;
|
|
272
|
+
// Load manifest to get hash
|
|
273
|
+
const snapshotDir = options.snapshotDir || getSnapshotDir();
|
|
274
|
+
const manifestPath = path.join(snapshotDir, timestamp_ns, 'manifest.json');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
278
|
+
const manifest = JSON.parse(manifestData);
|
|
279
|
+
expected_hash = manifest.o_hash;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(`Snapshot not found: ${timestamp_ns}`);
|
|
282
|
+
}
|
|
283
|
+
} else if (snapshot && typeof snapshot === 'object') {
|
|
284
|
+
if (!snapshot.timestamp_ns || !snapshot.o_hash) {
|
|
285
|
+
throw new TypeError('verifyFreeze: snapshot must have timestamp_ns and o_hash');
|
|
286
|
+
}
|
|
287
|
+
timestamp_ns = snapshot.timestamp_ns;
|
|
288
|
+
expected_hash = snapshot.o_hash;
|
|
289
|
+
} else {
|
|
290
|
+
throw new TypeError('verifyFreeze: snapshot must be manifest object or timestamp string');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// 1. Load manifest to check compression
|
|
295
|
+
const snapshotDir = options.snapshotDir || getSnapshotDir();
|
|
296
|
+
const manifestPath = path.join(snapshotDir, timestamp_ns, 'manifest.json');
|
|
297
|
+
let manifest;
|
|
298
|
+
try {
|
|
299
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
300
|
+
manifest = JSON.parse(manifestData);
|
|
301
|
+
} catch {
|
|
302
|
+
manifest = { compressed: false };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 2. Load state file (handle compression)
|
|
306
|
+
const isCompressed = manifest.compressed === true;
|
|
307
|
+
const stateFileName = isCompressed ? 'state.json.gz' : 'state.json';
|
|
308
|
+
const statePath = path.join(snapshotDir, timestamp_ns, stateFileName);
|
|
309
|
+
|
|
310
|
+
let stateData;
|
|
311
|
+
if (isCompressed) {
|
|
312
|
+
stateData = await readCompressed(statePath);
|
|
313
|
+
} else {
|
|
314
|
+
stateData = await fs.readFile(statePath, 'utf-8');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 3. Recompute hash
|
|
318
|
+
const recomputed_hash = await blake3(stateData);
|
|
319
|
+
|
|
320
|
+
// 4. Compare hashes
|
|
321
|
+
return recomputed_hash === expected_hash;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new Error(`Failed to verify snapshot: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Reconstruct universe state from snapshot at specific time
|
|
329
|
+
*
|
|
330
|
+
* Loads and validates snapshot, returning reconstructed state:
|
|
331
|
+
* - Finds snapshot at or before target time
|
|
332
|
+
* - Validates manifest integrity
|
|
333
|
+
* - Deserializes state
|
|
334
|
+
* - Returns fully reconstructed object
|
|
335
|
+
*
|
|
336
|
+
* @param {bigint|string} t_ns - Target time in nanoseconds (BigInt or string)
|
|
337
|
+
* @param {Object} [options] - Reconstruction options
|
|
338
|
+
* @param {string} [options.snapshotDir] - Custom snapshot directory
|
|
339
|
+
* @param {boolean} [options.exact=false] - Require exact timestamp match
|
|
340
|
+
* @returns {Promise<UniverseState>} Reconstructed universe state
|
|
341
|
+
* @throws {TypeError} If t_ns is invalid
|
|
342
|
+
* @throws {Error} If no snapshot found before target time
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* const targetTime = 1234567890123456789n;
|
|
346
|
+
* const reconstructed = await reconstructTo(targetTime);
|
|
347
|
+
* console.log('State at', targetTime, ':', reconstructed);
|
|
348
|
+
*/
|
|
349
|
+
export async function reconstructTo(t_ns, options = {}) {
|
|
350
|
+
// Normalize to BigInt
|
|
351
|
+
let targetTime;
|
|
352
|
+
if (typeof t_ns === 'bigint') {
|
|
353
|
+
targetTime = t_ns;
|
|
354
|
+
} else if (typeof t_ns === 'string') {
|
|
355
|
+
targetTime = BigInt(t_ns);
|
|
356
|
+
} else {
|
|
357
|
+
throw new TypeError('reconstructTo: t_ns must be BigInt or string');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (targetTime < 0n) {
|
|
361
|
+
throw new RangeError('reconstructTo: t_ns must be non-negative');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// 1. Get list of all snapshots
|
|
366
|
+
const snapshots = await getSnapshotList(options);
|
|
367
|
+
|
|
368
|
+
if (snapshots.length === 0) {
|
|
369
|
+
throw new Error('No snapshots available for reconstruction');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 2. Find best snapshot (at or before target time)
|
|
373
|
+
let bestSnapshot = null;
|
|
374
|
+
let bestTime = -1n;
|
|
375
|
+
|
|
376
|
+
for (const snapshot of snapshots) {
|
|
377
|
+
const snapshotTime = BigInt(snapshot.manifest.timestamp_ns);
|
|
378
|
+
|
|
379
|
+
if (options.exact) {
|
|
380
|
+
// Exact match required
|
|
381
|
+
if (snapshotTime === targetTime) {
|
|
382
|
+
bestSnapshot = snapshot;
|
|
383
|
+
bestTime = snapshotTime;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Find closest snapshot at or before target
|
|
388
|
+
if (snapshotTime <= targetTime && snapshotTime > bestTime) {
|
|
389
|
+
bestSnapshot = snapshot;
|
|
390
|
+
bestTime = snapshotTime;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!bestSnapshot) {
|
|
396
|
+
const matchType = options.exact ? 'exact' : 'at or before';
|
|
397
|
+
throw new Error(`No snapshot found ${matchType} time ${targetTime}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 3. Verify snapshot integrity
|
|
401
|
+
const isValid = await verifyFreeze(bestSnapshot.manifest, options);
|
|
402
|
+
if (!isValid) {
|
|
403
|
+
throw new Error(`Snapshot integrity check failed for ${bestSnapshot.manifest.timestamp_ns}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 4. Load and deserialize state (handle compression and incremental)
|
|
407
|
+
const snapshotDir = options.snapshotDir || getSnapshotDir();
|
|
408
|
+
const isCompressed = bestSnapshot.manifest.compressed === true;
|
|
409
|
+
const stateFileName = isCompressed ? 'state.json.gz' : 'state.json';
|
|
410
|
+
const statePath = path.join(
|
|
411
|
+
snapshotDir,
|
|
412
|
+
bestSnapshot.manifest.timestamp_ns,
|
|
413
|
+
stateFileName
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
let stateData;
|
|
417
|
+
if (isCompressed) {
|
|
418
|
+
stateData = await readCompressed(statePath);
|
|
419
|
+
} else {
|
|
420
|
+
stateData = await fs.readFile(statePath, 'utf-8');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 5. Handle incremental snapshots
|
|
424
|
+
let finalState;
|
|
425
|
+
if (bestSnapshot.manifest.incremental && bestSnapshot.manifest.base_snapshot) {
|
|
426
|
+
// Reconstruct from base + delta
|
|
427
|
+
const delta = JSON.parse(stateData);
|
|
428
|
+
const baseState = await reconstructTo(
|
|
429
|
+
BigInt(bestSnapshot.manifest.base_snapshot),
|
|
430
|
+
options
|
|
431
|
+
);
|
|
432
|
+
finalState = applyDelta(baseState, delta);
|
|
433
|
+
} else {
|
|
434
|
+
// Revive BigInt values
|
|
435
|
+
finalState = JSON.parse(stateData, (key, value) => {
|
|
436
|
+
if (typeof value === 'string' && value.endsWith('n')) {
|
|
437
|
+
try {
|
|
438
|
+
return BigInt(value.slice(0, -1));
|
|
439
|
+
} catch {
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return finalState;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
throw new Error(`Failed to reconstruct state: ${error.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get list of all snapshots sorted by timestamp
|
|
455
|
+
*
|
|
456
|
+
* Scans snapshot directory and returns sorted list of available snapshots
|
|
457
|
+
* with their manifests loaded and validated.
|
|
458
|
+
*
|
|
459
|
+
* @param {Object} [options] - List options
|
|
460
|
+
* @param {string} [options.snapshotDir] - Custom snapshot directory
|
|
461
|
+
* @param {boolean} [options.ascending=false] - Sort oldest first (default: newest first)
|
|
462
|
+
* @returns {Promise<Snapshot[]>} Array of snapshots sorted by time
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* const snapshots = await getSnapshotList();
|
|
466
|
+
* console.log(`Found ${snapshots.length} snapshots`);
|
|
467
|
+
* console.log('Latest:', snapshots[0].manifest.timestamp_ns);
|
|
468
|
+
*/
|
|
469
|
+
export async function getSnapshotList(options = {}) {
|
|
470
|
+
const snapshotDir = options.snapshotDir || getSnapshotDir();
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
// Ensure directory exists
|
|
474
|
+
await fs.mkdir(snapshotDir, { recursive: true });
|
|
475
|
+
|
|
476
|
+
// Read directory entries
|
|
477
|
+
const entries = await fs.readdir(snapshotDir, { withFileTypes: true });
|
|
478
|
+
|
|
479
|
+
// Filter for directories (each snapshot is a directory)
|
|
480
|
+
const snapshotDirs = entries
|
|
481
|
+
.filter(entry => entry.isDirectory())
|
|
482
|
+
.map(entry => entry.name);
|
|
483
|
+
|
|
484
|
+
// Load manifests
|
|
485
|
+
const snapshots = [];
|
|
486
|
+
for (const dirName of snapshotDirs) {
|
|
487
|
+
try {
|
|
488
|
+
const manifestPath = path.join(snapshotDir, dirName, 'manifest.json');
|
|
489
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
490
|
+
const manifest = JSON.parse(manifestData);
|
|
491
|
+
|
|
492
|
+
snapshots.push({
|
|
493
|
+
manifest,
|
|
494
|
+
path: path.join(snapshotDir, dirName),
|
|
495
|
+
});
|
|
496
|
+
} catch (error) {
|
|
497
|
+
// Skip invalid snapshots
|
|
498
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
499
|
+
console.warn(`[KGC Snapshots] Skipped invalid snapshot ${dirName}: ${error.message}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Sort by timestamp (newest first by default)
|
|
505
|
+
snapshots.sort((a, b) => {
|
|
506
|
+
const timeA = BigInt(a.manifest.timestamp_ns);
|
|
507
|
+
const timeB = BigInt(b.manifest.timestamp_ns);
|
|
508
|
+
|
|
509
|
+
if (options.ascending) {
|
|
510
|
+
return timeA < timeB ? -1 : timeA > timeB ? 1 : 0;
|
|
511
|
+
} else {
|
|
512
|
+
return timeB < timeA ? -1 : timeB > timeA ? 1 : 0;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return snapshots;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
// If directory doesn't exist or other error, return empty array
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGC Runtime - Main Entry Point
|
|
3
|
+
* Exports admission gate, receipt chain system, work item executor, merge functionality,
|
|
4
|
+
* custom validators, and enhanced bounds enforcement
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { AdmissionGate } from './admission-gate.mjs';
|
|
8
|
+
export { WorkItemExecutor, WORK_ITEM_STATES } from './work-item.mjs';
|
|
9
|
+
export {
|
|
10
|
+
shardMerge,
|
|
11
|
+
mergeCapsules,
|
|
12
|
+
ConflictDetector,
|
|
13
|
+
ConflictResolver,
|
|
14
|
+
} from './merge.mjs';
|
|
15
|
+
export { EnhancedBoundsChecker } from './enhanced-bounds.mjs';
|
|
16
|
+
export {
|
|
17
|
+
validateReceiptChainIntegrity,
|
|
18
|
+
validateTemporalConsistency,
|
|
19
|
+
validateArtifactHash,
|
|
20
|
+
validateDependencyDAG,
|
|
21
|
+
validateAsyncPolicy,
|
|
22
|
+
validateTimeRange,
|
|
23
|
+
ReceiptChainSchema,
|
|
24
|
+
TemporallyOrderedSchema,
|
|
25
|
+
ArtifactSchema,
|
|
26
|
+
WorkItemDependencySchema,
|
|
27
|
+
RunCapsuleTimeRangeSchema,
|
|
28
|
+
createAsyncPolicySchema,
|
|
29
|
+
detectCycle,
|
|
30
|
+
combineValidators,
|
|
31
|
+
createValidationResult,
|
|
32
|
+
} from './validators.mjs';
|
|
33
|
+
export {
|
|
34
|
+
generateReceipt,
|
|
35
|
+
verifyReceiptHash,
|
|
36
|
+
verifyReceiptChain,
|
|
37
|
+
ReceiptStore,
|
|
38
|
+
} from './receipt.mjs';
|
|
39
|
+
export {
|
|
40
|
+
RunCapsule,
|
|
41
|
+
storeCapsule,
|
|
42
|
+
replayCapsule,
|
|
43
|
+
listCapsules,
|
|
44
|
+
} from './capsule.mjs';
|
|
45
|
+
export {
|
|
46
|
+
PluginManager,
|
|
47
|
+
PLUGIN_STATES,
|
|
48
|
+
createPluginManager,
|
|
49
|
+
} from './plugin-manager.mjs';
|
|
50
|
+
export {
|
|
51
|
+
PluginIsolation,
|
|
52
|
+
createPluginIsolation,
|
|
53
|
+
createPublicAPI,
|
|
54
|
+
} from './plugin-isolation.mjs';
|
|
55
|
+
export {
|
|
56
|
+
APIVersionManager,
|
|
57
|
+
CURRENT_API_VERSION,
|
|
58
|
+
API_STATUS,
|
|
59
|
+
getVersionManager,
|
|
60
|
+
isPluginCompatible,
|
|
61
|
+
validatePluginVersion,
|
|
62
|
+
} from './api-version.mjs';
|