@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,341 @@
1
+ /**
2
+ * @file Tool Registry Tests
3
+ * @description Tests for ToolRegistry class functionality
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from 'vitest';
7
+ import { ToolRegistry, createRegistry } from '../src/tool-registry.mjs';
8
+ import { z } from 'zod';
9
+ import { join } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname } from 'path';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ describe('ToolRegistry', () => {
17
+ let registry;
18
+
19
+ beforeEach(() => {
20
+ registry = new ToolRegistry();
21
+ });
22
+
23
+ describe('Tool Registration', () => {
24
+ it('should register a tool with valid manifest', () => {
25
+ const manifest = {
26
+ name: 'TestTool',
27
+ version: '1.0.0',
28
+ description: 'Test tool',
29
+ schema_in: z.object({ input: z.string() }),
30
+ schema_out: z.object({ output: z.string() }),
31
+ capabilities: ['test'],
32
+ };
33
+
34
+ registry.registerTool(manifest);
35
+ const tool = registry.getTool('TestTool');
36
+
37
+ expect(tool).toBeDefined();
38
+ expect(tool.name).toBe('TestTool');
39
+ expect(tool.version).toBe('1.0.0');
40
+ });
41
+
42
+ it('should reject invalid manifest', () => {
43
+ const invalidManifest = {
44
+ name: 'Invalid',
45
+ // Missing required fields
46
+ };
47
+
48
+ expect(() => registry.registerTool(invalidManifest)).toThrow();
49
+ });
50
+
51
+ it('should store multiple versions of same tool', () => {
52
+ const v1 = {
53
+ name: 'Tool',
54
+ version: '1.0.0',
55
+ schema_in: z.object({}),
56
+ schema_out: z.object({}),
57
+ capabilities: [],
58
+ };
59
+
60
+ const v2 = {
61
+ name: 'Tool',
62
+ version: '2.0.0',
63
+ schema_in: z.object({}),
64
+ schema_out: z.object({}),
65
+ capabilities: [],
66
+ };
67
+
68
+ registry.registerTool(v1);
69
+ registry.registerTool(v2);
70
+
71
+ expect(registry.getToolVersion('Tool', '1.0.0')).toBeDefined();
72
+ expect(registry.getToolVersion('Tool', '2.0.0')).toBeDefined();
73
+ expect(registry.getTool('Tool').version).toBe('2.0.0'); // Latest
74
+ });
75
+ });
76
+
77
+ describe('Schema Conversion', () => {
78
+ it('should convert object schema definition to Zod', () => {
79
+ const manifest = {
80
+ name: 'Read',
81
+ version: '1.0.0',
82
+ schema_in: {
83
+ type: 'object',
84
+ properties: {
85
+ path: { type: 'string' },
86
+ encoding: { type: 'string' },
87
+ },
88
+ required: ['path'],
89
+ },
90
+ schema_out: {
91
+ type: 'object',
92
+ properties: {
93
+ content: { type: 'string' },
94
+ },
95
+ required: ['content'],
96
+ },
97
+ capabilities: ['file-read'],
98
+ };
99
+
100
+ registry.registerTool(manifest);
101
+ const tool = registry.getTool('Read');
102
+
103
+ // Test that schemas work
104
+ expect(tool.schema_in.parse({ path: '/test' })).toBeDefined();
105
+ expect(() => tool.schema_in.parse({})).toThrow(); // Missing required field
106
+ });
107
+
108
+ it('should handle nested object schemas', () => {
109
+ const manifest = {
110
+ name: 'Complex',
111
+ version: '1.0.0',
112
+ schema_in: {
113
+ type: 'object',
114
+ properties: {
115
+ config: {
116
+ type: 'object',
117
+ properties: {
118
+ timeout: { type: 'number' },
119
+ },
120
+ required: ['timeout'],
121
+ },
122
+ },
123
+ required: ['config'],
124
+ },
125
+ schema_out: {
126
+ type: 'object',
127
+ properties: {
128
+ result: { type: 'string' },
129
+ },
130
+ required: ['result'],
131
+ },
132
+ capabilities: [],
133
+ };
134
+
135
+ registry.registerTool(manifest);
136
+ const tool = registry.getTool('Complex');
137
+
138
+ expect(
139
+ tool.schema_in.parse({ config: { timeout: 5000 } }),
140
+ ).toBeDefined();
141
+ });
142
+
143
+ it('should handle array schemas', () => {
144
+ const manifest = {
145
+ name: 'ArrayTool',
146
+ version: '1.0.0',
147
+ schema_in: {
148
+ type: 'object',
149
+ properties: {},
150
+ required: [],
151
+ },
152
+ schema_out: {
153
+ type: 'object',
154
+ properties: {
155
+ items: {
156
+ type: 'array',
157
+ items: { type: 'string' },
158
+ },
159
+ },
160
+ required: ['items'],
161
+ },
162
+ capabilities: [],
163
+ };
164
+
165
+ registry.registerTool(manifest);
166
+ const tool = registry.getTool('ArrayTool');
167
+
168
+ expect(tool.schema_out.parse({ items: ['a', 'b'] })).toBeDefined();
169
+ expect(() => tool.schema_out.parse({ items: [1, 2] })).toThrow();
170
+ });
171
+ });
172
+
173
+ describe('Tool Queries', () => {
174
+ beforeEach(() => {
175
+ registry.registerTool({
176
+ name: 'Read',
177
+ version: '1.0.0',
178
+ schema_in: z.object({}),
179
+ schema_out: z.object({}),
180
+ capabilities: ['file-read', 'filesystem-access'],
181
+ });
182
+
183
+ registry.registerTool({
184
+ name: 'Write',
185
+ version: '1.0.0',
186
+ schema_in: z.object({}),
187
+ schema_out: z.object({}),
188
+ capabilities: ['file-write', 'filesystem-access'],
189
+ });
190
+
191
+ registry.registerTool({
192
+ name: 'Bash',
193
+ version: '1.0.0',
194
+ schema_in: z.object({}),
195
+ schema_out: z.object({}),
196
+ capabilities: ['command-execution'],
197
+ });
198
+ });
199
+
200
+ it('should get all tools', () => {
201
+ const tools = registry.getAllTools();
202
+ expect(tools).toHaveLength(3);
203
+ expect(tools.map((t) => t.name).sort()).toEqual([
204
+ 'Bash',
205
+ 'Read',
206
+ 'Write',
207
+ ]);
208
+ });
209
+
210
+ it('should query tools by capability', () => {
211
+ const fileTools = registry.getToolsByCapability('filesystem-access');
212
+ expect(fileTools).toHaveLength(2);
213
+ expect(fileTools.map((t) => t.name).sort()).toEqual([
214
+ 'Read',
215
+ 'Write',
216
+ ]);
217
+ });
218
+
219
+ it('should check if tool has capability', () => {
220
+ expect(registry.hasCapability('Read', 'file-read')).toBe(true);
221
+ expect(registry.hasCapability('Read', 'file-write')).toBe(false);
222
+ expect(registry.hasCapability('NonExistent', 'any')).toBe(false);
223
+ });
224
+ });
225
+
226
+ describe('Output Validation', () => {
227
+ beforeEach(() => {
228
+ registry.registerTool({
229
+ name: 'TestTool',
230
+ version: '1.0.0',
231
+ schema_in: z.object({}),
232
+ schema_out: z.object({
233
+ result: z.string(),
234
+ count: z.number(),
235
+ }),
236
+ capabilities: [],
237
+ });
238
+ });
239
+
240
+ it('should validate correct output', () => {
241
+ const output = { result: 'success', count: 42 };
242
+ expect(registry.validateOutput('TestTool', output)).toBe(true);
243
+ });
244
+
245
+ it('should reject invalid output', () => {
246
+ const output = { result: 'success' }; // Missing count
247
+ expect(registry.validateOutput('TestTool', output)).toBe(false);
248
+ });
249
+
250
+ it('should throw on non-existent tool', () => {
251
+ expect(() =>
252
+ registry.validateOutput('NonExistent', {}),
253
+ ).toThrow();
254
+ });
255
+ });
256
+
257
+ describe('Registry Statistics', () => {
258
+ beforeEach(() => {
259
+ registry.registerTool({
260
+ name: 'Read',
261
+ version: '1.0.0',
262
+ schema_in: z.object({}),
263
+ schema_out: z.object({}),
264
+ capabilities: ['file-read', 'filesystem-access'],
265
+ });
266
+
267
+ registry.registerTool({
268
+ name: 'Write',
269
+ version: '1.0.0',
270
+ schema_in: z.object({}),
271
+ schema_out: z.object({}),
272
+ capabilities: ['file-write', 'filesystem-access'],
273
+ });
274
+ });
275
+
276
+ it('should return registry statistics', () => {
277
+ const stats = registry.getStats();
278
+
279
+ expect(stats.total_tools).toBe(2);
280
+ expect(stats.unique_capabilities).toBe(3);
281
+ expect(stats.capabilities.sort()).toEqual([
282
+ 'file-read',
283
+ 'file-write',
284
+ 'filesystem-access',
285
+ ]);
286
+ });
287
+ });
288
+
289
+ describe('Loading from File', () => {
290
+ it('should load tools from registry file', () => {
291
+ const registryPath = join(
292
+ __dirname,
293
+ '../../../var/kgc/tool-registry.json',
294
+ );
295
+ const fileRegistry = new ToolRegistry({ registryPath });
296
+
297
+ const tools = fileRegistry.getAllTools();
298
+ expect(tools.length).toBeGreaterThan(0);
299
+
300
+ // Check built-in tools
301
+ const bashTool = fileRegistry.getTool('Bash');
302
+ expect(bashTool).toBeDefined();
303
+ expect(bashTool.name).toBe('Bash');
304
+ expect(bashTool.capabilities).toContain('command-execution');
305
+
306
+ const readTool = fileRegistry.getTool('Read');
307
+ expect(readTool).toBeDefined();
308
+ expect(readTool.capabilities).toContain('file-read');
309
+ });
310
+
311
+ it('should validate loaded schemas work correctly', () => {
312
+ const registryPath = join(
313
+ __dirname,
314
+ '../../../var/kgc/tool-registry.json',
315
+ );
316
+ const fileRegistry = new ToolRegistry({ registryPath });
317
+
318
+ const readTool = fileRegistry.getTool('Read');
319
+
320
+ // Should accept valid input
321
+ const validInput = readTool.schema_in.parse({ path: '/test.txt' });
322
+ expect(validInput.path).toBe('/test.txt');
323
+
324
+ // Should reject invalid input
325
+ expect(() => readTool.schema_in.parse({})).toThrow();
326
+ });
327
+ });
328
+
329
+ describe('Factory Function', () => {
330
+ it('should create registry with createRegistry', () => {
331
+ const registryPath = join(
332
+ __dirname,
333
+ '../../../var/kgc/tool-registry.json',
334
+ );
335
+ const reg = createRegistry(registryPath);
336
+
337
+ expect(reg).toBeInstanceOf(ToolRegistry);
338
+ expect(reg.getAllTools().length).toBeGreaterThan(0);
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @fileoverview Tests for Two-Phase Commit Transaction Manager
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { TransactionManager } from '../src/transaction.mjs';
7
+
8
+ describe('TransactionManager', () => {
9
+ let txManager;
10
+
11
+ beforeEach(() => {
12
+ txManager = new TransactionManager();
13
+ });
14
+
15
+ it('should execute two-phase commit successfully', async () => {
16
+ // Begin transaction
17
+ const tx = txManager.begin([
18
+ {
19
+ id: 'op1',
20
+ type: 'add_capsule',
21
+ data: { id: 'capsule1', content: 'test data' },
22
+ },
23
+ {
24
+ id: 'op2',
25
+ type: 'add_capsule',
26
+ data: { id: 'capsule2', content: 'more test data' },
27
+ },
28
+ ]);
29
+
30
+ expect(tx.status).toBe('pending');
31
+ expect(tx.operations).toHaveLength(2);
32
+
33
+ // Phase 1: Prepare
34
+ const prepareResult = await txManager.prepare(tx.id);
35
+
36
+ expect(prepareResult.success).toBe(true);
37
+ expect(prepareResult.errors).toHaveLength(0);
38
+ expect(prepareResult.undoOps).toHaveLength(2);
39
+
40
+ const updatedTx = txManager.getTransaction(tx.id);
41
+ expect(updatedTx.status).toBe('prepared');
42
+
43
+ // Phase 2: Commit
44
+ const commitResult = await txManager.commit(tx.id);
45
+
46
+ expect(commitResult.success).toBe(true);
47
+ expect(commitResult.receipts).toHaveLength(2);
48
+ expect(commitResult.errors).toHaveLength(0);
49
+
50
+ const finalTx = txManager.getTransaction(tx.id);
51
+ expect(finalTx.status).toBe('committed');
52
+
53
+ // Verify state updated
54
+ const state = txManager.getState();
55
+ expect(state.capsules).toBeDefined();
56
+ expect(state.capsules['capsule1']).toBeDefined();
57
+ expect(state.capsules['capsule2']).toBeDefined();
58
+ });
59
+
60
+ it('should rollback transaction on commit failure', async () => {
61
+ // Add a capsule first
62
+ const initialTx = txManager.begin([
63
+ {
64
+ id: 'initial',
65
+ type: 'add_capsule',
66
+ data: { id: 'existing', content: 'existing data' },
67
+ },
68
+ ]);
69
+
70
+ await txManager.prepare(initialTx.id);
71
+ await txManager.commit(initialTx.id);
72
+
73
+ // Create transaction that will fail during commit
74
+ const tx = txManager.begin([
75
+ {
76
+ id: 'op1',
77
+ type: 'add_capsule',
78
+ data: { id: 'new_capsule', content: 'new data' },
79
+ },
80
+ {
81
+ id: 'op2',
82
+ type: 'invalid_type', // This will cause failure
83
+ data: {},
84
+ },
85
+ ]);
86
+
87
+ const prepareResult = await txManager.prepare(tx.id);
88
+ expect(prepareResult.success).toBe(false);
89
+
90
+ const finalTx = txManager.getTransaction(tx.id);
91
+ expect(finalTx.status).toBe('aborted');
92
+
93
+ // Verify state not changed
94
+ const state = txManager.getState();
95
+ expect(state.capsules['new_capsule']).toBeUndefined();
96
+ expect(state.capsules['existing']).toBeDefined(); // Original still there
97
+ });
98
+
99
+ it('should support cascading transactions with parent hash', async () => {
100
+ // First transaction
101
+ const tx1 = txManager.begin([
102
+ {
103
+ id: 'op1',
104
+ type: 'add_capsule',
105
+ data: { id: 'capsule1', content: 'first' },
106
+ },
107
+ ]);
108
+
109
+ await txManager.prepare(tx1.id);
110
+ const commit1 = await txManager.commit(tx1.id);
111
+
112
+ expect(commit1.success).toBe(true);
113
+ expect(commit1.receipts).toHaveLength(1);
114
+
115
+ const receipt1 = commit1.receipts[0];
116
+ const parentHash = receipt1.hash;
117
+
118
+ // Second transaction with parent hash
119
+ const tx2 = txManager.begin(
120
+ [
121
+ {
122
+ id: 'op2',
123
+ type: 'add_capsule',
124
+ data: { id: 'capsule2', content: 'second' },
125
+ },
126
+ ],
127
+ parentHash
128
+ );
129
+
130
+ expect(tx2.parentHash).toBe(parentHash);
131
+
132
+ await txManager.prepare(tx2.id);
133
+ await txManager.commit(tx2.id);
134
+
135
+ // Verify both transactions committed
136
+ const finalTx1 = txManager.getTransaction(tx1.id);
137
+ const finalTx2 = txManager.getTransaction(tx2.id);
138
+
139
+ expect(finalTx1.status).toBe('committed');
140
+ expect(finalTx2.status).toBe('committed');
141
+ expect(finalTx2.parentHash).toBe(receipt1.hash);
142
+ });
143
+
144
+ it('should handle rollback with undo operations', async () => {
145
+ // Add initial capsule
146
+ const tx1 = txManager.begin([
147
+ {
148
+ id: 'op1',
149
+ type: 'add_capsule',
150
+ data: { id: 'capsule1', content: 'original' },
151
+ },
152
+ ]);
153
+
154
+ await txManager.prepare(tx1.id);
155
+ await txManager.commit(tx1.id);
156
+
157
+ // Verify capsule added
158
+ let state = txManager.getState();
159
+ expect(state.capsules).toBeDefined();
160
+ expect(state.capsules['capsule1']).toBeDefined();
161
+
162
+ // Create transaction to remove it
163
+ const tx2 = txManager.begin([
164
+ {
165
+ id: 'op2',
166
+ type: 'remove_capsule',
167
+ data: { capsule_id: 'capsule1' },
168
+ },
169
+ ]);
170
+
171
+ await txManager.prepare(tx2.id);
172
+ await txManager.commit(tx2.id);
173
+
174
+ // Verify capsule removed
175
+ state = txManager.getState();
176
+ expect(state.capsules['capsule1']).toBeUndefined();
177
+
178
+ // Rollback the removal
179
+ const rollbackResult = await txManager.rollback(tx2.id);
180
+
181
+ expect(rollbackResult.success).toBe(true);
182
+ expect(rollbackResult.undone).toBe(1);
183
+
184
+ // Verify capsule restored
185
+ state = txManager.getState();
186
+ expect(state.capsules['capsule1']).toBeDefined();
187
+ expect(state.capsules['capsule1'].content).toBe('original');
188
+ });
189
+ });