@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,614 @@
1
+ /**
2
+ * @file Enhanced Bounds - Soft limits, quotas, and rate limiting
3
+ * @module @unrdf/kgc-runtime/enhanced-bounds
4
+ *
5
+ * @description
6
+ * Advanced bounds enforcement with:
7
+ * - Soft limits with warning receipts at 80% threshold
8
+ * - Per-agent/per-user quotas
9
+ * - Sliding window rate limiting
10
+ * - Integration with existing receipt system
11
+ */
12
+
13
+ import { z } from 'zod';
14
+ import { randomUUID } from 'node:crypto';
15
+
16
+ // =============================================================================
17
+ // Schemas
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Enhanced bounds schema with soft limits
22
+ */
23
+ const EnhancedBoundsSchema = z.object({
24
+ // Hard limits (100%)
25
+ max_files_touched: z.number().int().positive().optional(),
26
+ max_bytes_changed: z.number().int().positive().optional(),
27
+ max_tool_ops: z.number().int().positive().optional(),
28
+ max_runtime_ms: z.number().int().positive().optional(),
29
+ max_graph_rewrites: z.number().int().positive().optional(),
30
+
31
+ // Warning thresholds (0.0-1.0 as fraction of limit)
32
+ warnings: z
33
+ .object({
34
+ filesThreshold: z.number().min(0).max(1).default(0.8),
35
+ bytesThreshold: z.number().min(0).max(1).default(0.9),
36
+ opsThreshold: z.number().min(0).max(1).default(0.75),
37
+ runtimeThreshold: z.number().min(0).max(1).default(0.9),
38
+ graphRewritesThreshold: z.number().min(0).max(1).default(0.8),
39
+ })
40
+ .optional(),
41
+ });
42
+
43
+ /**
44
+ * Per-agent quota schema
45
+ */
46
+ const AgentQuotaSchema = z.object({
47
+ agentId: z.string(),
48
+ max_files: z.number().int().positive().optional(),
49
+ max_bytes: z.number().int().positive().optional(),
50
+ max_ops: z.number().int().positive().optional(),
51
+ max_runtime_ms: z.number().int().positive().optional(),
52
+ });
53
+
54
+ /**
55
+ * Rate limit schema
56
+ */
57
+ const RateLimitSchema = z.object({
58
+ maxOpsPerSecond: z.number().positive(),
59
+ windowSizeMs: z.number().int().positive().default(1000),
60
+ });
61
+
62
+ // =============================================================================
63
+ // Enhanced Bounds Checker
64
+ // =============================================================================
65
+
66
+ /**
67
+ * Enhanced bounds checker with soft limits, quotas, and rate limiting
68
+ *
69
+ * @class
70
+ *
71
+ * @example
72
+ * const checker = new EnhancedBoundsChecker({
73
+ * max_files_touched: 100,
74
+ * max_bytes_changed: 10485760,
75
+ * warnings: { filesThreshold: 0.8, bytesThreshold: 0.9 }
76
+ * });
77
+ *
78
+ * // Set per-agent quota
79
+ * checker.setAgentQuota('agent:backend-dev', {
80
+ * max_files: 50,
81
+ * max_bytes: 5242880
82
+ * });
83
+ *
84
+ * // Enable rate limiting
85
+ * checker.setRateLimit({ maxOpsPerSecond: 10 });
86
+ *
87
+ * // Check operation
88
+ * const receipt = await checker.checkAndRecord({
89
+ * type: 'file_write',
90
+ * files: 1,
91
+ * bytes: 1024
92
+ * }, 'agent:backend-dev');
93
+ */
94
+ export class EnhancedBoundsChecker {
95
+ /**
96
+ * @param {Object} bounds - Bounds configuration
97
+ */
98
+ constructor(bounds = {}) {
99
+ this.bounds = EnhancedBoundsSchema.parse(bounds);
100
+
101
+ // Global usage tracking
102
+ this.usage = {
103
+ files_touched: 0,
104
+ bytes_changed: 0,
105
+ tool_ops: 0,
106
+ runtime_ms: 0,
107
+ graph_rewrites: 0,
108
+ };
109
+
110
+ // Per-agent usage tracking
111
+ /** @type {Map<string, Object>} */
112
+ this.agentUsage = new Map();
113
+
114
+ // Per-agent quotas
115
+ /** @type {Map<string, Object>} */
116
+ this.agentQuotas = new Map();
117
+
118
+ // Rate limiting state
119
+ /** @type {Map<string, Array<number>>} */
120
+ this.rateLimitWindows = new Map();
121
+
122
+ /** @type {Object|null} */
123
+ this.rateLimit = null;
124
+
125
+ // Receipt log
126
+ this.receipts = [];
127
+
128
+ // Warning thresholds (defaults)
129
+ this.warnings = this.bounds.warnings || {
130
+ filesThreshold: 0.8,
131
+ bytesThreshold: 0.9,
132
+ opsThreshold: 0.75,
133
+ runtimeThreshold: 0.9,
134
+ graphRewritesThreshold: 0.8,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Set per-agent quota
140
+ *
141
+ * @param {string} agentId - Agent identifier
142
+ * @param {Object} quota - Quota limits
143
+ */
144
+ setAgentQuota(agentId, quota) {
145
+ const validatedQuota = AgentQuotaSchema.parse({ agentId, ...quota });
146
+ this.agentQuotas.set(agentId, validatedQuota);
147
+
148
+ // Initialize usage if not exists
149
+ if (!this.agentUsage.has(agentId)) {
150
+ this.agentUsage.set(agentId, {
151
+ files_touched: 0,
152
+ bytes_changed: 0,
153
+ tool_ops: 0,
154
+ runtime_ms: 0,
155
+ });
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Set rate limit
161
+ *
162
+ * @param {Object} rateLimit - Rate limit configuration
163
+ */
164
+ setRateLimit(rateLimit) {
165
+ this.rateLimit = RateLimitSchema.parse(rateLimit);
166
+ }
167
+
168
+ /**
169
+ * Check if operation exceeds soft limit (warning threshold)
170
+ *
171
+ * @param {string} metric - Metric name (files, bytes, ops, runtime, rewrites)
172
+ * @param {number} current - Current usage
173
+ * @param {number} delta - Operation delta
174
+ * @param {number} limit - Hard limit
175
+ * @param {number} threshold - Warning threshold (0.0-1.0)
176
+ * @returns {Object|null} Warning receipt if threshold exceeded
177
+ * @private
178
+ */
179
+ _checkSoftLimit(metric, current, delta, limit, threshold) {
180
+ if (!limit || !threshold) {
181
+ return null;
182
+ }
183
+
184
+ const newUsage = current + delta;
185
+ const percentage = newUsage / limit;
186
+
187
+ // Error when exceeding limit (>100%)
188
+ if (newUsage > limit) {
189
+ return {
190
+ type: 'error',
191
+ metric,
192
+ current: newUsage,
193
+ limit,
194
+ percentage: Math.round(percentage * 100),
195
+ message: `Exceeded ${metric} limit: ${newUsage}/${limit}`,
196
+ };
197
+ }
198
+
199
+ // Warning at threshold (default 80%) but strictly below 100%
200
+ if (percentage >= threshold && percentage < 1.0) {
201
+ return {
202
+ type: 'warning',
203
+ metric,
204
+ current: newUsage,
205
+ limit,
206
+ percentage: Math.round(percentage * 100),
207
+ message: `Approaching ${metric} limit: ${Math.round(percentage * 100)}% used (${newUsage}/${limit})`,
208
+ };
209
+ }
210
+
211
+ return null;
212
+ }
213
+
214
+ /**
215
+ * Check rate limit for agent
216
+ *
217
+ * @param {string} agentId - Agent identifier
218
+ * @returns {Object|null} Error if rate limit exceeded
219
+ * @private
220
+ */
221
+ _checkRateLimit(agentId) {
222
+ if (!this.rateLimit) {
223
+ return null;
224
+ }
225
+
226
+ const now = Date.now();
227
+ const windowSize = this.rateLimit.windowSizeMs;
228
+ const maxOps = this.rateLimit.maxOpsPerSecond;
229
+
230
+ // Get or create window for agent
231
+ if (!this.rateLimitWindows.has(agentId)) {
232
+ this.rateLimitWindows.set(agentId, []);
233
+ }
234
+
235
+ const window = this.rateLimitWindows.get(agentId);
236
+
237
+ // Remove timestamps outside window
238
+ const cutoff = now - windowSize;
239
+ const activeOps = window.filter((timestamp) => timestamp > cutoff);
240
+ this.rateLimitWindows.set(agentId, activeOps);
241
+
242
+ // Check if rate limit exceeded
243
+ if (activeOps.length >= maxOps) {
244
+ return {
245
+ type: 'error',
246
+ metric: 'rate_limit',
247
+ current: activeOps.length,
248
+ limit: maxOps,
249
+ message: `Rate limit exceeded: ${activeOps.length} ops in ${windowSize}ms (max ${maxOps})`,
250
+ };
251
+ }
252
+
253
+ // Add current operation
254
+ activeOps.push(now);
255
+ this.rateLimitWindows.set(agentId, activeOps);
256
+
257
+ return null;
258
+ }
259
+
260
+ /**
261
+ * Check per-agent quota
262
+ *
263
+ * @param {string} agentId - Agent identifier
264
+ * @param {Object} operation - Operation to check
265
+ * @returns {Object|null} Error if quota exceeded
266
+ * @private
267
+ */
268
+ _checkAgentQuota(agentId, operation) {
269
+ const quota = this.agentQuotas.get(agentId);
270
+ if (!quota) {
271
+ return null; // No quota set for this agent
272
+ }
273
+
274
+ const usage = this.agentUsage.get(agentId) || {
275
+ files_touched: 0,
276
+ bytes_changed: 0,
277
+ tool_ops: 0,
278
+ runtime_ms: 0,
279
+ };
280
+
281
+ // Check files quota
282
+ if (quota.max_files && operation.files) {
283
+ if (usage.files_touched + operation.files > quota.max_files) {
284
+ return {
285
+ type: 'error',
286
+ metric: 'agent_quota_files',
287
+ agentId,
288
+ current: usage.files_touched + operation.files,
289
+ limit: quota.max_files,
290
+ message: `Agent ${agentId} exceeded files quota: ${usage.files_touched + operation.files}/${quota.max_files}`,
291
+ };
292
+ }
293
+ }
294
+
295
+ // Check bytes quota
296
+ if (quota.max_bytes && operation.bytes) {
297
+ if (usage.bytes_changed + operation.bytes > quota.max_bytes) {
298
+ return {
299
+ type: 'error',
300
+ metric: 'agent_quota_bytes',
301
+ agentId,
302
+ current: usage.bytes_changed + operation.bytes,
303
+ limit: quota.max_bytes,
304
+ message: `Agent ${agentId} exceeded bytes quota: ${usage.bytes_changed + operation.bytes}/${quota.max_bytes}`,
305
+ };
306
+ }
307
+ }
308
+
309
+ // Check ops quota
310
+ if (quota.max_ops && operation.ops) {
311
+ if (usage.tool_ops + operation.ops > quota.max_ops) {
312
+ return {
313
+ type: 'error',
314
+ metric: 'agent_quota_ops',
315
+ agentId,
316
+ current: usage.tool_ops + operation.ops,
317
+ limit: quota.max_ops,
318
+ message: `Agent ${agentId} exceeded ops quota: ${usage.tool_ops + operation.ops}/${quota.max_ops}`,
319
+ };
320
+ }
321
+ }
322
+
323
+ // Check runtime quota
324
+ if (quota.max_runtime_ms && operation.ms) {
325
+ if (usage.runtime_ms + operation.ms > quota.max_runtime_ms) {
326
+ return {
327
+ type: 'error',
328
+ metric: 'agent_quota_runtime',
329
+ agentId,
330
+ current: usage.runtime_ms + operation.ms,
331
+ limit: quota.max_runtime_ms,
332
+ message: `Agent ${agentId} exceeded runtime quota: ${usage.runtime_ms + operation.ms}/${quota.max_runtime_ms}`,
333
+ };
334
+ }
335
+ }
336
+
337
+ return null;
338
+ }
339
+
340
+ /**
341
+ * Check and record operation with comprehensive validation
342
+ *
343
+ * @param {Object} operation - Operation to check
344
+ * @param {string} [agentId] - Agent identifier
345
+ * @returns {Object} Receipt with admission decision and warnings
346
+ *
347
+ * @example
348
+ * const receipt = await checker.checkAndRecord({
349
+ * type: 'file_write',
350
+ * files: 1,
351
+ * bytes: 1024,
352
+ * ops: 1
353
+ * }, 'agent:backend-dev');
354
+ *
355
+ * // Receipt structure:
356
+ * // {
357
+ * // admit: true/false,
358
+ * // receipt_id: 'uuid',
359
+ * // timestamp: 123456789,
360
+ * // warnings: [...],
361
+ * // errors: [...]
362
+ * // }
363
+ */
364
+ async checkAndRecord(operation, agentId = 'system') {
365
+ const receipt = {
366
+ receipt_id: randomUUID(),
367
+ timestamp: Date.now(),
368
+ agentId,
369
+ operation: operation.type,
370
+ warnings: [],
371
+ errors: [],
372
+ admit: true,
373
+ };
374
+
375
+ // Check rate limit
376
+ const rateLimitError = this._checkRateLimit(agentId);
377
+ if (rateLimitError) {
378
+ receipt.errors.push(rateLimitError);
379
+ receipt.admit = false;
380
+ }
381
+
382
+ // Check agent quota
383
+ const quotaError = this._checkAgentQuota(agentId, operation);
384
+ if (quotaError) {
385
+ receipt.errors.push(quotaError);
386
+ receipt.admit = false;
387
+ }
388
+
389
+ // Check soft limits for each metric
390
+ if (operation.files !== undefined) {
391
+ const check = this._checkSoftLimit(
392
+ 'files',
393
+ this.usage.files_touched,
394
+ operation.files,
395
+ this.bounds.max_files_touched,
396
+ this.warnings.filesThreshold
397
+ );
398
+ if (check) {
399
+ if (check.type === 'warning') {
400
+ receipt.warnings.push(check);
401
+ } else {
402
+ receipt.errors.push(check);
403
+ receipt.admit = false;
404
+ }
405
+ }
406
+ }
407
+
408
+ if (operation.bytes !== undefined) {
409
+ const check = this._checkSoftLimit(
410
+ 'bytes',
411
+ this.usage.bytes_changed,
412
+ operation.bytes,
413
+ this.bounds.max_bytes_changed,
414
+ this.warnings.bytesThreshold
415
+ );
416
+ if (check) {
417
+ if (check.type === 'warning') {
418
+ receipt.warnings.push(check);
419
+ } else {
420
+ receipt.errors.push(check);
421
+ receipt.admit = false;
422
+ }
423
+ }
424
+ }
425
+
426
+ if (operation.ops !== undefined) {
427
+ const check = this._checkSoftLimit(
428
+ 'ops',
429
+ this.usage.tool_ops,
430
+ operation.ops,
431
+ this.bounds.max_tool_ops,
432
+ this.warnings.opsThreshold
433
+ );
434
+ if (check) {
435
+ if (check.type === 'warning') {
436
+ receipt.warnings.push(check);
437
+ } else {
438
+ receipt.errors.push(check);
439
+ receipt.admit = false;
440
+ }
441
+ }
442
+ }
443
+
444
+ if (operation.ms !== undefined) {
445
+ const check = this._checkSoftLimit(
446
+ 'runtime',
447
+ this.usage.runtime_ms,
448
+ operation.ms,
449
+ this.bounds.max_runtime_ms,
450
+ this.warnings.runtimeThreshold
451
+ );
452
+ if (check) {
453
+ if (check.type === 'warning') {
454
+ receipt.warnings.push(check);
455
+ } else {
456
+ receipt.errors.push(check);
457
+ receipt.admit = false;
458
+ }
459
+ }
460
+ }
461
+
462
+ if (operation.rewrites !== undefined) {
463
+ const check = this._checkSoftLimit(
464
+ 'rewrites',
465
+ this.usage.graph_rewrites,
466
+ operation.rewrites,
467
+ this.bounds.max_graph_rewrites,
468
+ this.warnings.graphRewritesThreshold
469
+ );
470
+ if (check) {
471
+ if (check.type === 'warning') {
472
+ receipt.warnings.push(check);
473
+ } else {
474
+ receipt.errors.push(check);
475
+ receipt.admit = false;
476
+ }
477
+ }
478
+ }
479
+
480
+ // If admitted, record usage
481
+ if (receipt.admit) {
482
+ this._recordUsage(operation, agentId);
483
+ }
484
+
485
+ // Store receipt
486
+ this.receipts.push(receipt);
487
+
488
+ return receipt;
489
+ }
490
+
491
+ /**
492
+ * Record operation usage
493
+ *
494
+ * @param {Object} operation - Operation to record
495
+ * @param {string} agentId - Agent identifier
496
+ * @private
497
+ */
498
+ _recordUsage(operation, agentId) {
499
+ // Update global usage
500
+ if (operation.files !== undefined) {
501
+ this.usage.files_touched += operation.files;
502
+ }
503
+ if (operation.bytes !== undefined) {
504
+ this.usage.bytes_changed += operation.bytes;
505
+ }
506
+ if (operation.ops !== undefined) {
507
+ this.usage.tool_ops += operation.ops;
508
+ }
509
+ if (operation.ms !== undefined) {
510
+ this.usage.runtime_ms += operation.ms;
511
+ }
512
+ if (operation.rewrites !== undefined) {
513
+ this.usage.graph_rewrites += operation.rewrites;
514
+ }
515
+
516
+ // Update agent usage
517
+ if (!this.agentUsage.has(agentId)) {
518
+ this.agentUsage.set(agentId, {
519
+ files_touched: 0,
520
+ bytes_changed: 0,
521
+ tool_ops: 0,
522
+ runtime_ms: 0,
523
+ });
524
+ }
525
+
526
+ const agentUsageData = this.agentUsage.get(agentId);
527
+ if (operation.files !== undefined) {
528
+ agentUsageData.files_touched += operation.files;
529
+ }
530
+ if (operation.bytes !== undefined) {
531
+ agentUsageData.bytes_changed += operation.bytes;
532
+ }
533
+ if (operation.ops !== undefined) {
534
+ agentUsageData.tool_ops += operation.ops;
535
+ }
536
+ if (operation.ms !== undefined) {
537
+ agentUsageData.runtime_ms += operation.ms;
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Get current usage
543
+ *
544
+ * @returns {Object} Global usage
545
+ */
546
+ getUsage() {
547
+ return { ...this.usage };
548
+ }
549
+
550
+ /**
551
+ * Get agent usage
552
+ *
553
+ * @param {string} agentId - Agent identifier
554
+ * @returns {Object|null} Agent usage or null
555
+ */
556
+ getAgentUsage(agentId) {
557
+ const usage = this.agentUsage.get(agentId);
558
+ return usage ? { ...usage } : null;
559
+ }
560
+
561
+ /**
562
+ * Get all receipts
563
+ *
564
+ * @returns {Array<Object>} Receipt history
565
+ */
566
+ getReceipts() {
567
+ return [...this.receipts];
568
+ }
569
+
570
+ /**
571
+ * Get receipts by type
572
+ *
573
+ * @param {string} type - Receipt type ('warning' or 'error')
574
+ * @returns {Array<Object>} Filtered receipts
575
+ */
576
+ getReceiptsByType(type) {
577
+ return this.receipts.filter((receipt) => {
578
+ if (type === 'warning') {
579
+ return receipt.warnings.length > 0;
580
+ }
581
+ if (type === 'error') {
582
+ return receipt.errors.length > 0;
583
+ }
584
+ return false;
585
+ });
586
+ }
587
+
588
+ /**
589
+ * Reset usage (for testing)
590
+ */
591
+ reset() {
592
+ this.usage = {
593
+ files_touched: 0,
594
+ bytes_changed: 0,
595
+ tool_ops: 0,
596
+ runtime_ms: 0,
597
+ graph_rewrites: 0,
598
+ };
599
+ this.agentUsage.clear();
600
+ this.rateLimitWindows.clear();
601
+ this.receipts = [];
602
+ }
603
+ }
604
+
605
+ // =============================================================================
606
+ // Module Exports
607
+ // =============================================================================
608
+
609
+ export default {
610
+ EnhancedBoundsChecker,
611
+ EnhancedBoundsSchema,
612
+ AgentQuotaSchema,
613
+ RateLimitSchema,
614
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Receipt-based operation executor
3
+ * Ensures all operations are deterministic and produce verifiable receipts
4
+ */
5
+
6
+ import { generateReceipt } from './receipt.mjs';
7
+
8
+ /**
9
+ * Execute an operation with receipt generation
10
+ * @template T
11
+ * @param {string} operation - Operation name
12
+ * @param {Record<string, any>} inputs - Operation inputs
13
+ * @param {() => Promise<T>} fn - Operation function
14
+ * @param {string} [parentHash] - Parent receipt hash
15
+ * @returns {Promise<{result: T, receipt: import('./receipt.mjs').Receipt}>}
16
+ */
17
+ export async function executeWithReceipt(operation, inputs, fn, parentHash) {
18
+ try {
19
+ const result = await fn();
20
+
21
+ const receipt = await generateReceipt(
22
+ operation,
23
+ inputs,
24
+ { result, success: true },
25
+ parentHash
26
+ );
27
+
28
+ return { result, receipt };
29
+ } catch (error) {
30
+ const receipt = await generateReceipt(
31
+ operation,
32
+ inputs,
33
+ {
34
+ error: error.message,
35
+ stack: error.stack,
36
+ success: false
37
+ },
38
+ parentHash
39
+ );
40
+
41
+ return {
42
+ result: null,
43
+ receipt,
44
+ error
45
+ };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Batch executor for multiple operations
51
+ * @param {Array<{operation: string, inputs: Record<string, any>, fn: () => Promise<any>}>} operations
52
+ * @returns {Promise<{results: any[], receipts: import('./receipt.mjs').Receipt[]}>}
53
+ */
54
+ export async function executeBatch(operations) {
55
+ const results = [];
56
+ const receipts = [];
57
+ let lastHash;
58
+
59
+ for (const op of operations) {
60
+ const { result, receipt } = await executeWithReceipt(
61
+ op.operation,
62
+ op.inputs,
63
+ op.fn,
64
+ lastHash
65
+ );
66
+
67
+ results.push(result);
68
+ receipts.push(receipt);
69
+ lastHash = receipt.hash;
70
+ }
71
+
72
+ return { results, receipts };
73
+ }