@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.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,466 @@
1
+ /**
2
+ * @fileoverview Two-Phase Commit Transaction Manager for KGC Runtime
3
+ * Implements atomic transactions with prepare/commit/rollback phases
4
+ *
5
+ * Pattern: Pure functions + Zod validation + Receipt generation
6
+ * Guarantees: ACID properties for capsule operations
7
+ */
8
+
9
+ import { blake3 } from 'hash-wasm';
10
+ import { z } from 'zod';
11
+ import { generateReceipt } from './receipt.mjs';
12
+
13
+ // ============================================================================
14
+ // Schemas
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Operation schema - represents a single atomic operation
19
+ */
20
+ const OperationSchema = z.object({
21
+ id: z.string(),
22
+ type: z.enum(['add_capsule', 'remove_capsule', 'update_state', 'merge']),
23
+ data: z.any(),
24
+ undo: z.any().optional(), // Undo information for rollback
25
+ });
26
+
27
+ /**
28
+ * Transaction schema
29
+ */
30
+ const TransactionSchema = z.object({
31
+ id: z.string(),
32
+ timestamp: z.string(),
33
+ operations: z.array(OperationSchema),
34
+ status: z.enum(['pending', 'prepared', 'committed', 'aborted']),
35
+ preparedAt: z.string().optional(),
36
+ committedAt: z.string().optional(),
37
+ abortedAt: z.string().optional(),
38
+ hash: z.string().optional(),
39
+ parentHash: z.string().optional().nullable(),
40
+ });
41
+
42
+ /**
43
+ * @typedef {z.infer<typeof TransactionSchema>} Transaction
44
+ */
45
+
46
+ /**
47
+ * @typedef {z.infer<typeof OperationSchema>} Operation
48
+ */
49
+
50
+ // ============================================================================
51
+ // TransactionManager Class
52
+ // ============================================================================
53
+
54
+ /**
55
+ * TransactionManager - Implements two-phase commit protocol
56
+ *
57
+ * Phase 1 (Prepare):
58
+ * - Validate all operations
59
+ * - Reserve resources
60
+ * - Create undo log entries
61
+ * - Check constraints
62
+ *
63
+ * Phase 2 (Commit):
64
+ * - Apply all changes atomically
65
+ * - Generate receipts
66
+ * - Update state
67
+ *
68
+ * Rollback:
69
+ * - If phase 2 fails, undo phase 1 changes
70
+ * - Use undo log to restore previous state
71
+ *
72
+ * @example
73
+ * const txManager = new TransactionManager();
74
+ * const tx = txManager.begin([
75
+ * { id: 'op1', type: 'add_capsule', data: capsule1 },
76
+ * { id: 'op2', type: 'add_capsule', data: capsule2 }
77
+ * ]);
78
+ *
79
+ * const prepared = await txManager.prepare(tx.id);
80
+ * if (prepared.success) {
81
+ * const committed = await txManager.commit(tx.id);
82
+ * } else {
83
+ * await txManager.rollback(tx.id);
84
+ * }
85
+ */
86
+ export class TransactionManager {
87
+ /**
88
+ * @param {Object} options - Configuration options
89
+ * @param {Function} options.onRollback - Callback for rollback events
90
+ * @param {string} options.logPath - Path to rollback log file
91
+ */
92
+ constructor(options = {}) {
93
+ /** @type {Map<string, Transaction>} */
94
+ this.transactions = new Map();
95
+
96
+ /** @type {string[]} */
97
+ this.transactionHistory = [];
98
+
99
+ /** @type {Function} */
100
+ this.onRollback = options.onRollback || (() => {});
101
+
102
+ /** @type {string} */
103
+ this.logPath = options.logPath || './var/kgc/undo-log.json';
104
+
105
+ /** @type {any} */
106
+ this.state = {};
107
+ }
108
+
109
+ /**
110
+ * Begin a new transaction
111
+ *
112
+ * @param {Operation[]} operations - Operations to execute
113
+ * @param {string} [parentHash] - Parent transaction hash for chaining
114
+ * @returns {Transaction} New transaction
115
+ */
116
+ begin(operations, parentHash = null) {
117
+ const timestamp = new Date().toISOString();
118
+ const id = `tx_${Date.now()}_${Math.random().toString(36).substring(7)}`;
119
+
120
+ const transaction = {
121
+ id,
122
+ timestamp,
123
+ operations: z.array(OperationSchema).parse(operations),
124
+ status: 'pending',
125
+ parentHash,
126
+ };
127
+
128
+ this.transactions.set(id, transaction);
129
+ this.transactionHistory.push(id);
130
+
131
+ return TransactionSchema.parse(transaction);
132
+ }
133
+
134
+ /**
135
+ * Phase 1: Prepare transaction
136
+ * Validates operations and reserves resources
137
+ *
138
+ * @param {string} txId - Transaction ID
139
+ * @returns {Promise<{success: boolean, errors: string[], undoOps: any[]}>} Prepare result
140
+ */
141
+ async prepare(txId) {
142
+ const tx = this.transactions.get(txId);
143
+ if (!tx) {
144
+ return { success: false, errors: ['Transaction not found'], undoOps: [] };
145
+ }
146
+
147
+ if (tx.status !== 'pending') {
148
+ return { success: false, errors: ['Transaction not in pending state'], undoOps: [] };
149
+ }
150
+
151
+ const errors = [];
152
+ const undoOps = [];
153
+
154
+ // Validate each operation and create undo entries
155
+ for (const op of tx.operations) {
156
+ try {
157
+ // Validate operation structure
158
+ OperationSchema.parse(op);
159
+
160
+ // Create undo operation
161
+ const undoOp = this._createUndoOperation(op);
162
+ undoOps.push(undoOp);
163
+
164
+ // Store undo info in operation
165
+ op.undo = undoOp;
166
+
167
+ // Validate constraints (without applying changes)
168
+ const validation = this._validateOperation(op);
169
+ if (!validation.success) {
170
+ errors.push(...validation.errors);
171
+ }
172
+ } catch (error) {
173
+ errors.push(`Operation ${op.id} validation failed: ${error.message}`);
174
+ }
175
+ }
176
+
177
+ if (errors.length > 0) {
178
+ tx.status = 'aborted';
179
+ tx.abortedAt = new Date().toISOString();
180
+ return { success: false, errors, undoOps: [] };
181
+ }
182
+
183
+ // All validations passed - mark as prepared
184
+ tx.status = 'prepared';
185
+ tx.preparedAt = new Date().toISOString();
186
+
187
+ // Generate transaction hash
188
+ const hash = await this._hashTransaction(tx);
189
+ tx.hash = hash;
190
+
191
+ return { success: true, errors: [], undoOps };
192
+ }
193
+
194
+ /**
195
+ * Phase 2: Commit transaction
196
+ * Applies all changes atomically
197
+ *
198
+ * @param {string} txId - Transaction ID
199
+ * @returns {Promise<{success: boolean, receipts: any[], errors: string[]}>} Commit result
200
+ */
201
+ async commit(txId) {
202
+ const tx = this.transactions.get(txId);
203
+ if (!tx) {
204
+ return { success: false, receipts: [], errors: ['Transaction not found'] };
205
+ }
206
+
207
+ if (tx.status !== 'prepared') {
208
+ return { success: false, receipts: [], errors: ['Transaction not prepared'] };
209
+ }
210
+
211
+ const receipts = [];
212
+ const errors = [];
213
+ const appliedOps = [];
214
+
215
+ try {
216
+ // Apply all operations
217
+ for (const op of tx.operations) {
218
+ try {
219
+ // Apply operation to state
220
+ this._applyOperation(op);
221
+ appliedOps.push(op);
222
+
223
+ // Generate receipt
224
+ const receipt = await generateReceipt(
225
+ op.type,
226
+ { operation_id: op.id, data: op.data },
227
+ { success: true, transaction_id: txId },
228
+ tx.parentHash
229
+ );
230
+ receipts.push(receipt);
231
+ } catch (error) {
232
+ errors.push(`Operation ${op.id} failed: ${error.message}`);
233
+
234
+ // Rollback applied operations
235
+ for (const appliedOp of appliedOps.reverse()) {
236
+ if (appliedOp.undo) {
237
+ this._applyOperation(appliedOp.undo);
238
+ }
239
+ }
240
+
241
+ tx.status = 'aborted';
242
+ tx.abortedAt = new Date().toISOString();
243
+ return { success: false, receipts: [], errors };
244
+ }
245
+ }
246
+
247
+ // All operations applied successfully
248
+ tx.status = 'committed';
249
+ tx.committedAt = new Date().toISOString();
250
+
251
+ return { success: true, receipts, errors: [] };
252
+ } catch (error) {
253
+ tx.status = 'aborted';
254
+ tx.abortedAt = new Date().toISOString();
255
+ return { success: false, receipts: [], errors: [error.message] };
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Rollback transaction
261
+ * Undoes all changes made during prepare/commit
262
+ *
263
+ * @param {string} txId - Transaction ID
264
+ * @returns {Promise<{success: boolean, undone: number}>} Rollback result
265
+ */
266
+ async rollback(txId) {
267
+ const tx = this.transactions.get(txId);
268
+ if (!tx) {
269
+ return { success: false, undone: 0 };
270
+ }
271
+
272
+ let undone = 0;
273
+
274
+ // Undo operations in reverse order
275
+ for (const op of tx.operations.slice().reverse()) {
276
+ if (op.undo) {
277
+ try {
278
+ this._applyOperation(op.undo);
279
+ undone++;
280
+ } catch (error) {
281
+ // Log error but continue rollback
282
+ console.error(`Failed to undo operation ${op.id}:`, error);
283
+ }
284
+ }
285
+ }
286
+
287
+ tx.status = 'aborted';
288
+ tx.abortedAt = new Date().toISOString();
289
+
290
+ // Notify callback
291
+ this.onRollback({ transaction_id: txId, operations_undone: undone });
292
+
293
+ return { success: true, undone };
294
+ }
295
+
296
+ /**
297
+ * Get transaction by ID
298
+ * @param {string} txId - Transaction ID
299
+ * @returns {Transaction|null} Transaction or null
300
+ */
301
+ getTransaction(txId) {
302
+ const tx = this.transactions.get(txId);
303
+ return tx ? TransactionSchema.parse(tx) : null;
304
+ }
305
+
306
+ /**
307
+ * Get all transactions
308
+ * @returns {Transaction[]} All transactions
309
+ */
310
+ getAllTransactions() {
311
+ return this.transactionHistory.map(id => this.getTransaction(id)).filter(Boolean);
312
+ }
313
+
314
+ /**
315
+ * Create undo operation for given operation
316
+ * @private
317
+ */
318
+ _createUndoOperation(operation) {
319
+ switch (operation.type) {
320
+ case 'add_capsule':
321
+ return {
322
+ id: `undo_${operation.id}`,
323
+ type: 'remove_capsule',
324
+ data: { capsule_id: operation.data.id },
325
+ };
326
+ case 'remove_capsule':
327
+ return {
328
+ id: `undo_${operation.id}`,
329
+ type: 'add_capsule',
330
+ data: this.state.capsules?.[operation.data.capsule_id] || operation.data,
331
+ };
332
+ case 'update_state':
333
+ return {
334
+ id: `undo_${operation.id}`,
335
+ type: 'update_state',
336
+ data: {
337
+ key: operation.data.key,
338
+ value: this.state[operation.data.key],
339
+ previous: operation.data.value,
340
+ },
341
+ };
342
+ case 'merge':
343
+ return {
344
+ id: `undo_${operation.id}`,
345
+ type: 'update_state',
346
+ data: {
347
+ key: 'last_merge',
348
+ value: this.state.last_merge || null,
349
+ },
350
+ };
351
+ default:
352
+ return {
353
+ id: `undo_${operation.id}`,
354
+ type: 'update_state',
355
+ data: {},
356
+ };
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Validate operation without applying it
362
+ * @private
363
+ */
364
+ _validateOperation(operation) {
365
+ const errors = [];
366
+
367
+ switch (operation.type) {
368
+ case 'add_capsule':
369
+ if (!operation.data?.id) {
370
+ errors.push('Capsule must have an id');
371
+ }
372
+ break;
373
+ case 'remove_capsule':
374
+ if (!operation.data?.capsule_id) {
375
+ errors.push('Remove operation must specify capsule_id');
376
+ }
377
+ break;
378
+ case 'update_state':
379
+ if (!operation.data?.key) {
380
+ errors.push('Update operation must specify key');
381
+ }
382
+ break;
383
+ case 'merge':
384
+ if (!operation.data?.capsules) {
385
+ errors.push('Merge operation must specify capsules');
386
+ }
387
+ break;
388
+ default:
389
+ errors.push(`Unknown operation type: ${operation.type}`);
390
+ }
391
+
392
+ return { success: errors.length === 0, errors };
393
+ }
394
+
395
+ /**
396
+ * Apply operation to state
397
+ * @private
398
+ */
399
+ _applyOperation(operation) {
400
+ if (!this.state.capsules) {
401
+ this.state.capsules = {};
402
+ }
403
+
404
+ switch (operation.type) {
405
+ case 'add_capsule':
406
+ this.state.capsules[operation.data.id] = operation.data;
407
+ break;
408
+ case 'remove_capsule':
409
+ delete this.state.capsules[operation.data.capsule_id];
410
+ break;
411
+ case 'update_state':
412
+ this.state[operation.data.key] = operation.data.value;
413
+ break;
414
+ case 'merge':
415
+ this.state.last_merge = {
416
+ capsules: operation.data.capsules,
417
+ timestamp: new Date().toISOString(),
418
+ };
419
+ break;
420
+ default:
421
+ throw new Error(`Unknown operation type: ${operation.type}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Hash transaction for integrity verification
427
+ * @private
428
+ */
429
+ async _hashTransaction(tx) {
430
+ const data = JSON.stringify({
431
+ id: tx.id,
432
+ timestamp: tx.timestamp,
433
+ operations: tx.operations.map(op => ({
434
+ id: op.id,
435
+ type: op.type,
436
+ data: op.data,
437
+ })),
438
+ status: tx.status,
439
+ }, null, 0);
440
+
441
+ return await blake3(data);
442
+ }
443
+
444
+ /**
445
+ * Get current state
446
+ * @returns {any} Current state
447
+ */
448
+ getState() {
449
+ return { ...this.state };
450
+ }
451
+
452
+ /**
453
+ * Reset state (for testing)
454
+ */
455
+ reset() {
456
+ this.transactions.clear();
457
+ this.transactionHistory = [];
458
+ this.state = {};
459
+ }
460
+ }
461
+
462
+ // ============================================================================
463
+ // Exports
464
+ // ============================================================================
465
+
466
+ export default TransactionManager;