@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,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "KGC Plugin Registry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official registry for KGC Runtime plugins",
|
|
5
|
+
"updated": "2024-12-27",
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "example-receipt",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"description": "Example custom receipt plugin",
|
|
11
|
+
"author": "UNRDF Team",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"entryPoint": "./example-plugin.mjs",
|
|
14
|
+
"capabilities": [
|
|
15
|
+
"receipt:generate",
|
|
16
|
+
"receipt:validate",
|
|
17
|
+
"custom:example"
|
|
18
|
+
],
|
|
19
|
+
"api_version": "5.0.1",
|
|
20
|
+
"repository": "https://github.com/unrdf/kgc-plugins",
|
|
21
|
+
"tags": ["receipt", "example"],
|
|
22
|
+
"verified": true
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"categories": {
|
|
26
|
+
"receipt": "Custom receipt types and validation",
|
|
27
|
+
"schema": "Schema validation and transformation",
|
|
28
|
+
"tools": "Tool registry extensions",
|
|
29
|
+
"governance": "Governance policy plugins"
|
|
30
|
+
},
|
|
31
|
+
"metadata": {
|
|
32
|
+
"total_plugins": 1,
|
|
33
|
+
"last_updated": "2024-12-27T00:00:00Z"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGC 4D Admission Gate & Receipt Chain System
|
|
3
|
+
* Implements cryptographic receipts with BLAKE3 hashing and Merkle root computation
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Pure functions + Zod validation + BLAKE3 for deterministic hashing
|
|
6
|
+
* Receipts are immutable and form a tamper-evident chain
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { blake3 } from 'hash-wasm';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// SCHEMAS & TYPES
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bounds schema - resource limits for operations
|
|
18
|
+
*/
|
|
19
|
+
const BoundsSchema = z.object({
|
|
20
|
+
maxTriples: z.number().int().positive().optional(),
|
|
21
|
+
maxMemoryMB: z.number().positive().optional(),
|
|
22
|
+
maxTimeMs: z.number().positive().optional(),
|
|
23
|
+
maxQueries: z.number().int().positive().optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Delta schema - represents a change to be admitted
|
|
28
|
+
*/
|
|
29
|
+
const DeltaSchema = z.object({
|
|
30
|
+
operation: z.enum(['add', 'delete', 'query', 'update']),
|
|
31
|
+
triples: z.array(z.any()).optional(),
|
|
32
|
+
query: z.string().optional(),
|
|
33
|
+
metadata: z.any().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Predicate schema - validation rules for admission
|
|
38
|
+
*/
|
|
39
|
+
const PredicateSchema = z.object({
|
|
40
|
+
name: z.string(),
|
|
41
|
+
check: z.custom((val) => typeof val === 'function', {
|
|
42
|
+
message: 'check must be a function',
|
|
43
|
+
}),
|
|
44
|
+
description: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Receipt schema - immutable record of admission decision
|
|
49
|
+
*/
|
|
50
|
+
const ReceiptSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
timestamp_ns: z.bigint(),
|
|
53
|
+
operation: z.string(),
|
|
54
|
+
result: z.enum(['admit', 'deny']),
|
|
55
|
+
reason: z.string(),
|
|
56
|
+
bounds_used: z.any(),
|
|
57
|
+
parent_receipt_id: z.string().nullable(),
|
|
58
|
+
hash: z.string(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// ADMISSION GATE CLASS
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* AdmissionGate - Controls admission of operations with receipt generation
|
|
67
|
+
*
|
|
68
|
+
* Features:
|
|
69
|
+
* - BLAKE3 cryptographic hashing for receipts
|
|
70
|
+
* - Merkle tree receipt chaining
|
|
71
|
+
* - Bounds enforcement (resource limits)
|
|
72
|
+
* - Forbidden operation detection
|
|
73
|
+
* - Preserve(Q) invariant checking
|
|
74
|
+
* - Deterministic batch anchoring
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* import { AdmissionGate } from './admission-gate.mjs';
|
|
78
|
+
*
|
|
79
|
+
* const gate = new AdmissionGate();
|
|
80
|
+
* const delta = { operation: 'add', triples: [{ s: 'a', p: 'b', o: 'c' }] };
|
|
81
|
+
* const bounds = { maxTriples: 1000 };
|
|
82
|
+
* const predicates = [];
|
|
83
|
+
*
|
|
84
|
+
* const receipt = await gate.admit(delta, bounds, predicates);
|
|
85
|
+
* console.log(receipt.result); // 'admit' or 'deny'
|
|
86
|
+
*/
|
|
87
|
+
export class AdmissionGate {
|
|
88
|
+
/**
|
|
89
|
+
* @param {Object} options - Configuration options
|
|
90
|
+
* @param {string[]} options.forbiddenOperations - Operations that require receipts
|
|
91
|
+
* @param {Function} options.timeSource - Custom time source (for testing)
|
|
92
|
+
*/
|
|
93
|
+
constructor(options = {}) {
|
|
94
|
+
/** @type {Receipt[]} */
|
|
95
|
+
this.receiptChain = [];
|
|
96
|
+
|
|
97
|
+
/** @type {Set<string>} */
|
|
98
|
+
this.forbiddenOperations = new Set(options.forbiddenOperations || ['update', 'delete']);
|
|
99
|
+
|
|
100
|
+
/** @type {Function} */
|
|
101
|
+
this.timeSource = options.timeSource || (() => process.hrtime.bigint());
|
|
102
|
+
|
|
103
|
+
/** @type {Map<string, any>} */
|
|
104
|
+
this.preservedQueries = new Map();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Admit a delta (operation) with bounds and predicate checks
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} delta - The operation to admit
|
|
111
|
+
* @param {Object} bounds - Resource bounds/limits
|
|
112
|
+
* @param {Array<Object>} predicates - Validation predicates
|
|
113
|
+
* @returns {Promise<Object>} Receipt of admission decision
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const receipt = await gate.admit(
|
|
117
|
+
* { operation: 'add', triples: [{s: 'a', p: 'b', o: 'c'}] },
|
|
118
|
+
* { maxTriples: 1000 },
|
|
119
|
+
* []
|
|
120
|
+
* );
|
|
121
|
+
*/
|
|
122
|
+
async admit(delta, bounds = {}, predicates = []) {
|
|
123
|
+
// Validate inputs
|
|
124
|
+
const validatedDelta = DeltaSchema.parse(delta);
|
|
125
|
+
const validatedBounds = BoundsSchema.parse(bounds);
|
|
126
|
+
const validatedPredicates = z.array(PredicateSchema).parse(predicates);
|
|
127
|
+
|
|
128
|
+
// Check forbidden operations
|
|
129
|
+
if (this.forbiddenOperations.has(validatedDelta.operation)) {
|
|
130
|
+
const hasValidReceipt = this._hasValidReceiptForOperation(validatedDelta.operation);
|
|
131
|
+
if (!hasValidReceipt) {
|
|
132
|
+
return this._createReceipt(validatedDelta, 'deny', `Forbidden operation '${validatedDelta.operation}' without valid receipt`, validatedBounds);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check bounds
|
|
137
|
+
const boundsViolation = this._checkBounds(validatedDelta, validatedBounds);
|
|
138
|
+
if (boundsViolation) {
|
|
139
|
+
return this._createReceipt(validatedDelta, 'deny', boundsViolation, validatedBounds);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check predicates
|
|
143
|
+
for (const predicate of validatedPredicates) {
|
|
144
|
+
try {
|
|
145
|
+
const passed = predicate.check(validatedDelta);
|
|
146
|
+
if (!passed) {
|
|
147
|
+
return this._createReceipt(
|
|
148
|
+
validatedDelta,
|
|
149
|
+
'deny',
|
|
150
|
+
`Predicate '${predicate.name}' failed: ${predicate.description || 'validation failed'}`,
|
|
151
|
+
validatedBounds
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return this._createReceipt(validatedDelta, 'deny', `Predicate '${predicate.name}' threw error: ${error.message}`, validatedBounds);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check Preserve(Q) invariant for queries
|
|
160
|
+
if (validatedDelta.operation === 'query' && validatedDelta.query) {
|
|
161
|
+
const preserved = this._checkPreserveInvariant(validatedDelta.query);
|
|
162
|
+
if (!preserved) {
|
|
163
|
+
return this._createReceipt(validatedDelta, 'deny', `Preserve(Q) invariant violated for query`, validatedBounds);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// All checks passed - admit the operation
|
|
168
|
+
return this._createReceipt(validatedDelta, 'admit', 'All checks passed', validatedBounds);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if operation has valid receipt in chain
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
_hasValidReceiptForOperation(operation) {
|
|
176
|
+
// For now, we allow if there's ANY admitted operation in the chain
|
|
177
|
+
// In production, you'd check for specific authorization receipts
|
|
178
|
+
return this.receiptChain.some((receipt) => receipt.result === 'admit' && receipt.operation === operation);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check bounds violations
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_checkBounds(delta, bounds) {
|
|
186
|
+
if (bounds.maxTriples && delta.triples && delta.triples.length > bounds.maxTriples) {
|
|
187
|
+
return `Triple count ${delta.triples.length} exceeds bound ${bounds.maxTriples}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (bounds.maxQueries && delta.operation === 'query') {
|
|
191
|
+
const queryCount = this.receiptChain.filter((r) => r.operation === 'query').length;
|
|
192
|
+
if (queryCount >= bounds.maxQueries) {
|
|
193
|
+
return `Query count ${queryCount + 1} exceeds bound ${bounds.maxQueries}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Memory and time bounds would be checked during execution
|
|
198
|
+
// Here we just validate the structure
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check Preserve(Q) invariant - query results should remain unchanged
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_checkPreserveInvariant(query) {
|
|
207
|
+
if (!this.preservedQueries.has(query)) {
|
|
208
|
+
// First time seeing this query - preserve its result
|
|
209
|
+
this.preservedQueries.set(query, { timestamp: this.timeSource(), count: 1 });
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Query has been seen before - check if it's still valid
|
|
214
|
+
// In a real implementation, you'd re-execute and compare results
|
|
215
|
+
// For now, we just track that it's preserved
|
|
216
|
+
const preserved = this.preservedQueries.get(query);
|
|
217
|
+
preserved.count += 1;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a receipt with BLAKE3 hash and chain it
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
async _createReceipt(delta, result, reason, bounds) {
|
|
226
|
+
const timestamp = this.timeSource();
|
|
227
|
+
const parentId = this.receiptChain.length > 0 ? this.receiptChain[this.receiptChain.length - 1].id : null;
|
|
228
|
+
|
|
229
|
+
// Create receipt ID from timestamp + operation
|
|
230
|
+
const id = `receipt_${timestamp}_${delta.operation}`;
|
|
231
|
+
|
|
232
|
+
// Compute BLAKE3 hash of receipt content (excluding hash field itself)
|
|
233
|
+
const receiptContent = {
|
|
234
|
+
id,
|
|
235
|
+
timestamp_ns: timestamp.toString(),
|
|
236
|
+
operation: delta.operation,
|
|
237
|
+
result,
|
|
238
|
+
reason,
|
|
239
|
+
bounds_used: bounds,
|
|
240
|
+
parent_receipt_id: parentId,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const hash = await blake3(JSON.stringify(receiptContent));
|
|
244
|
+
|
|
245
|
+
const receipt = {
|
|
246
|
+
id,
|
|
247
|
+
timestamp_ns: timestamp,
|
|
248
|
+
operation: delta.operation,
|
|
249
|
+
result,
|
|
250
|
+
reason,
|
|
251
|
+
bounds_used: bounds,
|
|
252
|
+
parent_receipt_id: parentId,
|
|
253
|
+
hash,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Validate receipt
|
|
257
|
+
ReceiptSchema.parse(receipt);
|
|
258
|
+
|
|
259
|
+
// Add to chain
|
|
260
|
+
this.receiptChain.push(receipt);
|
|
261
|
+
|
|
262
|
+
return receipt;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Verify the integrity of a receipt chain using Merkle root computation
|
|
267
|
+
*
|
|
268
|
+
* @param {Array<Object>} receipts - Array of receipts to verify
|
|
269
|
+
* @returns {Promise<boolean>} True if chain is valid
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* const isValid = await gate.verifyChain(gate.receiptChain);
|
|
273
|
+
* console.assert(isValid === true, 'Chain should be valid');
|
|
274
|
+
*/
|
|
275
|
+
async verifyChain(receipts) {
|
|
276
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Verify each receipt's hash
|
|
281
|
+
for (const receipt of receipts) {
|
|
282
|
+
const { hash: storedHash, ...contentWithoutHash } = receipt;
|
|
283
|
+
|
|
284
|
+
// Convert BigInt to string for hashing
|
|
285
|
+
const hashableContent = {
|
|
286
|
+
...contentWithoutHash,
|
|
287
|
+
timestamp_ns: contentWithoutHash.timestamp_ns.toString(),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const computedHash = await blake3(JSON.stringify(hashableContent));
|
|
291
|
+
|
|
292
|
+
if (computedHash !== storedHash) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Verify chain linkage - each receipt should reference previous
|
|
298
|
+
for (let i = 1; i < receipts.length; i++) {
|
|
299
|
+
const current = receipts[i];
|
|
300
|
+
const previous = receipts[i - 1];
|
|
301
|
+
|
|
302
|
+
if (current.parent_receipt_id !== previous.id) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Verify monotonic timestamps
|
|
307
|
+
if (current.timestamp_ns <= previous.timestamp_ns) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Compute Merkle root of receipt chain
|
|
317
|
+
*
|
|
318
|
+
* @param {Array<Object>} receipts - Receipts to compute root for
|
|
319
|
+
* @returns {Promise<string>} Merkle root hash
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const root = await gate.computeMerkleRoot(gate.receiptChain);
|
|
323
|
+
* console.log('Merkle root:', root);
|
|
324
|
+
*/
|
|
325
|
+
async computeMerkleRoot(receipts) {
|
|
326
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
327
|
+
return await blake3('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (receipts.length === 1) {
|
|
331
|
+
return receipts[0].hash;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build Merkle tree bottom-up
|
|
335
|
+
let currentLevel = receipts.map((r) => r.hash);
|
|
336
|
+
|
|
337
|
+
while (currentLevel.length > 1) {
|
|
338
|
+
const nextLevel = [];
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
341
|
+
if (i + 1 < currentLevel.length) {
|
|
342
|
+
// Hash pair
|
|
343
|
+
const combined = currentLevel[i] + currentLevel[i + 1];
|
|
344
|
+
nextLevel.push(await blake3(combined));
|
|
345
|
+
} else {
|
|
346
|
+
// Odd one out - promote to next level
|
|
347
|
+
nextLevel.push(currentLevel[i]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
currentLevel = nextLevel;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return currentLevel[0];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Batch anchor multiple operations deterministically
|
|
359
|
+
*
|
|
360
|
+
* @param {Array<Object>} deltas - Operations to anchor
|
|
361
|
+
* @param {Object} bounds - Shared bounds for all operations
|
|
362
|
+
* @returns {Promise<Object>} Batch receipt with Merkle root
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* const batch = await gate.batchAnchor([delta1, delta2], { maxTriples: 1000 });
|
|
366
|
+
* console.log('Batch Merkle root:', batch.merkle_root);
|
|
367
|
+
*/
|
|
368
|
+
async batchAnchor(deltas, bounds = {}) {
|
|
369
|
+
if (!Array.isArray(deltas) || deltas.length === 0) {
|
|
370
|
+
throw new Error('batchAnchor requires non-empty array of deltas');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Sort deltas deterministically by operation type for consistency
|
|
374
|
+
const sortedDeltas = [...deltas].sort((a, b) => a.operation.localeCompare(b.operation));
|
|
375
|
+
|
|
376
|
+
// Admit all operations
|
|
377
|
+
const receipts = [];
|
|
378
|
+
for (const delta of sortedDeltas) {
|
|
379
|
+
const receipt = await this.admit(delta, bounds, []);
|
|
380
|
+
receipts.push(receipt);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Compute Merkle root
|
|
384
|
+
const merkleRoot = await this.computeMerkleRoot(receipts);
|
|
385
|
+
|
|
386
|
+
// Create batch receipt
|
|
387
|
+
const batchReceipt = {
|
|
388
|
+
type: 'batch',
|
|
389
|
+
timestamp_ns: this.timeSource(),
|
|
390
|
+
operation_count: receipts.length,
|
|
391
|
+
merkle_root: merkleRoot,
|
|
392
|
+
receipts,
|
|
393
|
+
all_admitted: receipts.every((r) => r.result === 'admit'),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return batchReceipt;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get current receipt chain
|
|
401
|
+
* @returns {Array<Object>} Current receipt chain
|
|
402
|
+
*/
|
|
403
|
+
getReceiptChain() {
|
|
404
|
+
return [...this.receiptChain];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Clear receipt chain (for testing)
|
|
409
|
+
*/
|
|
410
|
+
clearReceiptChain() {
|
|
411
|
+
this.receiptChain = [];
|
|
412
|
+
this.preservedQueries.clear();
|
|
413
|
+
}
|
|
414
|
+
}
|