@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,463 @@
1
+ /**
2
+ * @file Custom validators tests
3
+ */
4
+
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { blake3 } from 'hash-wasm';
8
+ import {
9
+ validateReceiptChainIntegrity,
10
+ ReceiptChainSchema,
11
+ validateTemporalConsistency,
12
+ TemporallyOrderedSchema,
13
+ validateArtifactHash,
14
+ ArtifactSchema,
15
+ validateDependencyDAG,
16
+ WorkItemDependencySchema,
17
+ detectCycle,
18
+ validateAsyncPolicy,
19
+ createAsyncPolicySchema,
20
+ validateTimeRange,
21
+ RunCapsuleTimeRangeSchema,
22
+ combineValidators,
23
+ createValidationResult,
24
+ } from '../src/validators.mjs';
25
+
26
+ // =============================================================================
27
+ // Receipt Chain Integrity Tests
28
+ // =============================================================================
29
+
30
+ describe('Receipt Chain Integrity Validator', () => {
31
+ it('should validate valid receipt chain with proper linkage', () => {
32
+ const receipts = [
33
+ {
34
+ id: 'receipt-1',
35
+ hash: 'abc123',
36
+ parentHash: null,
37
+ timestamp: 1000,
38
+ operation: 'add',
39
+ },
40
+ {
41
+ id: 'receipt-2',
42
+ hash: 'def456',
43
+ parentHash: 'abc123',
44
+ timestamp: 2000,
45
+ operation: 'update',
46
+ },
47
+ {
48
+ id: 'receipt-3',
49
+ hash: 'ghi789',
50
+ parentHash: 'def456',
51
+ timestamp: 3000,
52
+ operation: 'delete',
53
+ },
54
+ ];
55
+
56
+ const isValid = validateReceiptChainIntegrity(receipts);
57
+ assert.equal(isValid, true);
58
+ });
59
+
60
+ it('should reject chain with broken parentHash linkage', () => {
61
+ const receipts = [
62
+ {
63
+ id: 'receipt-1',
64
+ hash: 'abc123',
65
+ parentHash: null,
66
+ timestamp: 1000,
67
+ operation: 'add',
68
+ },
69
+ {
70
+ id: 'receipt-2',
71
+ hash: 'def456',
72
+ parentHash: 'wrong-hash', // Should be 'abc123'
73
+ timestamp: 2000,
74
+ operation: 'update',
75
+ },
76
+ ];
77
+
78
+ const isValid = validateReceiptChainIntegrity(receipts);
79
+ assert.equal(isValid, false);
80
+ });
81
+
82
+ it('should reject chain where first receipt has parent', () => {
83
+ const receipts = [
84
+ {
85
+ id: 'receipt-1',
86
+ hash: 'abc123',
87
+ parentHash: 'should-be-null', // First receipt should have null parent
88
+ timestamp: 1000,
89
+ operation: 'add',
90
+ },
91
+ ];
92
+
93
+ const isValid = validateReceiptChainIntegrity(receipts);
94
+ assert.equal(isValid, false);
95
+ });
96
+
97
+ it('should validate empty chain', () => {
98
+ const isValid = validateReceiptChainIntegrity([]);
99
+ assert.equal(isValid, true);
100
+ });
101
+
102
+ it('should validate chain using ReceiptChainSchema', async () => {
103
+ const receipts = [
104
+ {
105
+ id: 'receipt-1',
106
+ hash: 'abc123',
107
+ parentHash: null,
108
+ timestamp: 1000,
109
+ operation: 'add',
110
+ },
111
+ {
112
+ id: 'receipt-2',
113
+ hash: 'def456',
114
+ parentHash: 'abc123',
115
+ timestamp: 2000,
116
+ operation: 'update',
117
+ },
118
+ ];
119
+
120
+ const result = ReceiptChainSchema.parse(receipts);
121
+ assert.equal(result.length, 2);
122
+ });
123
+ });
124
+
125
+ // =============================================================================
126
+ // Temporal Consistency Tests
127
+ // =============================================================================
128
+
129
+ describe('Temporal Consistency Validator', () => {
130
+ it('should validate monotonically increasing timestamps', () => {
131
+ const items = [
132
+ { timestamp: 1000 },
133
+ { timestamp: 2000 },
134
+ { timestamp: 3000 },
135
+ { timestamp: 4000 },
136
+ ];
137
+
138
+ const isValid = validateTemporalConsistency(items);
139
+ assert.equal(isValid, true);
140
+ });
141
+
142
+ it('should reject non-increasing timestamps', () => {
143
+ const items = [
144
+ { timestamp: 1000 },
145
+ { timestamp: 2000 },
146
+ { timestamp: 1500 }, // Goes backwards
147
+ ];
148
+
149
+ const isValid = validateTemporalConsistency(items);
150
+ assert.equal(isValid, false);
151
+ });
152
+
153
+ it('should reject equal timestamps', () => {
154
+ const items = [
155
+ { timestamp: 1000 },
156
+ { timestamp: 2000 },
157
+ { timestamp: 2000 }, // Duplicate
158
+ ];
159
+
160
+ const isValid = validateTemporalConsistency(items);
161
+ assert.equal(isValid, false);
162
+ });
163
+
164
+ it('should validate single item', () => {
165
+ const items = [{ timestamp: 1000 }];
166
+
167
+ const isValid = validateTemporalConsistency(items);
168
+ assert.equal(isValid, true);
169
+ });
170
+
171
+ it('should validate using TemporallyOrderedSchema', () => {
172
+ const items = [
173
+ { timestamp: 1000, data: 'test1' },
174
+ { timestamp: 2000, data: 'test2' },
175
+ ];
176
+
177
+ const result = TemporallyOrderedSchema.parse(items);
178
+ assert.equal(result.length, 2);
179
+ });
180
+ });
181
+
182
+ // =============================================================================
183
+ // Artifact Hash Validation Tests
184
+ // =============================================================================
185
+
186
+ describe('Artifact Hash Validator', () => {
187
+ it('should validate artifact with correct hash', async () => {
188
+ const content = 'Hello World';
189
+ const hash = await blake3(content);
190
+
191
+ const artifact = {
192
+ type: 'file',
193
+ content,
194
+ hash,
195
+ };
196
+
197
+ const isValid = await validateArtifactHash(artifact);
198
+ assert.equal(isValid, true);
199
+ });
200
+
201
+ it('should reject artifact with incorrect hash', async () => {
202
+ const artifact = {
203
+ type: 'file',
204
+ content: 'Hello World',
205
+ hash: 'incorrect-hash',
206
+ };
207
+
208
+ const isValid = await validateArtifactHash(artifact);
209
+ assert.equal(isValid, false);
210
+ });
211
+
212
+ it('should validate artifact without content or hash', async () => {
213
+ const artifact = {
214
+ type: 'file',
215
+ path: '/test/file.txt',
216
+ };
217
+
218
+ const isValid = await validateArtifactHash(artifact);
219
+ assert.equal(isValid, true);
220
+ });
221
+
222
+ it('should validate using ArtifactSchema', async () => {
223
+ const content = 'Test Content';
224
+ const hash = await blake3(content);
225
+
226
+ const artifact = {
227
+ type: 'file',
228
+ path: '/test.txt',
229
+ content,
230
+ hash,
231
+ size: 12,
232
+ };
233
+
234
+ const result = await ArtifactSchema.parseAsync(artifact);
235
+ assert.equal(result.type, 'file');
236
+ });
237
+ });
238
+
239
+ // =============================================================================
240
+ // Dependency DAG Validation Tests
241
+ // =============================================================================
242
+
243
+ describe('Dependency DAG Validator', () => {
244
+ it('should validate acyclic dependency graph', () => {
245
+ const workItems = [
246
+ { id: 'A', dependencies: ['B', 'C'] },
247
+ { id: 'B', dependencies: ['D'] },
248
+ { id: 'C', dependencies: ['D'] },
249
+ { id: 'D', dependencies: [] },
250
+ ];
251
+
252
+ const result = validateDependencyDAG(workItems);
253
+ assert.equal(result.valid, true);
254
+ assert.equal(result.cycle.length, 0);
255
+ });
256
+
257
+ it('should detect simple cycle', () => {
258
+ const workItems = [
259
+ { id: 'A', dependencies: ['B'] },
260
+ { id: 'B', dependencies: ['C'] },
261
+ { id: 'C', dependencies: ['A'] }, // Cycle: A -> B -> C -> A
262
+ ];
263
+
264
+ const result = validateDependencyDAG(workItems);
265
+ assert.equal(result.valid, false);
266
+ assert.ok(result.cycle.length > 0);
267
+ });
268
+
269
+ it('should detect self-loop', () => {
270
+ const workItems = [
271
+ { id: 'A', dependencies: ['A'] }, // Self-loop
272
+ ];
273
+
274
+ const result = validateDependencyDAG(workItems);
275
+ assert.equal(result.valid, false);
276
+ });
277
+
278
+ it('should validate empty graph', () => {
279
+ const result = validateDependencyDAG([]);
280
+ assert.equal(result.valid, true);
281
+ });
282
+
283
+ it('should detect non-existent dependency', () => {
284
+ const workItems = [
285
+ { id: 'A', dependencies: ['B'] },
286
+ // 'B' does not exist
287
+ ];
288
+
289
+ const result = validateDependencyDAG(workItems);
290
+ assert.equal(result.valid, false);
291
+ assert.ok(result.error);
292
+ });
293
+
294
+ it('should validate using WorkItemDependencySchema', async () => {
295
+ const workItems = [
296
+ { id: 'A', dependencies: ['B'] },
297
+ { id: 'B', dependencies: [] },
298
+ ];
299
+
300
+ const result = WorkItemDependencySchema.parse(workItems);
301
+ assert.equal(result.length, 2);
302
+ });
303
+
304
+ it('should reject cycle using WorkItemDependencySchema', async () => {
305
+ const workItems = [
306
+ { id: 'A', dependencies: ['B'] },
307
+ { id: 'B', dependencies: ['A'] },
308
+ ];
309
+
310
+ assert.throws(() => {
311
+ WorkItemDependencySchema.parse(workItems);
312
+ }, /cycle detected/i);
313
+ });
314
+ });
315
+
316
+ describe('Cycle Detection', () => {
317
+ it('should detect cycle in graph', () => {
318
+ const graph = new Map([
319
+ ['A', ['B']],
320
+ ['B', ['C']],
321
+ ['C', ['A']],
322
+ ]);
323
+
324
+ const result = detectCycle(graph);
325
+ assert.equal(result.hasCycle, true);
326
+ assert.ok(result.cycle.length > 0);
327
+ });
328
+
329
+ it('should return no cycle for DAG', () => {
330
+ const graph = new Map([
331
+ ['A', ['B', 'C']],
332
+ ['B', ['D']],
333
+ ['C', ['D']],
334
+ ['D', []],
335
+ ]);
336
+
337
+ const result = detectCycle(graph);
338
+ assert.equal(result.hasCycle, false);
339
+ });
340
+ });
341
+
342
+ // =============================================================================
343
+ // Async Validator Tests
344
+ // =============================================================================
345
+
346
+ describe('Async Policy Validator', () => {
347
+ it('should validate operation allowed by policy', async () => {
348
+ const policyCheck = async (op) => op.type === 'safe_operation';
349
+
350
+ const operation = { type: 'safe_operation', data: 'test' };
351
+ const isAllowed = await validateAsyncPolicy(operation, policyCheck);
352
+
353
+ assert.equal(isAllowed, true);
354
+ });
355
+
356
+ it('should reject operation denied by policy', async () => {
357
+ const policyCheck = async (op) => op.type !== 'forbidden_operation';
358
+
359
+ const operation = { type: 'forbidden_operation', data: 'test' };
360
+ const isAllowed = await validateAsyncPolicy(operation, policyCheck);
361
+
362
+ assert.equal(isAllowed, false);
363
+ });
364
+
365
+ it('should handle async policy check errors', async () => {
366
+ const policyCheck = async () => {
367
+ throw new Error('Policy service unavailable');
368
+ };
369
+
370
+ const operation = { type: 'test', data: 'test' };
371
+ const isAllowed = await validateAsyncPolicy(operation, policyCheck);
372
+
373
+ assert.equal(isAllowed, false);
374
+ });
375
+
376
+ it('should validate using createAsyncPolicySchema', async () => {
377
+ const policyCheck = async (op) => op.type === 'allowed';
378
+
379
+ const schema = createAsyncPolicySchema(policyCheck);
380
+ const result = await schema.parseAsync({ type: 'allowed', data: 'test' });
381
+
382
+ assert.equal(result.type, 'allowed');
383
+ });
384
+
385
+ it('should reject using createAsyncPolicySchema', async () => {
386
+ const policyCheck = async (op) => op.type === 'allowed';
387
+
388
+ const schema = createAsyncPolicySchema(policyCheck);
389
+
390
+ await assert.rejects(async () => {
391
+ await schema.parseAsync({ type: 'denied', data: 'test' });
392
+ }, /denied by policy/i);
393
+ });
394
+ });
395
+
396
+ // =============================================================================
397
+ // Time Range Validation Tests
398
+ // =============================================================================
399
+
400
+ describe('Time Range Validator', () => {
401
+ it('should validate valid time range', () => {
402
+ const capsule = {
403
+ startTime: 1000,
404
+ endTime: 2000,
405
+ };
406
+
407
+ const isValid = validateTimeRange(capsule);
408
+ assert.equal(isValid, true);
409
+ });
410
+
411
+ it('should reject invalid time range', () => {
412
+ const capsule = {
413
+ startTime: 2000,
414
+ endTime: 1000, // End before start
415
+ };
416
+
417
+ const isValid = validateTimeRange(capsule);
418
+ assert.equal(isValid, false);
419
+ });
420
+
421
+ it('should validate using RunCapsuleTimeRangeSchema', () => {
422
+ const capsule = {
423
+ startTime: 1000,
424
+ endTime: 2000,
425
+ other: 'data',
426
+ };
427
+
428
+ const result = RunCapsuleTimeRangeSchema.parse(capsule);
429
+ assert.equal(result.startTime, 1000);
430
+ });
431
+ });
432
+
433
+ // =============================================================================
434
+ // Validator Utilities Tests
435
+ // =============================================================================
436
+
437
+ describe('Validator Utilities', () => {
438
+ it('should combine multiple validators', () => {
439
+ const validators = [
440
+ (data) => data.value > 0 || 'Value must be positive',
441
+ (data) => data.value < 100 || 'Value must be less than 100',
442
+ ];
443
+
444
+ const validator = combineValidators(validators);
445
+
446
+ const result1 = validator({ value: 50 });
447
+ assert.equal(result1.valid, true);
448
+ assert.equal(result1.errors.length, 0);
449
+
450
+ const result2 = validator({ value: -10 });
451
+ assert.equal(result2.valid, false);
452
+ assert.ok(result2.errors.length > 0);
453
+ });
454
+
455
+ it('should create validation result', () => {
456
+ const result = createValidationResult(true, 'Success', { detail: 'test' });
457
+
458
+ assert.equal(result.valid, true);
459
+ assert.equal(result.message, 'Success');
460
+ assert.equal(result.context.detail, 'test');
461
+ assert.ok(result.timestamp);
462
+ });
463
+ });