@unrdf/kgc-substrate 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/src/Router.mjs ADDED
@@ -0,0 +1,382 @@
1
+ /**
2
+ * @fileoverview Task Graph Router & Constraint Routing
3
+ *
4
+ * Deterministic routing of WorkItems to Agents based on predicates.
5
+ * Supports AND, OR, NOT operators with bounded evaluation cost.
6
+ *
7
+ * @module @unrdf/kgc-substrate/router
8
+ */
9
+
10
+ import { z } from 'zod';
11
+
12
+ /**
13
+ * Predicate schema for constraint validation
14
+ */
15
+ const PredicateSchema = z.object({
16
+ field: z.string(),
17
+ operator: z.enum(['==', 'in', 'not', '!=', '>', '<', '>=', '<=']),
18
+ value: z.union([z.string(), z.number(), z.boolean(), z.array(z.any())]),
19
+ });
20
+
21
+ /**
22
+ * Agent capability schema
23
+ */
24
+ const AgentSchema = z.object({
25
+ id: z.string(),
26
+ capabilities: z.record(z.any()),
27
+ });
28
+
29
+ /**
30
+ * WorkItem schema with routing predicates
31
+ */
32
+ const WorkItemSchema = z.object({
33
+ id: z.string(),
34
+ predicates: z.string(), // Constraint string like "requires_auth==true AND language=='js'"
35
+ });
36
+
37
+ /**
38
+ * Maximum number of predicates to prevent unbounded evaluation
39
+ * @constant {number}
40
+ */
41
+ const MAX_PREDICATES = 1000;
42
+
43
+ /**
44
+ * Parse constraint string into structured predicates
45
+ *
46
+ * @param {string} constraintString - Constraint expression (e.g., "requires_auth==true AND language=='js'")
47
+ * @returns {{field: string, operator: string, value: any}[]} Parsed predicates
48
+ * @throws {Error} If constraint string exceeds MAX_PREDICATES or has invalid syntax
49
+ *
50
+ * @example
51
+ * parsePredicates("language=='js'")
52
+ * // => [{ field: 'language', operator: '==', value: 'js' }]
53
+ *
54
+ * @example
55
+ * parsePredicates("requires_auth==true AND language in ['js','ts']")
56
+ * // => [
57
+ * // { field: 'requires_auth', operator: '==', value: true },
58
+ * // { field: 'language', operator: 'in', value: ['js', 'ts'] }
59
+ * // ]
60
+ */
61
+ export function parsePredicates(constraintString) {
62
+ if (typeof constraintString !== 'string') {
63
+ throw new Error('Constraint must be a string');
64
+ }
65
+
66
+ // Guard against unbounded evaluation
67
+ const tokenCount = constraintString.split(/\s+/).length;
68
+ if (tokenCount > MAX_PREDICATES * 5) { // Approximate: each predicate ~5 tokens
69
+ throw new Error(`Constraint exceeds maximum complexity (${MAX_PREDICATES} predicates)`);
70
+ }
71
+
72
+ // Normalize whitespace
73
+ let normalized = constraintString.trim();
74
+
75
+ // Handle empty constraint
76
+ if (!normalized) {
77
+ return [];
78
+ }
79
+
80
+ // Split by logical operators (AND, OR) - case insensitive
81
+ const logicalSplit = normalized.split(/\s+(AND|OR)\s+/i);
82
+
83
+ const predicates = [];
84
+ let predicateCount = 0;
85
+
86
+ for (let i = 0; i < logicalSplit.length; i += 2) { // Skip operators at odd indices
87
+ const clause = logicalSplit[i].trim();
88
+
89
+ if (!clause || clause === 'AND' || clause === 'OR') {
90
+ continue;
91
+ }
92
+
93
+ // Guard: enforce predicate limit
94
+ if (++predicateCount > MAX_PREDICATES) {
95
+ throw new Error(`Exceeded maximum of ${MAX_PREDICATES} predicates`);
96
+ }
97
+
98
+ // Handle NOT prefix
99
+ let isNegated = false;
100
+ let cleanClause = clause;
101
+ if (clause.match(/^NOT\s+/i)) {
102
+ isNegated = true;
103
+ cleanClause = clause.replace(/^NOT\s+/i, '');
104
+ }
105
+
106
+ // Parse operators: ==, !=, in, not, >, <, >=, <=
107
+ let match = cleanClause.match(/^(\w+)\s*(==|!=|in|not|>=?|<=?)\s*(.+)$/i);
108
+
109
+ if (!match) {
110
+ throw new Error(`Invalid predicate syntax: ${clause}`);
111
+ }
112
+
113
+ const [, field, operator, rawValue] = match;
114
+
115
+ // Parse value (handle strings, numbers, booleans, arrays)
116
+ let value;
117
+ const trimmedValue = rawValue.trim();
118
+
119
+ if (operator.toLowerCase() === 'in' || operator.toLowerCase() === 'not') {
120
+ // Parse array: ['js', 'ts'] or ["js", "ts"]
121
+ const arrayMatch = trimmedValue.match(/^\[(.+)\]$/);
122
+ if (arrayMatch) {
123
+ value = arrayMatch[1].split(',').map(v => {
124
+ const trimmed = v.trim().replace(/^['"]|['"]$/g, '');
125
+ return parseValue(trimmed);
126
+ });
127
+ } else {
128
+ throw new Error(`Operator '${operator}' requires array value: ${trimmedValue}`);
129
+ }
130
+ } else {
131
+ // Parse single value
132
+ value = parseValue(trimmedValue.replace(/^['"]|['"]$/g, ''));
133
+ }
134
+
135
+ // Normalize 'not' operator to '!=' or 'not in'
136
+ let normalizedOp = operator;
137
+ if (isNegated) {
138
+ if (operator === '==') normalizedOp = '!=';
139
+ else if (operator === 'in') normalizedOp = 'not';
140
+ } else if (operator.toLowerCase() === 'not') {
141
+ normalizedOp = 'not';
142
+ }
143
+
144
+ predicates.push({
145
+ field: field.trim(),
146
+ operator: normalizedOp.toLowerCase() === 'not' ? 'not' : normalizedOp,
147
+ value,
148
+ });
149
+ }
150
+
151
+ // Validate predicates with schema
152
+ predicates.forEach((pred, idx) => {
153
+ try {
154
+ PredicateSchema.parse(pred);
155
+ } catch (err) {
156
+ throw new Error(`Predicate ${idx} validation failed: ${err.message}`);
157
+ }
158
+ });
159
+
160
+ return predicates;
161
+ }
162
+
163
+ /**
164
+ * Parse string value to appropriate type
165
+ * @private
166
+ */
167
+ function parseValue(str) {
168
+ // Boolean
169
+ if (str === 'true') return true;
170
+ if (str === 'false') return false;
171
+
172
+ // Number
173
+ if (/^-?\d+(\.\d+)?$/.test(str)) {
174
+ return parseFloat(str);
175
+ }
176
+
177
+ // String
178
+ return str;
179
+ }
180
+
181
+ /**
182
+ * Evaluate single predicate against agent capabilities
183
+ *
184
+ * @private
185
+ * @param {{field: string, operator: string, value: any}} predicate - Predicate to evaluate
186
+ * @param {Object} capabilities - Agent capabilities object
187
+ * @returns {boolean} Whether predicate matches
188
+ */
189
+ function evaluatePredicate(predicate, capabilities) {
190
+ const { field, operator, value } = predicate;
191
+ const agentValue = capabilities[field];
192
+
193
+ switch (operator) {
194
+ case '==':
195
+ return agentValue === value;
196
+
197
+ case '!=':
198
+ return agentValue !== value;
199
+
200
+ case 'in':
201
+ return Array.isArray(value) && value.includes(agentValue);
202
+
203
+ case 'not':
204
+ return Array.isArray(value) && !value.includes(agentValue);
205
+
206
+ case '>':
207
+ return typeof agentValue === 'number' && agentValue > value;
208
+
209
+ case '<':
210
+ return typeof agentValue === 'number' && agentValue < value;
211
+
212
+ case '>=':
213
+ return typeof agentValue === 'number' && agentValue >= value;
214
+
215
+ case '<=':
216
+ return typeof agentValue === 'number' && agentValue <= value;
217
+
218
+ default:
219
+ return false;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Evaluate logical expression with AND/OR operators
225
+ *
226
+ * @private
227
+ * @param {string} constraintString - Original constraint string
228
+ * @param {{field: string, operator: string, value: any}[]} predicates - Parsed predicates
229
+ * @param {Object} capabilities - Agent capabilities
230
+ * @returns {boolean} Whether expression evaluates to true
231
+ */
232
+ function evaluateExpression(constraintString, predicates, capabilities) {
233
+ if (predicates.length === 0) {
234
+ return true; // Empty constraint matches all
235
+ }
236
+
237
+ if (predicates.length === 1) {
238
+ return evaluatePredicate(predicates[0], capabilities);
239
+ }
240
+
241
+ // Determine logical operators
242
+ const hasAND = /\sAND\s/i.test(constraintString);
243
+ const hasOR = /\sOR\s/i.test(constraintString);
244
+
245
+ // Pure AND: all predicates must match
246
+ if (hasAND && !hasOR) {
247
+ return predicates.every(pred => evaluatePredicate(pred, capabilities));
248
+ }
249
+
250
+ // Pure OR: any predicate must match
251
+ if (hasOR && !hasAND) {
252
+ return predicates.some(pred => evaluatePredicate(pred, capabilities));
253
+ }
254
+
255
+ // Mixed AND/OR: evaluate left-to-right with precedence (AND before OR)
256
+ // For simplicity, treat as OR (any clause matches)
257
+ // For complex expressions, parse into AST (future enhancement)
258
+ if (hasAND && hasOR) {
259
+ // Split by OR, then check if any OR clause (with ANDs) matches
260
+ const orClauses = constraintString.split(/\sOR\s/i);
261
+ return orClauses.some(orClause => {
262
+ const andPredicates = parsePredicates(orClause);
263
+ return andPredicates.every(pred => evaluatePredicate(pred, capabilities));
264
+ });
265
+ }
266
+
267
+ // Default: all predicates must match (AND semantics)
268
+ return predicates.every(pred => evaluatePredicate(pred, capabilities));
269
+ }
270
+
271
+ /**
272
+ * Route WorkItem to matching Agent based on predicates
273
+ *
274
+ * Deterministic: same input → same output (no randomization)
275
+ * Pure function: no side effects, no mutations
276
+ *
277
+ * @param {Object} workItem - WorkItem with predicates
278
+ * @param {string} workItem.id - Unique identifier
279
+ * @param {string} workItem.predicates - Constraint expression
280
+ * @param {Array<{id: string, capabilities: Object}>} agents - Available agents
281
+ * @returns {string|null} Agent ID if match found, null otherwise
282
+ * @throws {Error} If input validation fails or evaluation exceeds bounds
283
+ *
284
+ * @example
285
+ * const workItem = { id: 'task-1', predicates: "language=='js'" };
286
+ * const agents = [
287
+ * { id: 'agent-1', capabilities: { language: 'js' } },
288
+ * { id: 'agent-2', capabilities: { language: 'py' } }
289
+ * ];
290
+ * routeTask(workItem, agents); // => 'agent-1'
291
+ *
292
+ * @example XOR routing
293
+ * const workItem = { id: 'task-2', predicates: "env=='dev' AND NOT env=='prod'" };
294
+ * const agents = [{ id: 'dev-agent', capabilities: { env: 'dev' } }];
295
+ * routeTask(workItem, agents); // => 'dev-agent'
296
+ */
297
+ export function routeTask(workItem, agents) {
298
+ // Validate inputs
299
+ if (!workItem || typeof workItem.id !== 'string' || typeof workItem.predicates !== 'string') {
300
+ throw new Error('Invalid workItem: must have id and predicates fields');
301
+ }
302
+ if (!Array.isArray(agents)) {
303
+ throw new Error('Invalid agents: must be an array');
304
+ }
305
+ for (const agent of agents) {
306
+ if (!agent || typeof agent.id !== 'string' || typeof agent.capabilities !== 'object') {
307
+ throw new Error('Invalid agent: must have id and capabilities fields');
308
+ }
309
+ }
310
+
311
+ // Parse predicates
312
+ const predicates = parsePredicates(workItem.predicates);
313
+
314
+ // Find first matching agent (deterministic: returns first match)
315
+ for (const agent of agents) {
316
+ const matches = evaluateExpression(workItem.predicates, predicates, agent.capabilities);
317
+
318
+ if (matches) {
319
+ return agent.id;
320
+ }
321
+ }
322
+
323
+ // No match found
324
+ return null;
325
+ }
326
+
327
+ /**
328
+ * Validate routing constraints without executing
329
+ *
330
+ * @param {string} constraintString - Constraint expression to validate
331
+ * @returns {{valid: boolean, predicateCount: number, error?: string}}
332
+ *
333
+ * @example
334
+ * validateConstraints("language=='js' AND requires_auth==true")
335
+ * // => { valid: true, predicateCount: 2 }
336
+ */
337
+ export function validateConstraints(constraintString) {
338
+ try {
339
+ const predicates = parsePredicates(constraintString);
340
+ return {
341
+ valid: true,
342
+ predicateCount: predicates.length,
343
+ };
344
+ } catch (err) {
345
+ return {
346
+ valid: false,
347
+ predicateCount: 0,
348
+ error: err.message,
349
+ };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Get routing statistics for diagnostics
355
+ *
356
+ * @param {Array<Object>} workItems - Work items to analyze
357
+ * @param {Array<Object>} agents - Available agents
358
+ * @returns {{totalItems: number, routed: number, unrouted: number, routingMap: Object}}
359
+ */
360
+ export function getRoutingStats(workItems, agents) {
361
+ const routingMap = {};
362
+ let routed = 0;
363
+ let unrouted = 0;
364
+
365
+ for (const item of workItems) {
366
+ const agentId = routeTask(item, agents);
367
+
368
+ if (agentId) {
369
+ routed++;
370
+ routingMap[agentId] = (routingMap[agentId] || 0) + 1;
371
+ } else {
372
+ unrouted++;
373
+ }
374
+ }
375
+
376
+ return {
377
+ totalItems: workItems.length,
378
+ routed,
379
+ unrouted,
380
+ routingMap,
381
+ };
382
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * TamperDetector - Cryptographic verification and tamper detection for receipt chains
3
+ *
4
+ * Provides comprehensive verification of receipt chains including:
5
+ * - Hash chain integrity (merkle tree verification)
6
+ * - Timestamp monotonicity
7
+ * - Block ordering
8
+ * - Content hash verification
9
+ * - Genesis hash validation
10
+ *
11
+ * @module TamperDetector
12
+ */
13
+
14
+ import { sha256 } from 'hash-wasm';
15
+ import { ReceiptChain } from './ReceiptChain.mjs';
16
+
17
+ /**
18
+ * Verification result schema
19
+ * @typedef {Object} VerificationResult
20
+ * @property {boolean} valid - Overall validity
21
+ * @property {Array<string>} errors - Array of error descriptions
22
+ * @property {Object} [details] - Additional verification details
23
+ */
24
+
25
+ /**
26
+ * TamperDetector class - Cryptographic verification and tamper detection
27
+ */
28
+ export class TamperDetector {
29
+ /**
30
+ * Verify a receipt chain for tampering
31
+ *
32
+ * Performs comprehensive validation:
33
+ * 1. Genesis hash check
34
+ * 2. Block count validation
35
+ * 3. Hash chain integrity
36
+ * 4. Content hash verification
37
+ * 5. Timestamp monotonicity
38
+ * 6. Block ordering
39
+ *
40
+ * @param {ReceiptChain|Object} chainOrJSON - Chain to verify (ReceiptChain instance or JSON)
41
+ * @returns {Promise<VerificationResult>} Verification result
42
+ *
43
+ * @example
44
+ * const detector = new TamperDetector();
45
+ * const result = await detector.verify(chain);
46
+ * if (!result.valid) {
47
+ * console.error('Tampering detected:', result.errors);
48
+ * }
49
+ */
50
+ async verify(chainOrJSON) {
51
+ const errors = [];
52
+ const details = {
53
+ blocks_checked: 0,
54
+ genesis_hash: null,
55
+ chain_length: 0,
56
+ timestamp_checks: 0
57
+ };
58
+
59
+ try {
60
+ // Accept either ReceiptChain or JSON
61
+ let chain;
62
+ if (chainOrJSON instanceof ReceiptChain) {
63
+ chain = chainOrJSON;
64
+ } else if (typeof chainOrJSON === 'object' && chainOrJSON.blocks) {
65
+ // Assume it's JSON serialized chain
66
+ chain = ReceiptChain.fromJSON(chainOrJSON);
67
+ } else {
68
+ throw new Error('Invalid input: expected ReceiptChain instance or JSON object');
69
+ }
70
+
71
+ details.genesis_hash = chain.genesis_hash;
72
+ details.chain_length = chain.getLength();
73
+
74
+ // Check 1: Validate genesis hash format
75
+ if (!/^[0-9a-f]{64}$/.test(chain.genesis_hash)) {
76
+ errors.push(`Invalid genesis hash format: ${chain.genesis_hash}`);
77
+ }
78
+
79
+ // Check 2: Verify each block
80
+ const blocks = chain.getAllBlocks();
81
+ let previous_hash = chain.genesis_hash;
82
+ let previous_timestamp = 0n;
83
+
84
+ for (let i = 0; i < blocks.length; i++) {
85
+ const block = blocks[i];
86
+ details.blocks_checked++;
87
+
88
+ // Check 2a: Verify before_hash matches previous block's after_hash
89
+ if (block.before_hash !== previous_hash) {
90
+ errors.push(
91
+ `Block ${i}: before_hash mismatch (expected ${previous_hash}, got ${block.before_hash})`
92
+ );
93
+ }
94
+
95
+ // Check 2b: Verify content hash matches after_hash
96
+ const computed_hash = await this._computeContentHash({
97
+ timestamp_ns: block.timestamp_ns,
98
+ agent_id: block.agent_id,
99
+ toolchain_version: block.toolchain_version,
100
+ artifacts: block.artifacts
101
+ });
102
+
103
+ if (computed_hash !== block.after_hash) {
104
+ errors.push(
105
+ `Block ${i}: content hash mismatch (expected ${block.after_hash}, got ${computed_hash})`
106
+ );
107
+ }
108
+
109
+ // Check 2c: Verify timestamp monotonicity
110
+ if (block.timestamp_ns <= previous_timestamp) {
111
+ errors.push(
112
+ `Block ${i}: timestamp not monotonic (${block.timestamp_ns} <= ${previous_timestamp})`
113
+ );
114
+ }
115
+ details.timestamp_checks++;
116
+
117
+ // Check 2d: Validate block structure
118
+ if (!block.agent_id || typeof block.agent_id !== 'string') {
119
+ errors.push(`Block ${i}: invalid agent_id`);
120
+ }
121
+ if (!block.toolchain_version || typeof block.toolchain_version !== 'string') {
122
+ errors.push(`Block ${i}: invalid toolchain_version`);
123
+ }
124
+ if (!Array.isArray(block.artifacts)) {
125
+ errors.push(`Block ${i}: artifacts must be an array`);
126
+ }
127
+
128
+ // Update previous hash and timestamp for next iteration
129
+ previous_hash = block.after_hash;
130
+ previous_timestamp = block.timestamp_ns;
131
+ }
132
+
133
+ return {
134
+ valid: errors.length === 0,
135
+ errors,
136
+ details
137
+ };
138
+ } catch (err) {
139
+ errors.push(`Verification exception: ${err.message}`);
140
+ return {
141
+ valid: false,
142
+ errors,
143
+ details
144
+ };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Compute hash of block content (same algorithm as ReceiptChain)
150
+ * @param {Object} content - Block content
151
+ * @returns {Promise<string>} SHA256 hex digest
152
+ * @private
153
+ */
154
+ async _computeContentHash(content) {
155
+ const canonical = JSON.stringify({
156
+ timestamp_ns: content.timestamp_ns.toString(),
157
+ agent_id: content.agent_id,
158
+ toolchain_version: content.toolchain_version,
159
+ artifacts: content.artifacts
160
+ });
161
+ return await sha256(canonical);
162
+ }
163
+
164
+ /**
165
+ * Detect specific tampering scenarios (for testing/auditing)
166
+ *
167
+ * @param {ReceiptChain} original - Original chain
168
+ * @param {ReceiptChain} suspect - Suspect chain to compare
169
+ * @returns {Promise<Object>} Tampering analysis
170
+ *
171
+ * @example
172
+ * const detector = new TamperDetector();
173
+ * const analysis = await detector.detectTampering(original, suspect);
174
+ * console.log(analysis.tamper_type); // e.g., 'bit_flip', 'reordered', 'replay'
175
+ */
176
+ async detectTampering(original, suspect) {
177
+ const original_blocks = original.getAllBlocks();
178
+ const suspect_blocks = suspect.getAllBlocks();
179
+
180
+ const analysis = {
181
+ tamper_type: 'none',
182
+ tampered_blocks: [],
183
+ description: ''
184
+ };
185
+
186
+ // Check 1: Length mismatch
187
+ if (original_blocks.length !== suspect_blocks.length) {
188
+ analysis.tamper_type = 'length_mismatch';
189
+ analysis.description = `Block count changed: ${original_blocks.length} → ${suspect_blocks.length}`;
190
+ return analysis;
191
+ }
192
+
193
+ // Check 2: Block-by-block comparison
194
+ for (let i = 0; i < original_blocks.length; i++) {
195
+ const orig = original_blocks[i];
196
+ const susp = suspect_blocks[i];
197
+
198
+ // Check timestamp
199
+ if (orig.timestamp_ns !== susp.timestamp_ns) {
200
+ analysis.tamper_type = 'timestamp_modified';
201
+ analysis.tampered_blocks.push(i);
202
+ analysis.description = `Block ${i}: timestamp changed`;
203
+ }
204
+
205
+ // Check hashes
206
+ if (orig.after_hash !== susp.after_hash) {
207
+ analysis.tamper_type = 'content_modified';
208
+ analysis.tampered_blocks.push(i);
209
+ analysis.description = `Block ${i}: content hash changed`;
210
+ }
211
+
212
+ // Check agent_id
213
+ if (orig.agent_id !== susp.agent_id) {
214
+ analysis.tamper_type = 'metadata_modified';
215
+ analysis.tampered_blocks.push(i);
216
+ analysis.description = `Block ${i}: agent_id changed`;
217
+ }
218
+ }
219
+
220
+ // Check 3: Ordering check (timestamps out of order)
221
+ for (let i = 1; i < suspect_blocks.length; i++) {
222
+ if (suspect_blocks[i].timestamp_ns <= suspect_blocks[i - 1].timestamp_ns) {
223
+ analysis.tamper_type = 'reordered';
224
+ analysis.tampered_blocks.push(i);
225
+ analysis.description = `Block ${i}: out of order`;
226
+ }
227
+ }
228
+
229
+ return analysis;
230
+ }
231
+
232
+ /**
233
+ * Verify merkle proof for a specific block
234
+ *
235
+ * @param {ReceiptChain} chain - Chain to verify
236
+ * @param {number} blockIndex - Index of block to verify
237
+ * @returns {Promise<Object>} Merkle proof verification result
238
+ *
239
+ * @example
240
+ * const detector = new TamperDetector();
241
+ * const proof = await detector.verifyMerkleProof(chain, 2);
242
+ * console.log(proof.valid); // true if merkle path is valid
243
+ */
244
+ async verifyMerkleProof(chain, blockIndex) {
245
+ const block = chain.getBlock(blockIndex);
246
+ if (!block) {
247
+ return {
248
+ valid: false,
249
+ error: 'Block index out of bounds'
250
+ };
251
+ }
252
+
253
+ // Verify merkle root = SHA256(before_hash || after_hash)
254
+ const computed_root = await sha256(block.before_hash + block.after_hash);
255
+
256
+ return {
257
+ valid: true,
258
+ block_index: blockIndex,
259
+ before_hash: block.before_hash,
260
+ after_hash: block.after_hash,
261
+ merkle_root: computed_root
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Generate tamper report for a chain
267
+ *
268
+ * @param {ReceiptChain} chain - Chain to report on
269
+ * @returns {Promise<Object>} Tamper detection report
270
+ *
271
+ * @example
272
+ * const detector = new TamperDetector();
273
+ * const report = await detector.generateReport(chain);
274
+ * console.log(report.summary); // "Valid: true, Blocks: 5, Errors: 0"
275
+ */
276
+ async generateReport(chain) {
277
+ const verification = await this.verify(chain);
278
+ const blocks = chain.getAllBlocks();
279
+
280
+ const report = {
281
+ timestamp: new Date().toISOString(),
282
+ chain_length: blocks.length,
283
+ genesis_hash: chain.genesis_hash,
284
+ head_hash: chain.getHeadHash(),
285
+ verification: verification,
286
+ summary: `Valid: ${verification.valid}, Blocks: ${blocks.length}, Errors: ${verification.errors.length}`,
287
+ blocks_analyzed: blocks.map((block, idx) => ({
288
+ index: idx,
289
+ agent_id: block.agent_id,
290
+ timestamp_ns: block.timestamp_ns.toString(),
291
+ artifacts_count: block.artifacts.length,
292
+ before_hash: block.before_hash,
293
+ after_hash: block.after_hash
294
+ }))
295
+ };
296
+
297
+ return report;
298
+ }
299
+ }