@thehoneyjar/sigil-anchor 4.3.1

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.
@@ -0,0 +1,3514 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readFile, mkdir, writeFile, readdir, unlink, rm } from 'node:fs/promises';
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { EventEmitter } from 'eventemitter3';
8
+ import { z } from 'zod';
9
+
10
+ var RpcError = class extends Error {
11
+ constructor(code, message, data) {
12
+ super(message);
13
+ this.code = code;
14
+ this.data = data;
15
+ this.name = "RpcError";
16
+ }
17
+ };
18
+ var RpcTimeoutError = class extends Error {
19
+ constructor(method, timeoutMs) {
20
+ super(`RPC call '${method}' timed out after ${timeoutMs}ms`);
21
+ this.method = method;
22
+ this.timeoutMs = timeoutMs;
23
+ this.name = "RpcTimeoutError";
24
+ }
25
+ };
26
+ var DEFAULT_TIMEOUT_MS = 3e4;
27
+ var requestId = 0;
28
+ async function rpcCall(url, method, params = [], timeoutMs = DEFAULT_TIMEOUT_MS) {
29
+ const controller = new AbortController();
30
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
31
+ const request = {
32
+ jsonrpc: "2.0",
33
+ id: ++requestId,
34
+ method,
35
+ params
36
+ };
37
+ try {
38
+ const response = await fetch(url, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json"
42
+ },
43
+ body: JSON.stringify(request),
44
+ signal: controller.signal
45
+ });
46
+ if (!response.ok) {
47
+ throw new RpcError(-32e3, `HTTP error: ${response.status} ${response.statusText}`);
48
+ }
49
+ const data = await response.json();
50
+ if (data.error) {
51
+ throw new RpcError(data.error.code, data.error.message, data.error.data);
52
+ }
53
+ if (data.result === void 0) {
54
+ throw new RpcError(-32e3, "RPC response missing result");
55
+ }
56
+ return data.result;
57
+ } catch (error) {
58
+ if (error instanceof Error && error.name === "AbortError") {
59
+ throw new RpcTimeoutError(method, timeoutMs);
60
+ }
61
+ throw error;
62
+ } finally {
63
+ clearTimeout(timeoutId);
64
+ }
65
+ }
66
+ async function isRpcReady(url, timeoutMs = 5e3) {
67
+ try {
68
+ await rpcCall(url, "eth_chainId", [], timeoutMs);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ async function waitForRpc(url, maxAttempts = 30, intervalMs = 1e3) {
75
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
76
+ if (await isRpcReady(url)) {
77
+ return;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
80
+ }
81
+ throw new Error(`RPC at ${url} not ready after ${maxAttempts} attempts`);
82
+ }
83
+ var hexStringSchema = z.string().regex(/^0x[0-9a-fA-F]*$/);
84
+ z.union([hexStringSchema, z.literal("latest"), z.literal("pending"), z.literal("earliest")]);
85
+ var ForkSchema = z.object({
86
+ id: z.string(),
87
+ network: z.object({
88
+ name: z.string(),
89
+ chainId: z.number(),
90
+ rpcUrl: z.string()
91
+ }),
92
+ blockNumber: z.number(),
93
+ rpcUrl: z.string(),
94
+ port: z.number(),
95
+ pid: z.number(),
96
+ createdAt: z.string().transform((s) => new Date(s)),
97
+ sessionId: z.string().optional()
98
+ });
99
+ var ForkRegistrySchema = z.object({
100
+ forks: z.array(ForkSchema),
101
+ lastUpdated: z.string().transform((s) => new Date(s))
102
+ });
103
+ var ZONE_HIERARCHY = ["critical", "elevated", "standard", "local"];
104
+ var ExitCode = {
105
+ PASS: 0,
106
+ DRIFT: 1,
107
+ DECEPTIVE: 2,
108
+ VIOLATION: 3,
109
+ REVERT: 4,
110
+ CORRUPT: 5,
111
+ SCHEMA: 6
112
+ };
113
+ var LensContextSchema = z.object({
114
+ impersonatedAddress: z.string(),
115
+ realAddress: z.string().optional(),
116
+ component: z.string(),
117
+ observedValue: z.string().optional(),
118
+ onChainValue: z.string().optional(),
119
+ indexedValue: z.string().optional(),
120
+ dataSource: z.enum(["on-chain", "indexed", "mixed", "unknown"]).optional()
121
+ });
122
+ z.object({
123
+ type: z.enum(["data_source_mismatch", "stale_indexed_data", "lens_financial_check", "impersonation_leak"]),
124
+ severity: z.enum(["error", "warning", "info"]),
125
+ message: z.string(),
126
+ component: z.string(),
127
+ zone: z.enum(["critical", "elevated", "standard", "local"]).optional(),
128
+ expected: z.string().optional(),
129
+ actual: z.string().optional(),
130
+ suggestion: z.string().optional()
131
+ });
132
+ var LensExitCode = {
133
+ ...ExitCode,
134
+ LENS_WARNING: 11,
135
+ LENS_ERROR: 10
136
+ };
137
+
138
+ // src/lifecycle/fork-manager.ts
139
+ var DEFAULT_PORT_START = 8545;
140
+ var DEFAULT_PORT_END = 8600;
141
+ var DEFAULT_REGISTRY_PATH = "grimoires/anchor/forks.json";
142
+ var ForkManager = class extends EventEmitter {
143
+ forks = /* @__PURE__ */ new Map();
144
+ processes = /* @__PURE__ */ new Map();
145
+ usedPorts = /* @__PURE__ */ new Set();
146
+ registryPath;
147
+ constructor(options) {
148
+ super();
149
+ this.registryPath = options?.registryPath ?? DEFAULT_REGISTRY_PATH;
150
+ }
151
+ /**
152
+ * Initialize the ForkManager by loading persisted registry
153
+ */
154
+ async init() {
155
+ await this.loadRegistry();
156
+ }
157
+ /**
158
+ * Spawn a new Anvil fork
159
+ *
160
+ * @param config - Fork configuration
161
+ * @returns Promise resolving to the created Fork
162
+ */
163
+ async fork(config) {
164
+ const port = config.port ?? this.findAvailablePort();
165
+ const forkId = this.generateForkId();
166
+ const args = [
167
+ "--fork-url",
168
+ config.network.rpcUrl,
169
+ "--port",
170
+ port.toString(),
171
+ "--chain-id",
172
+ config.network.chainId.toString()
173
+ ];
174
+ if (config.blockNumber !== void 0) {
175
+ args.push("--fork-block-number", config.blockNumber.toString());
176
+ }
177
+ const process2 = spawn("anvil", args, {
178
+ stdio: ["ignore", "pipe", "pipe"],
179
+ detached: false
180
+ });
181
+ const pid = process2.pid;
182
+ if (!pid) {
183
+ throw new Error("Failed to spawn Anvil process");
184
+ }
185
+ const rpcUrl = `http://127.0.0.1:${port}`;
186
+ try {
187
+ await waitForRpc(rpcUrl, 30, 500);
188
+ } catch {
189
+ process2.kill();
190
+ throw new Error(`Anvil fork failed to become ready at ${rpcUrl}`);
191
+ }
192
+ const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
193
+ const blockNumber = config.blockNumber ?? parseInt(blockNumberHex, 16);
194
+ const fork = {
195
+ id: forkId,
196
+ network: config.network,
197
+ blockNumber,
198
+ rpcUrl,
199
+ port,
200
+ pid,
201
+ createdAt: /* @__PURE__ */ new Date(),
202
+ ...config.sessionId !== void 0 && { sessionId: config.sessionId }
203
+ };
204
+ this.forks.set(forkId, fork);
205
+ this.processes.set(forkId, process2);
206
+ this.usedPorts.add(port);
207
+ process2.on("exit", (code) => {
208
+ this.handleProcessExit(forkId, code);
209
+ });
210
+ process2.on("error", (error) => {
211
+ this.emit("fork:error", forkId, error);
212
+ });
213
+ await this.saveRegistry();
214
+ this.emit("fork:created", fork);
215
+ return fork;
216
+ }
217
+ /**
218
+ * Wait for a fork to be ready
219
+ *
220
+ * @param forkId - Fork ID to wait for
221
+ * @param timeoutMs - Timeout in milliseconds
222
+ */
223
+ async waitForReady(forkId, timeoutMs = 3e4) {
224
+ const fork = this.forks.get(forkId);
225
+ if (!fork) {
226
+ throw new Error(`Fork ${forkId} not found`);
227
+ }
228
+ await waitForRpc(fork.rpcUrl, Math.ceil(timeoutMs / 500), 500);
229
+ }
230
+ /**
231
+ * Kill a specific fork
232
+ *
233
+ * @param forkId - Fork ID to kill
234
+ */
235
+ async kill(forkId) {
236
+ const process2 = this.processes.get(forkId);
237
+ const fork = this.forks.get(forkId);
238
+ if (process2) {
239
+ process2.kill("SIGTERM");
240
+ await new Promise((resolve) => setTimeout(resolve, 500));
241
+ if (!process2.killed) {
242
+ process2.kill("SIGKILL");
243
+ }
244
+ }
245
+ if (fork) {
246
+ this.usedPorts.delete(fork.port);
247
+ }
248
+ this.forks.delete(forkId);
249
+ this.processes.delete(forkId);
250
+ await this.saveRegistry();
251
+ }
252
+ /**
253
+ * Kill all forks
254
+ */
255
+ async killAll() {
256
+ const forkIds = Array.from(this.forks.keys());
257
+ await Promise.all(forkIds.map((id) => this.kill(id)));
258
+ }
259
+ /**
260
+ * List all active forks
261
+ *
262
+ * @returns Array of active forks
263
+ */
264
+ list() {
265
+ return Array.from(this.forks.values());
266
+ }
267
+ /**
268
+ * Get a fork by ID
269
+ *
270
+ * @param forkId - Fork ID
271
+ * @returns Fork if found, undefined otherwise
272
+ */
273
+ get(forkId) {
274
+ return this.forks.get(forkId);
275
+ }
276
+ /**
277
+ * Export environment variables for a fork
278
+ *
279
+ * @param forkId - Fork ID
280
+ * @returns Environment variables object
281
+ */
282
+ exportEnv(forkId) {
283
+ const fork = this.forks.get(forkId);
284
+ if (!fork) {
285
+ throw new Error(`Fork ${forkId} not found`);
286
+ }
287
+ return {
288
+ RPC_URL: fork.rpcUrl,
289
+ CHAIN_ID: fork.network.chainId.toString(),
290
+ FORK_BLOCK: fork.blockNumber.toString(),
291
+ FORK_ID: fork.id
292
+ };
293
+ }
294
+ /**
295
+ * Load fork registry from disk
296
+ */
297
+ async loadRegistry() {
298
+ try {
299
+ if (!existsSync(this.registryPath)) {
300
+ return;
301
+ }
302
+ const content = await readFile(this.registryPath, "utf-8");
303
+ const data = JSON.parse(content);
304
+ const registry = ForkRegistrySchema.parse(data);
305
+ for (const registryFork of registry.forks) {
306
+ try {
307
+ process.kill(registryFork.pid, 0);
308
+ const fork = {
309
+ id: registryFork.id,
310
+ network: registryFork.network,
311
+ blockNumber: registryFork.blockNumber,
312
+ rpcUrl: registryFork.rpcUrl,
313
+ port: registryFork.port,
314
+ pid: registryFork.pid,
315
+ createdAt: registryFork.createdAt,
316
+ ...registryFork.sessionId !== void 0 && { sessionId: registryFork.sessionId }
317
+ };
318
+ const ready = await this.checkForkHealth(fork);
319
+ if (ready) {
320
+ this.forks.set(fork.id, fork);
321
+ this.usedPorts.add(fork.port);
322
+ }
323
+ } catch {
324
+ }
325
+ }
326
+ } catch {
327
+ }
328
+ }
329
+ /**
330
+ * Save fork registry to disk
331
+ */
332
+ async saveRegistry() {
333
+ const registry = {
334
+ forks: Array.from(this.forks.values()).map((fork) => ({
335
+ ...fork,
336
+ createdAt: fork.createdAt.toISOString()
337
+ })),
338
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
339
+ };
340
+ const dir = dirname(this.registryPath);
341
+ if (!existsSync(dir)) {
342
+ await mkdir(dir, { recursive: true });
343
+ }
344
+ await writeFile(this.registryPath, JSON.stringify(registry, null, 2));
345
+ }
346
+ /**
347
+ * Check if a fork is still healthy
348
+ */
349
+ async checkForkHealth(fork) {
350
+ try {
351
+ await rpcCall(fork.rpcUrl, "eth_chainId", [], 2e3);
352
+ return true;
353
+ } catch {
354
+ return false;
355
+ }
356
+ }
357
+ /**
358
+ * Handle process exit
359
+ */
360
+ handleProcessExit(forkId, code) {
361
+ const fork = this.forks.get(forkId);
362
+ if (fork) {
363
+ this.usedPorts.delete(fork.port);
364
+ }
365
+ this.forks.delete(forkId);
366
+ this.processes.delete(forkId);
367
+ this.emit("fork:exit", forkId, code);
368
+ void this.saveRegistry();
369
+ }
370
+ /**
371
+ * Find an available port
372
+ */
373
+ findAvailablePort() {
374
+ for (let port = DEFAULT_PORT_START; port <= DEFAULT_PORT_END; port++) {
375
+ if (!this.usedPorts.has(port)) {
376
+ return port;
377
+ }
378
+ }
379
+ throw new Error("No available ports in range");
380
+ }
381
+ /**
382
+ * Generate a unique fork ID
383
+ */
384
+ generateForkId() {
385
+ const timestamp = Date.now().toString(36);
386
+ const random = Math.random().toString(36).substring(2, 8);
387
+ return `fork-${timestamp}-${random}`;
388
+ }
389
+ };
390
+ var DEFAULT_BASE_PATH = "grimoires/anchor/sessions";
391
+ var SnapshotManager = class {
392
+ snapshots = /* @__PURE__ */ new Map();
393
+ taskToSnapshot = /* @__PURE__ */ new Map();
394
+ basePath;
395
+ sessionId = null;
396
+ constructor(config) {
397
+ this.basePath = config?.basePath ?? DEFAULT_BASE_PATH;
398
+ }
399
+ /**
400
+ * Initialize the manager for a session
401
+ *
402
+ * @param sessionId - Session ID to manage snapshots for
403
+ */
404
+ async init(sessionId) {
405
+ this.sessionId = sessionId;
406
+ await this.loadSnapshots();
407
+ }
408
+ /**
409
+ * Create a new snapshot
410
+ *
411
+ * @param config - Snapshot configuration
412
+ * @param rpcUrl - RPC URL of the fork
413
+ * @returns Promise resolving to snapshot metadata
414
+ */
415
+ async create(config, rpcUrl) {
416
+ const snapshotId = await rpcCall(rpcUrl, "evm_snapshot");
417
+ const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
418
+ const blockNumber = parseInt(blockNumberHex, 16);
419
+ const metadata = {
420
+ id: snapshotId,
421
+ forkId: config.forkId,
422
+ sessionId: config.sessionId,
423
+ blockNumber,
424
+ createdAt: /* @__PURE__ */ new Date(),
425
+ ...config.taskId !== void 0 && { taskId: config.taskId },
426
+ ...config.description !== void 0 && { description: config.description }
427
+ };
428
+ this.snapshots.set(snapshotId, metadata);
429
+ if (config.taskId) {
430
+ this.taskToSnapshot.set(config.taskId, snapshotId);
431
+ }
432
+ await this.saveSnapshot(metadata);
433
+ return metadata;
434
+ }
435
+ /**
436
+ * Revert to a snapshot
437
+ *
438
+ * @param rpcUrl - RPC URL of the fork
439
+ * @param snapshotId - Snapshot ID to revert to
440
+ * @returns Promise resolving to true if successful
441
+ */
442
+ async revert(rpcUrl, snapshotId) {
443
+ const result = await rpcCall(rpcUrl, "evm_revert", [snapshotId]);
444
+ return result;
445
+ }
446
+ /**
447
+ * Get snapshot metadata by ID
448
+ *
449
+ * @param snapshotId - Snapshot ID
450
+ * @returns Snapshot metadata if found
451
+ */
452
+ get(snapshotId) {
453
+ return this.snapshots.get(snapshotId);
454
+ }
455
+ /**
456
+ * List all snapshots sorted by creation time
457
+ *
458
+ * @returns Array of snapshot metadata
459
+ */
460
+ list() {
461
+ return Array.from(this.snapshots.values()).sort(
462
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
463
+ );
464
+ }
465
+ /**
466
+ * Get snapshot for a specific task
467
+ *
468
+ * @param taskId - Task ID
469
+ * @returns Snapshot metadata if found
470
+ */
471
+ getForTask(taskId) {
472
+ const snapshotId = this.taskToSnapshot.get(taskId);
473
+ if (!snapshotId)
474
+ return void 0;
475
+ return this.snapshots.get(snapshotId);
476
+ }
477
+ /**
478
+ * Get the count of snapshots
479
+ *
480
+ * @returns Number of snapshots
481
+ */
482
+ count() {
483
+ return this.snapshots.size;
484
+ }
485
+ /**
486
+ * Cleanup old snapshots, keeping the most recent
487
+ *
488
+ * @param keepLast - Number of recent snapshots to keep
489
+ */
490
+ async cleanup(keepLast) {
491
+ const sorted = this.list();
492
+ const toDelete = sorted.slice(0, -keepLast);
493
+ for (const snapshot of toDelete) {
494
+ await this.deleteSnapshot(snapshot.id);
495
+ }
496
+ }
497
+ /**
498
+ * Get snapshot directory path for current session
499
+ */
500
+ getSnapshotDir() {
501
+ if (!this.sessionId) {
502
+ throw new Error("SnapshotManager not initialized with session ID");
503
+ }
504
+ return join(this.basePath, this.sessionId, "snapshots");
505
+ }
506
+ /**
507
+ * Get file path for a snapshot
508
+ */
509
+ getSnapshotPath(snapshotId) {
510
+ return join(this.getSnapshotDir(), `${snapshotId}.json`);
511
+ }
512
+ /**
513
+ * Load existing snapshots from disk
514
+ */
515
+ async loadSnapshots() {
516
+ const dir = this.getSnapshotDir();
517
+ if (!existsSync(dir)) {
518
+ return;
519
+ }
520
+ try {
521
+ const files = await readdir(dir);
522
+ for (const file of files) {
523
+ if (!file.endsWith(".json"))
524
+ continue;
525
+ try {
526
+ const content = await readFile(join(dir, file), "utf-8");
527
+ const data = JSON.parse(content);
528
+ data.createdAt = new Date(data.createdAt);
529
+ this.snapshots.set(data.id, data);
530
+ if (data.taskId) {
531
+ this.taskToSnapshot.set(data.taskId, data.id);
532
+ }
533
+ } catch {
534
+ }
535
+ }
536
+ } catch {
537
+ }
538
+ }
539
+ /**
540
+ * Save snapshot metadata to disk
541
+ */
542
+ async saveSnapshot(metadata) {
543
+ const dir = this.getSnapshotDir();
544
+ if (!existsSync(dir)) {
545
+ await mkdir(dir, { recursive: true });
546
+ }
547
+ await writeFile(
548
+ this.getSnapshotPath(metadata.id),
549
+ JSON.stringify(metadata, null, 2)
550
+ );
551
+ }
552
+ /**
553
+ * Delete a snapshot from memory and disk
554
+ */
555
+ async deleteSnapshot(snapshotId) {
556
+ const metadata = this.snapshots.get(snapshotId);
557
+ this.snapshots.delete(snapshotId);
558
+ if (metadata?.taskId) {
559
+ this.taskToSnapshot.delete(metadata.taskId);
560
+ }
561
+ try {
562
+ await unlink(this.getSnapshotPath(snapshotId));
563
+ } catch {
564
+ }
565
+ }
566
+ };
567
+ var DEFAULT_BASE_PATH2 = "grimoires/anchor/checkpoints";
568
+ var DEFAULT_SNAPSHOT_INTERVAL = 10;
569
+ var DEFAULT_MAX_CHECKPOINTS = 5;
570
+ var CheckpointManager = class {
571
+ checkpoints = /* @__PURE__ */ new Map();
572
+ snapshotCount = 0;
573
+ firstSnapshotId = null;
574
+ lastSnapshotId = null;
575
+ basePath;
576
+ snapshotInterval;
577
+ maxCheckpoints;
578
+ sessionId = null;
579
+ forkId = null;
580
+ constructor(config) {
581
+ this.basePath = config?.basePath ?? DEFAULT_BASE_PATH2;
582
+ this.snapshotInterval = config?.snapshotInterval ?? DEFAULT_SNAPSHOT_INTERVAL;
583
+ this.maxCheckpoints = config?.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
584
+ }
585
+ /**
586
+ * Initialize the manager for a session
587
+ *
588
+ * @param sessionId - Session ID
589
+ * @param forkId - Fork ID
590
+ */
591
+ async init(sessionId, forkId) {
592
+ this.sessionId = sessionId;
593
+ this.forkId = forkId;
594
+ await this.loadCheckpoints();
595
+ }
596
+ /**
597
+ * Called when a snapshot is created. May trigger checkpoint.
598
+ *
599
+ * @param snapshotId - ID of the created snapshot
600
+ * @param rpcUrl - RPC URL of the fork
601
+ * @returns True if checkpoint was created
602
+ */
603
+ async onSnapshot(snapshotId, rpcUrl) {
604
+ this.snapshotCount++;
605
+ if (!this.firstSnapshotId) {
606
+ this.firstSnapshotId = snapshotId;
607
+ }
608
+ this.lastSnapshotId = snapshotId;
609
+ if (this.snapshotCount >= this.snapshotInterval) {
610
+ await this.create(rpcUrl);
611
+ return true;
612
+ }
613
+ return false;
614
+ }
615
+ /**
616
+ * Create a checkpoint by exporting state
617
+ *
618
+ * @param rpcUrl - RPC URL of the fork
619
+ * @returns Checkpoint metadata
620
+ */
621
+ async create(rpcUrl) {
622
+ if (!this.sessionId || !this.forkId) {
623
+ throw new Error("CheckpointManager not initialized");
624
+ }
625
+ const state = await rpcCall(rpcUrl, "anvil_dumpState");
626
+ const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
627
+ const blockNumber = parseInt(blockNumberHex, 16);
628
+ const checkpointId = this.generateCheckpointId();
629
+ const metadata = {
630
+ id: checkpointId,
631
+ sessionId: this.sessionId,
632
+ forkId: this.forkId,
633
+ snapshotRange: {
634
+ first: this.firstSnapshotId ?? "",
635
+ last: this.lastSnapshotId ?? ""
636
+ },
637
+ blockNumber,
638
+ createdAt: /* @__PURE__ */ new Date(),
639
+ snapshotCount: this.snapshotCount
640
+ };
641
+ await this.saveCheckpoint(checkpointId, state, metadata);
642
+ this.checkpoints.set(checkpointId, metadata);
643
+ this.snapshotCount = 0;
644
+ this.firstSnapshotId = null;
645
+ this.lastSnapshotId = null;
646
+ await this.cleanup();
647
+ return metadata;
648
+ }
649
+ /**
650
+ * Restore from a checkpoint
651
+ *
652
+ * @param checkpointId - Checkpoint ID to restore
653
+ * @param forkManager - ForkManager instance
654
+ * @param network - Network configuration
655
+ * @returns New fork with restored state
656
+ */
657
+ async restore(checkpointId, forkManager, network) {
658
+ if (!this.sessionId) {
659
+ throw new Error("CheckpointManager not initialized");
660
+ }
661
+ const checkpoint = this.checkpoints.get(checkpointId);
662
+ if (!checkpoint) {
663
+ throw new Error(`Checkpoint ${checkpointId} not found`);
664
+ }
665
+ const statePath = this.getStatePath(checkpointId);
666
+ const state = await readFile(statePath, "utf-8");
667
+ await forkManager.killAll();
668
+ const fork = await forkManager.fork({
669
+ network,
670
+ blockNumber: checkpoint.blockNumber,
671
+ sessionId: this.sessionId
672
+ });
673
+ await rpcCall(fork.rpcUrl, "anvil_loadState", [state]);
674
+ this.forkId = fork.id;
675
+ return fork;
676
+ }
677
+ /**
678
+ * Find the checkpoint containing a specific snapshot
679
+ *
680
+ * @param snapshotId - Snapshot ID to find
681
+ * @returns Checkpoint metadata if found
682
+ */
683
+ findCheckpointForSnapshot(snapshotId) {
684
+ const sorted = this.list().sort(
685
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
686
+ );
687
+ for (const checkpoint of sorted) {
688
+ if (checkpoint.snapshotRange.first <= snapshotId && checkpoint.snapshotRange.last >= snapshotId) {
689
+ return checkpoint;
690
+ }
691
+ }
692
+ return sorted[0];
693
+ }
694
+ /**
695
+ * Get checkpoint by ID
696
+ *
697
+ * @param checkpointId - Checkpoint ID
698
+ * @returns Checkpoint metadata if found
699
+ */
700
+ get(checkpointId) {
701
+ return this.checkpoints.get(checkpointId);
702
+ }
703
+ /**
704
+ * List all checkpoints sorted by time
705
+ *
706
+ * @returns Array of checkpoint metadata
707
+ */
708
+ list() {
709
+ return Array.from(this.checkpoints.values()).sort(
710
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
711
+ );
712
+ }
713
+ /**
714
+ * Get the latest checkpoint
715
+ *
716
+ * @returns Latest checkpoint metadata
717
+ */
718
+ latest() {
719
+ const sorted = this.list();
720
+ return sorted[sorted.length - 1];
721
+ }
722
+ /**
723
+ * Cleanup old checkpoints, keeping only the most recent
724
+ */
725
+ async cleanup() {
726
+ const sorted = this.list();
727
+ if (sorted.length <= this.maxCheckpoints) {
728
+ return;
729
+ }
730
+ const toDelete = sorted.slice(0, sorted.length - this.maxCheckpoints);
731
+ for (const checkpoint of toDelete) {
732
+ await this.deleteCheckpoint(checkpoint.id);
733
+ }
734
+ }
735
+ /**
736
+ * Get session directory path
737
+ */
738
+ getSessionDir() {
739
+ if (!this.sessionId) {
740
+ throw new Error("Session ID not set");
741
+ }
742
+ return join(this.basePath, this.sessionId);
743
+ }
744
+ /**
745
+ * Get checkpoint directory path
746
+ */
747
+ getCheckpointDir(checkpointId) {
748
+ return join(this.getSessionDir(), checkpointId);
749
+ }
750
+ /**
751
+ * Get state file path
752
+ */
753
+ getStatePath(checkpointId) {
754
+ return join(this.getCheckpointDir(checkpointId), "state.json");
755
+ }
756
+ /**
757
+ * Get metadata file path
758
+ */
759
+ getMetaPath(checkpointId) {
760
+ return join(this.getCheckpointDir(checkpointId), "meta.json");
761
+ }
762
+ /**
763
+ * Load checkpoints from disk
764
+ */
765
+ async loadCheckpoints() {
766
+ const dir = this.getSessionDir();
767
+ if (!existsSync(dir)) {
768
+ return;
769
+ }
770
+ try {
771
+ const entries = await readdir(dir, { withFileTypes: true });
772
+ for (const entry of entries) {
773
+ if (!entry.isDirectory())
774
+ continue;
775
+ const metaPath = this.getMetaPath(entry.name);
776
+ if (!existsSync(metaPath))
777
+ continue;
778
+ try {
779
+ const content = await readFile(metaPath, "utf-8");
780
+ const data = JSON.parse(content);
781
+ data.createdAt = new Date(data.createdAt);
782
+ this.checkpoints.set(data.id, data);
783
+ } catch {
784
+ }
785
+ }
786
+ } catch {
787
+ }
788
+ }
789
+ /**
790
+ * Save checkpoint to disk
791
+ */
792
+ async saveCheckpoint(checkpointId, state, metadata) {
793
+ const dir = this.getCheckpointDir(checkpointId);
794
+ if (!existsSync(dir)) {
795
+ await mkdir(dir, { recursive: true });
796
+ }
797
+ await writeFile(this.getStatePath(checkpointId), state);
798
+ await writeFile(this.getMetaPath(checkpointId), JSON.stringify(metadata, null, 2));
799
+ }
800
+ /**
801
+ * Delete a checkpoint from disk
802
+ */
803
+ async deleteCheckpoint(checkpointId) {
804
+ this.checkpoints.delete(checkpointId);
805
+ const dir = this.getCheckpointDir(checkpointId);
806
+ if (existsSync(dir)) {
807
+ await rm(dir, { recursive: true });
808
+ }
809
+ }
810
+ /**
811
+ * Generate a unique checkpoint ID
812
+ */
813
+ generateCheckpointId() {
814
+ const timestamp = Date.now().toString(36);
815
+ const random = Math.random().toString(36).substring(2, 6);
816
+ return `cp-${timestamp}-${random}`;
817
+ }
818
+ };
819
+ var TaskSchema = z.object({
820
+ id: z.string(),
821
+ type: z.enum(["fork", "ground", "warden", "generate", "validate", "write"]),
822
+ status: z.enum(["pending", "running", "complete", "blocked", "failed"]),
823
+ snapshotId: z.string().optional(),
824
+ checkpointId: z.string().optional(),
825
+ dependencies: z.array(z.string()),
826
+ input: z.unknown(),
827
+ output: z.unknown().optional(),
828
+ error: z.string().optional(),
829
+ createdAt: z.string().transform((s) => new Date(s)),
830
+ completedAt: z.string().transform((s) => new Date(s)).optional()
831
+ });
832
+ var TaskGraphDataSchema = z.object({
833
+ sessionId: z.string(),
834
+ tasks: z.array(TaskSchema),
835
+ headTaskId: z.string().optional(),
836
+ lastUpdated: z.string().transform((s) => new Date(s))
837
+ });
838
+ var DEFAULT_BASE_PATH3 = "grimoires/anchor/sessions";
839
+ var TaskGraph = class {
840
+ tasks = /* @__PURE__ */ new Map();
841
+ dependents = /* @__PURE__ */ new Map();
842
+ sessionId;
843
+ basePath;
844
+ autoSave;
845
+ headTaskId;
846
+ constructor(config) {
847
+ this.sessionId = config.sessionId;
848
+ this.basePath = config.basePath ?? DEFAULT_BASE_PATH3;
849
+ this.autoSave = config.autoSave ?? true;
850
+ }
851
+ /**
852
+ * Initialize the graph by loading persisted state
853
+ */
854
+ async init() {
855
+ await this.load();
856
+ }
857
+ /**
858
+ * Add a task to the graph
859
+ *
860
+ * @param task - Task to add
861
+ */
862
+ async addTask(task) {
863
+ this.validateNoCycle(task);
864
+ this.tasks.set(task.id, task);
865
+ for (const depId of task.dependencies) {
866
+ if (!this.dependents.has(depId)) {
867
+ this.dependents.set(depId, /* @__PURE__ */ new Set());
868
+ }
869
+ this.dependents.get(depId).add(task.id);
870
+ }
871
+ this.headTaskId = task.id;
872
+ if (this.autoSave) {
873
+ await this.save();
874
+ }
875
+ }
876
+ /**
877
+ * Update task status
878
+ *
879
+ * @param taskId - Task ID
880
+ * @param status - New status
881
+ */
882
+ async updateStatus(taskId, status) {
883
+ const task = this.tasks.get(taskId);
884
+ if (!task) {
885
+ throw new Error(`Task ${taskId} not found`);
886
+ }
887
+ task.status = status;
888
+ if (status === "complete" || status === "failed") {
889
+ task.completedAt = /* @__PURE__ */ new Date();
890
+ }
891
+ if (this.autoSave) {
892
+ await this.save();
893
+ }
894
+ }
895
+ /**
896
+ * Set the snapshot binding for a task
897
+ *
898
+ * @param taskId - Task ID
899
+ * @param snapshotId - Snapshot ID
900
+ */
901
+ async setSnapshot(taskId, snapshotId) {
902
+ const task = this.tasks.get(taskId);
903
+ if (!task) {
904
+ throw new Error(`Task ${taskId} not found`);
905
+ }
906
+ task.snapshotId = snapshotId;
907
+ if (this.autoSave) {
908
+ await this.save();
909
+ }
910
+ }
911
+ /**
912
+ * Set the checkpoint binding for a task
913
+ *
914
+ * @param taskId - Task ID
915
+ * @param checkpointId - Checkpoint ID
916
+ */
917
+ async setCheckpoint(taskId, checkpointId) {
918
+ const task = this.tasks.get(taskId);
919
+ if (!task) {
920
+ throw new Error(`Task ${taskId} not found`);
921
+ }
922
+ task.checkpointId = checkpointId;
923
+ if (this.autoSave) {
924
+ await this.save();
925
+ }
926
+ }
927
+ /**
928
+ * Set task output
929
+ *
930
+ * @param taskId - Task ID
931
+ * @param output - Task output
932
+ */
933
+ async setOutput(taskId, output) {
934
+ const task = this.tasks.get(taskId);
935
+ if (!task) {
936
+ throw new Error(`Task ${taskId} not found`);
937
+ }
938
+ task.output = output;
939
+ if (this.autoSave) {
940
+ await this.save();
941
+ }
942
+ }
943
+ /**
944
+ * Set task error
945
+ *
946
+ * @param taskId - Task ID
947
+ * @param error - Error message
948
+ */
949
+ async setError(taskId, error) {
950
+ const task = this.tasks.get(taskId);
951
+ if (!task) {
952
+ throw new Error(`Task ${taskId} not found`);
953
+ }
954
+ task.error = error;
955
+ task.status = "failed";
956
+ if (this.autoSave) {
957
+ await this.save();
958
+ }
959
+ }
960
+ /**
961
+ * Get a task by ID
962
+ *
963
+ * @param taskId - Task ID
964
+ * @returns Task if found
965
+ */
966
+ getTask(taskId) {
967
+ return this.tasks.get(taskId);
968
+ }
969
+ /**
970
+ * Get all tasks
971
+ *
972
+ * @returns Array of all tasks
973
+ */
974
+ getAllTasks() {
975
+ return Array.from(this.tasks.values());
976
+ }
977
+ /**
978
+ * Get tasks by status
979
+ *
980
+ * @param status - Status to filter by
981
+ * @returns Array of matching tasks
982
+ */
983
+ getTasksByStatus(status) {
984
+ return Array.from(this.tasks.values()).filter((t) => t.status === status);
985
+ }
986
+ /**
987
+ * Check if a task can run (all dependencies complete)
988
+ *
989
+ * @param taskId - Task ID
990
+ * @returns True if all dependencies are complete
991
+ */
992
+ canRun(taskId) {
993
+ const task = this.tasks.get(taskId);
994
+ if (!task)
995
+ return false;
996
+ if (task.status !== "pending")
997
+ return false;
998
+ for (const depId of task.dependencies) {
999
+ const dep = this.tasks.get(depId);
1000
+ if (!dep || dep.status !== "complete") {
1001
+ return false;
1002
+ }
1003
+ }
1004
+ return true;
1005
+ }
1006
+ /**
1007
+ * Get the next runnable task (pending with all deps complete)
1008
+ *
1009
+ * @returns Next runnable task or undefined
1010
+ */
1011
+ getNextRunnable() {
1012
+ for (const task of this.tasks.values()) {
1013
+ if (this.canRun(task.id)) {
1014
+ return task;
1015
+ }
1016
+ }
1017
+ return void 0;
1018
+ }
1019
+ /**
1020
+ * Propagate blocked status to all dependents of a failed task
1021
+ *
1022
+ * @param taskId - ID of the failed task
1023
+ */
1024
+ async propagateBlocked(taskId) {
1025
+ const dependentIds = this.dependents.get(taskId);
1026
+ if (!dependentIds)
1027
+ return;
1028
+ for (const depId of dependentIds) {
1029
+ const task = this.tasks.get(depId);
1030
+ if (task && task.status === "pending") {
1031
+ task.status = "blocked";
1032
+ await this.propagateBlocked(depId);
1033
+ }
1034
+ }
1035
+ if (this.autoSave) {
1036
+ await this.save();
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Find the recovery point for a failed task
1041
+ *
1042
+ * @param taskId - ID of the task needing recovery
1043
+ * @returns Last complete task with snapshot, or undefined
1044
+ */
1045
+ findRecoveryPoint(taskId) {
1046
+ const task = this.tasks.get(taskId);
1047
+ if (!task)
1048
+ return void 0;
1049
+ const visited = /* @__PURE__ */ new Set();
1050
+ const queue = [...task.dependencies];
1051
+ let bestRecovery;
1052
+ while (queue.length > 0) {
1053
+ const id = queue.pop();
1054
+ if (visited.has(id))
1055
+ continue;
1056
+ visited.add(id);
1057
+ const dep = this.tasks.get(id);
1058
+ if (!dep)
1059
+ continue;
1060
+ if (dep.status === "complete" && dep.snapshotId) {
1061
+ if (!bestRecovery || dep.createdAt > bestRecovery.createdAt) {
1062
+ bestRecovery = dep;
1063
+ }
1064
+ }
1065
+ queue.push(...dep.dependencies);
1066
+ }
1067
+ return bestRecovery;
1068
+ }
1069
+ /**
1070
+ * Check if there are any blocked tasks
1071
+ *
1072
+ * @returns True if any tasks are blocked
1073
+ */
1074
+ hasBlocked() {
1075
+ for (const task of this.tasks.values()) {
1076
+ if (task.status === "blocked") {
1077
+ return true;
1078
+ }
1079
+ }
1080
+ return false;
1081
+ }
1082
+ /**
1083
+ * Check if all tasks are complete
1084
+ *
1085
+ * @returns True if all tasks are complete
1086
+ */
1087
+ isComplete() {
1088
+ for (const task of this.tasks.values()) {
1089
+ if (task.status !== "complete") {
1090
+ return false;
1091
+ }
1092
+ }
1093
+ return this.tasks.size > 0;
1094
+ }
1095
+ /**
1096
+ * Get the graph file path
1097
+ */
1098
+ getGraphPath() {
1099
+ return join(this.basePath, this.sessionId, "graph.json");
1100
+ }
1101
+ /**
1102
+ * Export graph data as JSON-serializable object
1103
+ *
1104
+ * @returns Task graph data
1105
+ */
1106
+ toJSON() {
1107
+ return {
1108
+ sessionId: this.sessionId,
1109
+ tasks: Array.from(this.tasks.values()),
1110
+ lastUpdated: /* @__PURE__ */ new Date(),
1111
+ ...this.headTaskId !== void 0 && { headTaskId: this.headTaskId }
1112
+ };
1113
+ }
1114
+ /**
1115
+ * Save the graph to disk
1116
+ */
1117
+ async save() {
1118
+ const data = this.toJSON();
1119
+ const path = this.getGraphPath();
1120
+ const dir = dirname(path);
1121
+ if (!existsSync(dir)) {
1122
+ await mkdir(dir, { recursive: true });
1123
+ }
1124
+ await writeFile(path, JSON.stringify(data, null, 2));
1125
+ }
1126
+ /**
1127
+ * Load the graph from disk
1128
+ */
1129
+ async load() {
1130
+ const path = this.getGraphPath();
1131
+ if (!existsSync(path)) {
1132
+ return;
1133
+ }
1134
+ try {
1135
+ const content = await readFile(path, "utf-8");
1136
+ const raw = JSON.parse(content);
1137
+ const data = TaskGraphDataSchema.parse(raw);
1138
+ this.tasks.clear();
1139
+ this.dependents.clear();
1140
+ for (const task of data.tasks) {
1141
+ this.tasks.set(task.id, task);
1142
+ for (const depId of task.dependencies) {
1143
+ if (!this.dependents.has(depId)) {
1144
+ this.dependents.set(depId, /* @__PURE__ */ new Set());
1145
+ }
1146
+ this.dependents.get(depId).add(task.id);
1147
+ }
1148
+ }
1149
+ this.headTaskId = data.headTaskId;
1150
+ } catch {
1151
+ }
1152
+ }
1153
+ /**
1154
+ * Validate that adding a task doesn't create a cycle
1155
+ */
1156
+ validateNoCycle(newTask) {
1157
+ const visited = /* @__PURE__ */ new Set();
1158
+ const stack = /* @__PURE__ */ new Set();
1159
+ const hasCycle = (taskId) => {
1160
+ if (stack.has(taskId))
1161
+ return true;
1162
+ if (visited.has(taskId))
1163
+ return false;
1164
+ visited.add(taskId);
1165
+ stack.add(taskId);
1166
+ const task = taskId === newTask.id ? newTask : this.tasks.get(taskId);
1167
+ if (task) {
1168
+ for (const depId of task.dependencies) {
1169
+ if (hasCycle(depId))
1170
+ return true;
1171
+ }
1172
+ }
1173
+ stack.delete(taskId);
1174
+ return false;
1175
+ };
1176
+ if (hasCycle(newTask.id)) {
1177
+ throw new Error(`Adding task ${newTask.id} would create a circular dependency`);
1178
+ }
1179
+ }
1180
+ };
1181
+
1182
+ // src/lifecycle/session-manager.ts
1183
+ var DEFAULT_BASE_PATH4 = "grimoires/anchor/sessions";
1184
+ var SessionManager = class {
1185
+ sessions = /* @__PURE__ */ new Map();
1186
+ currentSession = null;
1187
+ basePath;
1188
+ constructor(config) {
1189
+ this.basePath = config?.basePath ?? DEFAULT_BASE_PATH4;
1190
+ }
1191
+ /**
1192
+ * Initialize the manager by loading session index
1193
+ */
1194
+ async init() {
1195
+ await this.loadSessionIndex();
1196
+ }
1197
+ /**
1198
+ * Create a new session
1199
+ *
1200
+ * @param network - Network to fork
1201
+ * @param options - Session options
1202
+ * @returns Created session
1203
+ */
1204
+ async create(network, options) {
1205
+ const sessionId = this.generateSessionId();
1206
+ const forkManager = new ForkManager();
1207
+ await forkManager.init();
1208
+ const fork = await forkManager.fork({
1209
+ network,
1210
+ sessionId,
1211
+ ...options?.blockNumber !== void 0 && { blockNumber: options.blockNumber }
1212
+ });
1213
+ const snapshotManager = new SnapshotManager();
1214
+ await snapshotManager.init(sessionId);
1215
+ const checkpointManager = new CheckpointManager();
1216
+ await checkpointManager.init(sessionId, fork.id);
1217
+ const taskGraph = new TaskGraph({
1218
+ sessionId,
1219
+ basePath: this.basePath,
1220
+ autoSave: true
1221
+ });
1222
+ await taskGraph.init();
1223
+ const initialSnapshot = await snapshotManager.create(
1224
+ {
1225
+ forkId: fork.id,
1226
+ sessionId,
1227
+ description: "Initial session snapshot"
1228
+ },
1229
+ fork.rpcUrl
1230
+ );
1231
+ const forkTask = {
1232
+ id: `fork-${fork.id}`,
1233
+ type: "fork",
1234
+ status: "complete",
1235
+ snapshotId: initialSnapshot.id,
1236
+ dependencies: [],
1237
+ input: { network, blockNumber: fork.blockNumber },
1238
+ output: { forkId: fork.id, rpcUrl: fork.rpcUrl },
1239
+ createdAt: /* @__PURE__ */ new Date(),
1240
+ completedAt: /* @__PURE__ */ new Date()
1241
+ };
1242
+ await taskGraph.addTask(forkTask);
1243
+ const metadata = {
1244
+ id: sessionId,
1245
+ network,
1246
+ forkId: fork.id,
1247
+ createdAt: /* @__PURE__ */ new Date(),
1248
+ lastActivity: /* @__PURE__ */ new Date(),
1249
+ status: "active",
1250
+ initialBlock: fork.blockNumber
1251
+ };
1252
+ this.sessions.set(sessionId, metadata);
1253
+ await this.saveSession(metadata);
1254
+ await this.saveSessionIndex();
1255
+ this.currentSession = {
1256
+ metadata,
1257
+ fork,
1258
+ forkManager,
1259
+ snapshotManager,
1260
+ checkpointManager,
1261
+ taskGraph
1262
+ };
1263
+ return this.currentSession;
1264
+ }
1265
+ /**
1266
+ * Resume an existing session
1267
+ *
1268
+ * @param sessionId - Session ID to resume
1269
+ * @returns Resumed session
1270
+ */
1271
+ async resume(sessionId) {
1272
+ const metadata = this.sessions.get(sessionId);
1273
+ if (!metadata) {
1274
+ throw new Error(`Session ${sessionId} not found`);
1275
+ }
1276
+ const forkManager = new ForkManager();
1277
+ await forkManager.init();
1278
+ const snapshotManager = new SnapshotManager();
1279
+ await snapshotManager.init(sessionId);
1280
+ const checkpointManager = new CheckpointManager();
1281
+ const taskGraph = new TaskGraph({
1282
+ sessionId,
1283
+ basePath: this.basePath,
1284
+ autoSave: true
1285
+ });
1286
+ await taskGraph.init();
1287
+ let fork = forkManager.get(metadata.forkId);
1288
+ if (!fork || taskGraph.hasBlocked()) {
1289
+ fork = await this.recover(
1290
+ sessionId,
1291
+ metadata,
1292
+ forkManager,
1293
+ snapshotManager,
1294
+ checkpointManager,
1295
+ taskGraph
1296
+ );
1297
+ }
1298
+ if (!fork) {
1299
+ throw new Error(`Failed to restore fork for session ${sessionId}`);
1300
+ }
1301
+ await checkpointManager.init(sessionId, fork.id);
1302
+ metadata.lastActivity = /* @__PURE__ */ new Date();
1303
+ metadata.forkId = fork.id;
1304
+ metadata.status = "active";
1305
+ await this.saveSession(metadata);
1306
+ this.currentSession = {
1307
+ metadata,
1308
+ fork,
1309
+ forkManager,
1310
+ snapshotManager,
1311
+ checkpointManager,
1312
+ taskGraph
1313
+ };
1314
+ return this.currentSession;
1315
+ }
1316
+ /**
1317
+ * Recover a session from checkpoint or snapshot
1318
+ */
1319
+ async recover(sessionId, metadata, forkManager, snapshotManager, checkpointManager, taskGraph) {
1320
+ const latestCheckpoint = checkpointManager.latest();
1321
+ if (latestCheckpoint) {
1322
+ console.log(`Recovering session ${sessionId} from checkpoint ${latestCheckpoint.id}`);
1323
+ return await checkpointManager.restore(
1324
+ latestCheckpoint.id,
1325
+ forkManager,
1326
+ metadata.network
1327
+ );
1328
+ }
1329
+ const blockedTasks = taskGraph.getTasksByStatus("blocked");
1330
+ const failedTasks = taskGraph.getTasksByStatus("failed");
1331
+ const problematicTask = blockedTasks[0] ?? failedTasks[0];
1332
+ if (problematicTask) {
1333
+ const recoveryPoint = taskGraph.findRecoveryPoint(problematicTask.id);
1334
+ if (recoveryPoint?.snapshotId) {
1335
+ const fork = await forkManager.fork({
1336
+ network: metadata.network,
1337
+ blockNumber: metadata.initialBlock,
1338
+ sessionId
1339
+ });
1340
+ const success = await snapshotManager.revert(fork.rpcUrl, recoveryPoint.snapshotId);
1341
+ if (!success) {
1342
+ throw new Error(`Failed to revert to snapshot ${recoveryPoint.snapshotId}`);
1343
+ }
1344
+ for (const task of [...blockedTasks, ...failedTasks]) {
1345
+ await taskGraph.updateStatus(task.id, "pending");
1346
+ }
1347
+ return fork;
1348
+ }
1349
+ }
1350
+ console.log(`No recovery point found, creating fresh fork for session ${sessionId}`);
1351
+ return await forkManager.fork({
1352
+ network: metadata.network,
1353
+ blockNumber: metadata.initialBlock,
1354
+ sessionId
1355
+ });
1356
+ }
1357
+ /**
1358
+ * Get current session
1359
+ *
1360
+ * @returns Current session or null
1361
+ */
1362
+ current() {
1363
+ return this.currentSession;
1364
+ }
1365
+ /**
1366
+ * List all sessions
1367
+ *
1368
+ * @param filter - Optional filter for status
1369
+ * @returns Array of session metadata
1370
+ */
1371
+ list(filter) {
1372
+ let sessions = Array.from(this.sessions.values());
1373
+ if (filter?.status) {
1374
+ sessions = sessions.filter((s) => s.status === filter.status);
1375
+ }
1376
+ return sessions.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime());
1377
+ }
1378
+ /**
1379
+ * Get session by ID
1380
+ *
1381
+ * @param sessionId - Session ID
1382
+ * @returns Session metadata if found
1383
+ */
1384
+ get(sessionId) {
1385
+ return this.sessions.get(sessionId);
1386
+ }
1387
+ /**
1388
+ * Update session status
1389
+ *
1390
+ * @param sessionId - Session ID
1391
+ * @param status - New status
1392
+ */
1393
+ async updateStatus(sessionId, status) {
1394
+ const metadata = this.sessions.get(sessionId);
1395
+ if (!metadata) {
1396
+ throw new Error(`Session ${sessionId} not found`);
1397
+ }
1398
+ metadata.status = status;
1399
+ metadata.lastActivity = /* @__PURE__ */ new Date();
1400
+ await this.saveSession(metadata);
1401
+ }
1402
+ /**
1403
+ * Get session directory path
1404
+ */
1405
+ getSessionDir(sessionId) {
1406
+ return join(this.basePath, sessionId);
1407
+ }
1408
+ /**
1409
+ * Get session metadata path
1410
+ */
1411
+ getSessionPath(sessionId) {
1412
+ return join(this.getSessionDir(sessionId), "session.json");
1413
+ }
1414
+ /**
1415
+ * Load session index
1416
+ */
1417
+ async loadSessionIndex() {
1418
+ if (!existsSync(this.basePath)) {
1419
+ return;
1420
+ }
1421
+ try {
1422
+ const entries = await readdir(this.basePath, { withFileTypes: true });
1423
+ for (const entry of entries) {
1424
+ if (!entry.isDirectory())
1425
+ continue;
1426
+ const sessionPath = this.getSessionPath(entry.name);
1427
+ if (!existsSync(sessionPath))
1428
+ continue;
1429
+ try {
1430
+ const content = await readFile(sessionPath, "utf-8");
1431
+ const data = JSON.parse(content);
1432
+ data.createdAt = new Date(data.createdAt);
1433
+ data.lastActivity = new Date(data.lastActivity);
1434
+ this.sessions.set(data.id, data);
1435
+ } catch {
1436
+ }
1437
+ }
1438
+ } catch {
1439
+ }
1440
+ }
1441
+ /**
1442
+ * Save session index
1443
+ */
1444
+ async saveSessionIndex() {
1445
+ if (!existsSync(this.basePath)) {
1446
+ await mkdir(this.basePath, { recursive: true });
1447
+ }
1448
+ const index = Array.from(this.sessions.values()).map((s) => ({
1449
+ id: s.id,
1450
+ status: s.status,
1451
+ lastActivity: s.lastActivity
1452
+ }));
1453
+ await writeFile(
1454
+ join(this.basePath, "index.json"),
1455
+ JSON.stringify(index, null, 2)
1456
+ );
1457
+ }
1458
+ /**
1459
+ * Save session metadata
1460
+ */
1461
+ async saveSession(metadata) {
1462
+ const dir = this.getSessionDir(metadata.id);
1463
+ if (!existsSync(dir)) {
1464
+ await mkdir(dir, { recursive: true });
1465
+ }
1466
+ await writeFile(this.getSessionPath(metadata.id), JSON.stringify(metadata, null, 2));
1467
+ }
1468
+ /**
1469
+ * Generate unique session ID
1470
+ */
1471
+ generateSessionId() {
1472
+ const timestamp = Date.now().toString(36);
1473
+ const random = Math.random().toString(36).substring(2, 6);
1474
+ return `session-${timestamp}-${random}`;
1475
+ }
1476
+ };
1477
+ var DEFAULT_PHYSICS_PATH = ".claude/rules/01-sigil-physics.md";
1478
+ var cachedPhysics = null;
1479
+ var cachedPath = null;
1480
+ function parseSyncStrategy(value) {
1481
+ const normalized = value.toLowerCase().trim();
1482
+ if (normalized === "pessimistic")
1483
+ return "pessimistic";
1484
+ if (normalized === "optimistic")
1485
+ return "optimistic";
1486
+ if (normalized === "immediate")
1487
+ return "immediate";
1488
+ return "optimistic";
1489
+ }
1490
+ function parseTiming(value) {
1491
+ const match = value.match(/(\d+)\s*ms/i);
1492
+ if (match && match[1]) {
1493
+ return parseInt(match[1], 10);
1494
+ }
1495
+ const num = parseInt(value, 10);
1496
+ return isNaN(num) ? 200 : num;
1497
+ }
1498
+ function parseConfirmation(value) {
1499
+ const normalized = value.toLowerCase().trim();
1500
+ if (normalized === "required" || normalized === "yes")
1501
+ return "required";
1502
+ if (normalized.includes("toast") || normalized.includes("undo"))
1503
+ return "toast_undo";
1504
+ if (normalized === "none" || normalized === "no")
1505
+ return "none";
1506
+ return "none";
1507
+ }
1508
+ function parseEffectType(value) {
1509
+ const normalized = value.toLowerCase().replace(/[\s-]/g, "_").trim();
1510
+ const mapping = {
1511
+ financial: "financial",
1512
+ destructive: "destructive",
1513
+ soft_delete: "soft_delete",
1514
+ "soft delete": "soft_delete",
1515
+ standard: "standard",
1516
+ navigation: "navigation",
1517
+ query: "query",
1518
+ local_state: "local",
1519
+ "local state": "local",
1520
+ local: "local",
1521
+ high_freq: "high_freq",
1522
+ "high-freq": "high_freq",
1523
+ highfreq: "high_freq"
1524
+ };
1525
+ return mapping[normalized] ?? null;
1526
+ }
1527
+ function parsePhysicsTable(content) {
1528
+ const physics = /* @__PURE__ */ new Map();
1529
+ const tableMatch = content.match(
1530
+ /<physics_table>[\s\S]*?\|[\s\S]*?<\/physics_table>/
1531
+ );
1532
+ if (!tableMatch) {
1533
+ console.warn("Physics table not found in content");
1534
+ return getDefaultPhysics();
1535
+ }
1536
+ const tableContent = tableMatch[0];
1537
+ const lines = tableContent.split("\n");
1538
+ for (const line of lines) {
1539
+ if (!line.includes("|") || line.includes("---") || line.includes("Effect")) {
1540
+ continue;
1541
+ }
1542
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
1543
+ if (cells.length < 4)
1544
+ continue;
1545
+ const effectStr = cells[0];
1546
+ const syncStr = cells[1];
1547
+ const timingStr = cells[2];
1548
+ const confirmStr = cells[3];
1549
+ const whyParts = cells.slice(4);
1550
+ if (!effectStr || !syncStr || !timingStr || !confirmStr)
1551
+ continue;
1552
+ const effect = parseEffectType(effectStr);
1553
+ if (!effect)
1554
+ continue;
1555
+ const rule = {
1556
+ effect,
1557
+ sync: parseSyncStrategy(syncStr),
1558
+ timing: parseTiming(timingStr),
1559
+ confirmation: parseConfirmation(confirmStr),
1560
+ rationale: whyParts.join(" ").trim()
1561
+ };
1562
+ physics.set(effect, rule);
1563
+ }
1564
+ return physics;
1565
+ }
1566
+ function getDefaultPhysics() {
1567
+ const physics = /* @__PURE__ */ new Map();
1568
+ physics.set("financial", {
1569
+ effect: "financial",
1570
+ sync: "pessimistic",
1571
+ timing: 800,
1572
+ confirmation: "required",
1573
+ rationale: "Money can't roll back. Users need time to verify."
1574
+ });
1575
+ physics.set("destructive", {
1576
+ effect: "destructive",
1577
+ sync: "pessimistic",
1578
+ timing: 600,
1579
+ confirmation: "required",
1580
+ rationale: "Permanent actions need deliberation."
1581
+ });
1582
+ physics.set("soft_delete", {
1583
+ effect: "soft_delete",
1584
+ sync: "optimistic",
1585
+ timing: 200,
1586
+ confirmation: "toast_undo",
1587
+ rationale: "Undo exists, so we can be fast."
1588
+ });
1589
+ physics.set("standard", {
1590
+ effect: "standard",
1591
+ sync: "optimistic",
1592
+ timing: 200,
1593
+ confirmation: "none",
1594
+ rationale: "Low stakes = snappy feedback."
1595
+ });
1596
+ physics.set("navigation", {
1597
+ effect: "navigation",
1598
+ sync: "immediate",
1599
+ timing: 150,
1600
+ confirmation: "none",
1601
+ rationale: "URL changes feel instant."
1602
+ });
1603
+ physics.set("query", {
1604
+ effect: "query",
1605
+ sync: "optimistic",
1606
+ timing: 150,
1607
+ confirmation: "none",
1608
+ rationale: "Data retrieval, no state change."
1609
+ });
1610
+ physics.set("local", {
1611
+ effect: "local",
1612
+ sync: "immediate",
1613
+ timing: 100,
1614
+ confirmation: "none",
1615
+ rationale: "No server = instant expected."
1616
+ });
1617
+ physics.set("high_freq", {
1618
+ effect: "high_freq",
1619
+ sync: "immediate",
1620
+ timing: 0,
1621
+ confirmation: "none",
1622
+ rationale: "Animation becomes friction."
1623
+ });
1624
+ return physics;
1625
+ }
1626
+ async function loadPhysics(path) {
1627
+ const physicsPath = path ?? DEFAULT_PHYSICS_PATH;
1628
+ if (cachedPhysics && cachedPath === physicsPath) {
1629
+ return cachedPhysics;
1630
+ }
1631
+ if (!existsSync(physicsPath)) {
1632
+ console.warn(`Physics file not found at ${physicsPath}, using defaults`);
1633
+ cachedPhysics = getDefaultPhysics();
1634
+ cachedPath = physicsPath;
1635
+ return cachedPhysics;
1636
+ }
1637
+ try {
1638
+ const content = await readFile(physicsPath, "utf-8");
1639
+ cachedPhysics = parsePhysicsTable(content);
1640
+ cachedPath = physicsPath;
1641
+ if (cachedPhysics.size === 0) {
1642
+ console.warn("No physics rules parsed, using defaults");
1643
+ cachedPhysics = getDefaultPhysics();
1644
+ }
1645
+ return cachedPhysics;
1646
+ } catch (error) {
1647
+ console.warn(`Error loading physics from ${physicsPath}:`, error);
1648
+ cachedPhysics = getDefaultPhysics();
1649
+ cachedPath = physicsPath;
1650
+ return cachedPhysics;
1651
+ }
1652
+ }
1653
+ var DEFAULT_VOCABULARY_PATH = ".claude/rules/08-sigil-lexicon.md";
1654
+ var cachedVocabulary = null;
1655
+ var cachedPath2 = null;
1656
+ function parseKeywordsFromBlock(block) {
1657
+ const keywords = [];
1658
+ const lines = block.split("\n");
1659
+ for (const line of lines) {
1660
+ const colonIndex = line.indexOf(":");
1661
+ const content = colonIndex >= 0 ? line.slice(colonIndex + 1) : line;
1662
+ const words = content.split(/[,\s]+/).map((w) => w.trim().toLowerCase()).filter((w) => w.length > 0 && !w.includes("```"));
1663
+ keywords.push(...words);
1664
+ }
1665
+ return [...new Set(keywords)];
1666
+ }
1667
+ function parseEffectKeywords(content) {
1668
+ const effects = /* @__PURE__ */ new Map();
1669
+ const sectionMatch = content.match(
1670
+ /<effect_keywords>[\s\S]*?<\/effect_keywords>/
1671
+ );
1672
+ if (!sectionMatch) {
1673
+ return effects;
1674
+ }
1675
+ const section = sectionMatch[0];
1676
+ const effectPatterns = [
1677
+ {
1678
+ effect: "financial",
1679
+ pattern: /###\s*Financial[\s\S]*?```([\s\S]*?)```/i
1680
+ },
1681
+ {
1682
+ effect: "destructive",
1683
+ pattern: /###\s*Destructive[\s\S]*?```([\s\S]*?)```/i
1684
+ },
1685
+ {
1686
+ effect: "soft_delete",
1687
+ pattern: /###\s*Soft\s*Delete[\s\S]*?```([\s\S]*?)```/i
1688
+ },
1689
+ {
1690
+ effect: "standard",
1691
+ pattern: /###\s*Standard[\s\S]*?```([\s\S]*?)```/i
1692
+ },
1693
+ {
1694
+ effect: "local",
1695
+ pattern: /###\s*Local\s*State[\s\S]*?```([\s\S]*?)```/i
1696
+ },
1697
+ {
1698
+ effect: "navigation",
1699
+ pattern: /###\s*Navigation[\s\S]*?```([\s\S]*?)```/i
1700
+ },
1701
+ {
1702
+ effect: "query",
1703
+ pattern: /###\s*Query[\s\S]*?```([\s\S]*?)```/i
1704
+ }
1705
+ ];
1706
+ for (const { effect, pattern } of effectPatterns) {
1707
+ const match = section.match(pattern);
1708
+ if (match && match[1]) {
1709
+ const keywords = parseKeywordsFromBlock(match[1]);
1710
+ if (keywords.length > 0) {
1711
+ effects.set(effect, {
1712
+ keywords,
1713
+ effect,
1714
+ category: "lexicon"
1715
+ });
1716
+ }
1717
+ }
1718
+ }
1719
+ return effects;
1720
+ }
1721
+ function parseTypeOverrides(content) {
1722
+ const overrides = /* @__PURE__ */ new Map();
1723
+ const sectionMatch = content.match(
1724
+ /<type_overrides>[\s\S]*?<\/type_overrides>/
1725
+ );
1726
+ if (!sectionMatch) {
1727
+ return overrides;
1728
+ }
1729
+ const section = sectionMatch[0];
1730
+ const lines = section.split("\n");
1731
+ for (const line of lines) {
1732
+ if (!line.includes("|") || line.includes("---") || line.includes("Type Pattern")) {
1733
+ continue;
1734
+ }
1735
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
1736
+ if (cells.length < 2)
1737
+ continue;
1738
+ const typePattern = cells[0];
1739
+ const forcedEffect = cells[1];
1740
+ if (!typePattern || !forcedEffect)
1741
+ continue;
1742
+ const types = typePattern.replace(/`/g, "").split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
1743
+ const effect = mapEffectString(forcedEffect);
1744
+ if (effect) {
1745
+ for (const type of types) {
1746
+ overrides.set(type, effect);
1747
+ }
1748
+ }
1749
+ }
1750
+ return overrides;
1751
+ }
1752
+ function parseDomainDefaults(content) {
1753
+ const defaults = /* @__PURE__ */ new Map();
1754
+ const sectionMatch = content.match(
1755
+ /<domain_context>[\s\S]*?<\/domain_context>/
1756
+ );
1757
+ if (!sectionMatch) {
1758
+ return defaults;
1759
+ }
1760
+ const section = sectionMatch[0];
1761
+ const domainHeaderPattern = /###\s*([\w\/]+)\s*\n```([\s\S]*?)```/gi;
1762
+ let match;
1763
+ while ((match = domainHeaderPattern.exec(section)) !== null) {
1764
+ const domainName = match[1];
1765
+ const domainContent = match[2];
1766
+ if (!domainName || !domainContent)
1767
+ continue;
1768
+ const defaultMatch = domainContent.match(/Default:\s*([\w\s()]+)/i);
1769
+ if (defaultMatch && defaultMatch[1]) {
1770
+ const effect = mapEffectString(defaultMatch[1]);
1771
+ if (effect) {
1772
+ const keywordMatch = domainContent.match(/Keywords:\s*([\w,\s]+)/i);
1773
+ if (keywordMatch && keywordMatch[1]) {
1774
+ const keywords = keywordMatch[1].split(",").map((k) => k.trim().toLowerCase()).filter((k) => k.length > 0);
1775
+ for (const keyword of keywords) {
1776
+ defaults.set(keyword, effect);
1777
+ }
1778
+ }
1779
+ defaults.set(domainName.toLowerCase().replace("/", "_"), effect);
1780
+ }
1781
+ }
1782
+ }
1783
+ return defaults;
1784
+ }
1785
+ function mapEffectString(value) {
1786
+ const normalized = value.toLowerCase().trim();
1787
+ if (normalized.includes("financial"))
1788
+ return "financial";
1789
+ if (normalized.includes("destructive"))
1790
+ return "destructive";
1791
+ if (normalized.includes("soft") && normalized.includes("delete"))
1792
+ return "soft_delete";
1793
+ if (normalized.includes("standard"))
1794
+ return "standard";
1795
+ if (normalized.includes("local"))
1796
+ return "local";
1797
+ if (normalized.includes("navigation"))
1798
+ return "navigation";
1799
+ if (normalized.includes("query"))
1800
+ return "query";
1801
+ if (normalized.includes("immediate"))
1802
+ return "local";
1803
+ return null;
1804
+ }
1805
+ function getDefaultVocabulary() {
1806
+ const effects = /* @__PURE__ */ new Map();
1807
+ effects.set("financial", {
1808
+ keywords: [
1809
+ "claim",
1810
+ "deposit",
1811
+ "withdraw",
1812
+ "transfer",
1813
+ "swap",
1814
+ "send",
1815
+ "pay",
1816
+ "purchase",
1817
+ "mint",
1818
+ "burn",
1819
+ "stake",
1820
+ "unstake",
1821
+ "bridge",
1822
+ "approve",
1823
+ "redeem",
1824
+ "harvest"
1825
+ ],
1826
+ effect: "financial",
1827
+ category: "default"
1828
+ });
1829
+ effects.set("destructive", {
1830
+ keywords: [
1831
+ "delete",
1832
+ "remove",
1833
+ "destroy",
1834
+ "revoke",
1835
+ "terminate",
1836
+ "purge",
1837
+ "erase",
1838
+ "wipe"
1839
+ ],
1840
+ effect: "destructive",
1841
+ category: "default"
1842
+ });
1843
+ effects.set("soft_delete", {
1844
+ keywords: ["archive", "hide", "trash", "dismiss", "snooze", "mute"],
1845
+ effect: "soft_delete",
1846
+ category: "default"
1847
+ });
1848
+ effects.set("standard", {
1849
+ keywords: [
1850
+ "save",
1851
+ "update",
1852
+ "edit",
1853
+ "create",
1854
+ "add",
1855
+ "like",
1856
+ "follow",
1857
+ "bookmark"
1858
+ ],
1859
+ effect: "standard",
1860
+ category: "default"
1861
+ });
1862
+ effects.set("local", {
1863
+ keywords: ["toggle", "switch", "expand", "collapse", "select", "focus"],
1864
+ effect: "local",
1865
+ category: "default"
1866
+ });
1867
+ effects.set("navigation", {
1868
+ keywords: ["navigate", "go", "back", "forward", "link", "route"],
1869
+ effect: "navigation",
1870
+ category: "default"
1871
+ });
1872
+ effects.set("query", {
1873
+ keywords: ["fetch", "load", "get", "list", "search", "find"],
1874
+ effect: "query",
1875
+ category: "default"
1876
+ });
1877
+ const typeOverrides = /* @__PURE__ */ new Map([
1878
+ ["currency", "financial"],
1879
+ ["money", "financial"],
1880
+ ["amount", "financial"],
1881
+ ["wei", "financial"],
1882
+ ["bigint", "financial"],
1883
+ ["token", "financial"],
1884
+ ["balance", "financial"],
1885
+ ["price", "financial"],
1886
+ ["fee", "financial"],
1887
+ ["password", "destructive"],
1888
+ ["secret", "destructive"],
1889
+ ["key", "destructive"],
1890
+ ["permission", "destructive"],
1891
+ ["role", "destructive"],
1892
+ ["access", "destructive"],
1893
+ ["theme", "local"],
1894
+ ["preference", "local"],
1895
+ ["setting", "local"],
1896
+ ["filter", "local"],
1897
+ ["sort", "local"],
1898
+ ["view", "local"]
1899
+ ]);
1900
+ const domainDefaults = /* @__PURE__ */ new Map([
1901
+ ["wallet", "financial"],
1902
+ ["token", "financial"],
1903
+ ["nft", "financial"],
1904
+ ["contract", "financial"],
1905
+ ["chain", "financial"],
1906
+ ["gas", "financial"],
1907
+ ["cart", "standard"],
1908
+ ["checkout", "financial"],
1909
+ ["payment", "financial"]
1910
+ ]);
1911
+ return { effects, typeOverrides, domainDefaults };
1912
+ }
1913
+ async function loadVocabulary(path) {
1914
+ const vocabPath = path ?? DEFAULT_VOCABULARY_PATH;
1915
+ if (cachedVocabulary && cachedPath2 === vocabPath) {
1916
+ return cachedVocabulary;
1917
+ }
1918
+ if (!existsSync(vocabPath)) {
1919
+ console.warn(`Vocabulary file not found at ${vocabPath}, using defaults`);
1920
+ cachedVocabulary = getDefaultVocabulary();
1921
+ cachedPath2 = vocabPath;
1922
+ return cachedVocabulary;
1923
+ }
1924
+ try {
1925
+ const content = await readFile(vocabPath, "utf-8");
1926
+ const effects = parseEffectKeywords(content);
1927
+ const typeOverrides = parseTypeOverrides(content);
1928
+ const domainDefaults = parseDomainDefaults(content);
1929
+ if (effects.size === 0) {
1930
+ console.warn("No vocabulary parsed, using defaults");
1931
+ cachedVocabulary = getDefaultVocabulary();
1932
+ } else {
1933
+ cachedVocabulary = { effects, typeOverrides, domainDefaults };
1934
+ }
1935
+ cachedPath2 = vocabPath;
1936
+ return cachedVocabulary;
1937
+ } catch (error) {
1938
+ console.warn(`Error loading vocabulary from ${vocabPath}:`, error);
1939
+ cachedVocabulary = getDefaultVocabulary();
1940
+ cachedPath2 = vocabPath;
1941
+ return cachedVocabulary;
1942
+ }
1943
+ }
1944
+ async function resolveEffectFromKeywords(keywords, vocabulary) {
1945
+ const vocab = vocabulary ?? await loadVocabulary();
1946
+ const normalizedKeywords = keywords.map((k) => k.toLowerCase().trim());
1947
+ const priorityOrder = [
1948
+ "financial",
1949
+ "destructive",
1950
+ "soft_delete",
1951
+ "standard",
1952
+ "local",
1953
+ "navigation",
1954
+ "query",
1955
+ "high_freq"
1956
+ ];
1957
+ for (const effect of priorityOrder) {
1958
+ const entry = vocab.effects.get(effect);
1959
+ if (entry) {
1960
+ for (const keyword of normalizedKeywords) {
1961
+ if (entry.keywords.includes(keyword)) {
1962
+ return effect;
1963
+ }
1964
+ }
1965
+ }
1966
+ }
1967
+ for (const keyword of normalizedKeywords) {
1968
+ const override = vocab.typeOverrides.get(keyword);
1969
+ if (override) {
1970
+ return override;
1971
+ }
1972
+ }
1973
+ for (const keyword of normalizedKeywords) {
1974
+ const domainDefault = vocab.domainDefaults.get(keyword);
1975
+ if (domainDefault) {
1976
+ return domainDefault;
1977
+ }
1978
+ }
1979
+ return null;
1980
+ }
1981
+
1982
+ // src/warden/grounding-gate.ts
1983
+ var ZONE_TO_EFFECT = {
1984
+ critical: "financial",
1985
+ elevated: "destructive",
1986
+ standard: "standard",
1987
+ local: "local"
1988
+ };
1989
+ var EFFECT_TO_ZONE = {
1990
+ financial: "critical",
1991
+ destructive: "elevated",
1992
+ soft_delete: "standard",
1993
+ standard: "standard",
1994
+ navigation: "local",
1995
+ query: "local",
1996
+ local: "local",
1997
+ high_freq: "local"
1998
+ };
1999
+ function parseZone(value) {
2000
+ const normalized = value.toLowerCase().trim();
2001
+ if (normalized === "critical")
2002
+ return "critical";
2003
+ if (normalized === "elevated")
2004
+ return "elevated";
2005
+ if (normalized === "standard")
2006
+ return "standard";
2007
+ if (normalized === "local")
2008
+ return "local";
2009
+ return null;
2010
+ }
2011
+ function parseSyncStrategy2(value) {
2012
+ const normalized = value.toLowerCase().trim();
2013
+ if (normalized.includes("pessimistic"))
2014
+ return "pessimistic";
2015
+ if (normalized.includes("optimistic"))
2016
+ return "optimistic";
2017
+ if (normalized.includes("immediate"))
2018
+ return "immediate";
2019
+ return void 0;
2020
+ }
2021
+ function parseTiming2(value) {
2022
+ const match = value.match(/(\d+)\s*ms/i);
2023
+ if (match && match[1]) {
2024
+ return parseInt(match[1], 10);
2025
+ }
2026
+ return void 0;
2027
+ }
2028
+ function parseConfirmation2(value) {
2029
+ const normalized = value.toLowerCase().trim();
2030
+ if (normalized.includes("required") || normalized === "yes")
2031
+ return "required";
2032
+ if (normalized.includes("toast") || normalized.includes("undo"))
2033
+ return "toast_undo";
2034
+ if (normalized.includes("none") || normalized === "no")
2035
+ return "none";
2036
+ return void 0;
2037
+ }
2038
+ function extractKeywords(text) {
2039
+ const cleanedText = text.replace(/Zone:\s*\w+/gi, "").replace(/Effect:\s*[\w\s]+/gi, "").replace(/Sync:\s*\w+/gi, "").replace(/Confirmation:\s*[\w\s+]+/gi, "");
2040
+ const keywordPatterns = [
2041
+ // Financial
2042
+ "claim",
2043
+ "deposit",
2044
+ "withdraw",
2045
+ "transfer",
2046
+ "swap",
2047
+ "send",
2048
+ "pay",
2049
+ "purchase",
2050
+ "mint",
2051
+ "burn",
2052
+ "stake",
2053
+ "unstake",
2054
+ "bridge",
2055
+ "approve",
2056
+ "redeem",
2057
+ "harvest",
2058
+ // Destructive
2059
+ "delete",
2060
+ "remove",
2061
+ "destroy",
2062
+ "revoke",
2063
+ "terminate",
2064
+ "purge",
2065
+ "erase",
2066
+ "wipe",
2067
+ // Soft delete
2068
+ "archive",
2069
+ "hide",
2070
+ "trash",
2071
+ "dismiss",
2072
+ "snooze",
2073
+ "mute",
2074
+ // Standard
2075
+ "save",
2076
+ "update",
2077
+ "edit",
2078
+ "create",
2079
+ "add",
2080
+ "like",
2081
+ "follow",
2082
+ "bookmark",
2083
+ // Local
2084
+ "toggle",
2085
+ "switch",
2086
+ "expand",
2087
+ "collapse",
2088
+ "select",
2089
+ "focus",
2090
+ // Navigation
2091
+ "navigate",
2092
+ "go",
2093
+ "back",
2094
+ "forward",
2095
+ "link",
2096
+ "route",
2097
+ // Query
2098
+ "fetch",
2099
+ "load",
2100
+ "get",
2101
+ "list",
2102
+ "search",
2103
+ "find",
2104
+ // Domain/type hints
2105
+ "wallet",
2106
+ "token",
2107
+ "nft",
2108
+ "contract",
2109
+ "chain",
2110
+ "gas",
2111
+ "currency",
2112
+ "money",
2113
+ "amount",
2114
+ "balance",
2115
+ "price",
2116
+ "fee",
2117
+ // Effect type names (for inline detection in prose)
2118
+ "financial",
2119
+ "destructive"
2120
+ ];
2121
+ const words = cleanedText.toLowerCase().split(/[\s,.:;!?()\[\]{}]+/);
2122
+ const foundKeywords = [];
2123
+ for (const word of words) {
2124
+ if (keywordPatterns.includes(word)) {
2125
+ foundKeywords.push(word);
2126
+ }
2127
+ }
2128
+ return [...new Set(foundKeywords)];
2129
+ }
2130
+ function parseGroundingStatement(text) {
2131
+ const statement = {
2132
+ component: "",
2133
+ citedZone: null,
2134
+ detectedKeywords: [],
2135
+ inferredEffect: null,
2136
+ claimedPhysics: {},
2137
+ raw: text
2138
+ };
2139
+ const componentMatch = text.match(/(?:Component|Button|Modal|Form|Dialog):\s*["']?([^\s"'│|]+)/i);
2140
+ if (componentMatch && componentMatch[1]) {
2141
+ statement.component = componentMatch[1].trim();
2142
+ } else {
2143
+ const buttonMatch = text.match(/["']?(\w+(?:Button|Modal|Form|Dialog|Card|Input))["']?/);
2144
+ if (buttonMatch && buttonMatch[1]) {
2145
+ statement.component = buttonMatch[1];
2146
+ }
2147
+ }
2148
+ const zoneMatch = text.match(/Zone:\s*(\w+)/i);
2149
+ if (zoneMatch && zoneMatch[1]) {
2150
+ statement.citedZone = parseZone(zoneMatch[1]);
2151
+ }
2152
+ const effectMatch = text.match(/Effect:\s*(\w+(?:\s+\w+)?)/i);
2153
+ if (effectMatch && effectMatch[1]) {
2154
+ const effectStr = effectMatch[1].toLowerCase();
2155
+ if (effectStr.includes("financial"))
2156
+ statement.inferredEffect = "financial";
2157
+ else if (effectStr.includes("destructive"))
2158
+ statement.inferredEffect = "destructive";
2159
+ else if (effectStr.includes("soft"))
2160
+ statement.inferredEffect = "soft_delete";
2161
+ else if (effectStr.includes("standard"))
2162
+ statement.inferredEffect = "standard";
2163
+ else if (effectStr.includes("local"))
2164
+ statement.inferredEffect = "local";
2165
+ else if (effectStr.includes("navigation"))
2166
+ statement.inferredEffect = "navigation";
2167
+ else if (effectStr.includes("query"))
2168
+ statement.inferredEffect = "query";
2169
+ }
2170
+ const syncMatch = text.match(/Sync:\s*(\w+)/i);
2171
+ if (syncMatch && syncMatch[1]) {
2172
+ const parsed = parseSyncStrategy2(syncMatch[1]);
2173
+ if (parsed)
2174
+ statement.claimedPhysics.sync = parsed;
2175
+ } else {
2176
+ if (text.toLowerCase().includes("pessimistic")) {
2177
+ statement.claimedPhysics.sync = "pessimistic";
2178
+ } else if (text.toLowerCase().includes("optimistic")) {
2179
+ statement.claimedPhysics.sync = "optimistic";
2180
+ } else if (text.toLowerCase().includes("immediate")) {
2181
+ statement.claimedPhysics.sync = "immediate";
2182
+ }
2183
+ }
2184
+ const timingMatch = text.match(/Timing:\s*(\d+\s*ms)/i);
2185
+ if (timingMatch && timingMatch[1]) {
2186
+ const parsed = parseTiming2(timingMatch[1]);
2187
+ if (parsed !== void 0)
2188
+ statement.claimedPhysics.timing = parsed;
2189
+ } else {
2190
+ const inlineTiming = text.match(/(\d+)\s*ms/);
2191
+ if (inlineTiming && inlineTiming[1]) {
2192
+ statement.claimedPhysics.timing = parseInt(inlineTiming[1], 10);
2193
+ }
2194
+ }
2195
+ const confirmMatch = text.match(/Confirm(?:ation)?:\s*(\w+(?:\s*\+\s*\w+)?)/i);
2196
+ if (confirmMatch && confirmMatch[1]) {
2197
+ const parsed = parseConfirmation2(confirmMatch[1]);
2198
+ if (parsed)
2199
+ statement.claimedPhysics.confirmation = parsed;
2200
+ } else {
2201
+ if (text.toLowerCase().includes("confirmation required")) {
2202
+ statement.claimedPhysics.confirmation = "required";
2203
+ } else if (text.toLowerCase().includes("toast") && text.toLowerCase().includes("undo")) {
2204
+ statement.claimedPhysics.confirmation = "toast_undo";
2205
+ }
2206
+ }
2207
+ statement.detectedKeywords = extractKeywords(text);
2208
+ return statement;
2209
+ }
2210
+ function determineRequiredZone(keywords, effect, vocabulary) {
2211
+ if (effect) {
2212
+ return EFFECT_TO_ZONE[effect];
2213
+ }
2214
+ for (const keyword of keywords) {
2215
+ const override = vocabulary.typeOverrides.get(keyword.toLowerCase());
2216
+ if (override) {
2217
+ return EFFECT_TO_ZONE[override];
2218
+ }
2219
+ }
2220
+ const financialKeywords = vocabulary.effects.get("financial")?.keywords ?? [];
2221
+ for (const keyword of keywords) {
2222
+ if (financialKeywords.includes(keyword.toLowerCase())) {
2223
+ return "critical";
2224
+ }
2225
+ }
2226
+ const destructiveKeywords = vocabulary.effects.get("destructive")?.keywords ?? [];
2227
+ for (const keyword of keywords) {
2228
+ if (destructiveKeywords.includes(keyword.toLowerCase())) {
2229
+ return "elevated";
2230
+ }
2231
+ }
2232
+ return "standard";
2233
+ }
2234
+ function checkRelevance(statement, vocabulary) {
2235
+ const { detectedKeywords, component } = statement;
2236
+ if (detectedKeywords.length === 0) {
2237
+ const componentLower = component.toLowerCase();
2238
+ const allKeywords = [];
2239
+ for (const entry of vocabulary.effects.values()) {
2240
+ allKeywords.push(...entry.keywords);
2241
+ }
2242
+ const hasRelevantComponent = allKeywords.some((k) => componentLower.includes(k));
2243
+ if (!hasRelevantComponent) {
2244
+ return {
2245
+ passed: false,
2246
+ reason: "No relevant keywords detected in statement or component name"
2247
+ };
2248
+ }
2249
+ }
2250
+ return {
2251
+ passed: true,
2252
+ reason: `Keywords detected: ${detectedKeywords.join(", ") || "from component name"}`
2253
+ };
2254
+ }
2255
+ function checkHierarchy(statement, requiredZone) {
2256
+ const { citedZone } = statement;
2257
+ if (!citedZone) {
2258
+ return {
2259
+ passed: false,
2260
+ reason: "No zone cited in statement"
2261
+ };
2262
+ }
2263
+ const requiredIndex = ZONE_HIERARCHY.indexOf(requiredZone);
2264
+ const citedIndex = ZONE_HIERARCHY.indexOf(citedZone);
2265
+ if (citedIndex > requiredIndex) {
2266
+ return {
2267
+ passed: false,
2268
+ reason: `Zone "${citedZone}" is less restrictive than required "${requiredZone}"`
2269
+ };
2270
+ }
2271
+ if (citedIndex < requiredIndex) {
2272
+ return {
2273
+ passed: true,
2274
+ reason: `Zone "${citedZone}" is more restrictive than required "${requiredZone}" (OK)`
2275
+ };
2276
+ }
2277
+ return {
2278
+ passed: true,
2279
+ reason: `Zone "${citedZone}" matches required zone`
2280
+ };
2281
+ }
2282
+ async function checkRules(statement, requiredZone, physics) {
2283
+ const { claimedPhysics } = statement;
2284
+ const requiredEffect = ZONE_TO_EFFECT[requiredZone];
2285
+ const rule = physics.get(requiredEffect);
2286
+ if (!rule) {
2287
+ return {
2288
+ passed: false,
2289
+ reason: `No physics rule found for effect "${requiredEffect}"`
2290
+ };
2291
+ }
2292
+ const violations = [];
2293
+ if (claimedPhysics.sync && claimedPhysics.sync !== rule.sync) {
2294
+ if (claimedPhysics.sync !== "pessimistic") {
2295
+ violations.push(`Sync: claimed "${claimedPhysics.sync}", required "${rule.sync}"`);
2296
+ }
2297
+ }
2298
+ if (claimedPhysics.timing !== void 0) {
2299
+ const timingDiff = Math.abs(claimedPhysics.timing - rule.timing);
2300
+ if (timingDiff > 100 && claimedPhysics.timing < rule.timing) {
2301
+ violations.push(
2302
+ `Timing: claimed ${claimedPhysics.timing}ms, required minimum ${rule.timing}ms`
2303
+ );
2304
+ }
2305
+ }
2306
+ if (claimedPhysics.confirmation) {
2307
+ if (rule.confirmation === "required" && claimedPhysics.confirmation !== "required") {
2308
+ violations.push(
2309
+ `Confirmation: claimed "${claimedPhysics.confirmation}", required "${rule.confirmation}"`
2310
+ );
2311
+ }
2312
+ }
2313
+ if (violations.length > 0) {
2314
+ return {
2315
+ passed: false,
2316
+ reason: violations.join("; ")
2317
+ };
2318
+ }
2319
+ return {
2320
+ passed: true,
2321
+ reason: "Physics rules validated"
2322
+ };
2323
+ }
2324
+ async function validateGrounding(input, options) {
2325
+ const physics = await loadPhysics(options?.physicsPath);
2326
+ const vocabulary = await loadVocabulary(options?.vocabularyPath);
2327
+ const statement = typeof input === "string" ? parseGroundingStatement(input) : input;
2328
+ if (!statement.inferredEffect && statement.detectedKeywords.length > 0) {
2329
+ statement.inferredEffect = await resolveEffectFromKeywords(
2330
+ statement.detectedKeywords,
2331
+ vocabulary
2332
+ );
2333
+ }
2334
+ const requiredZone = determineRequiredZone(
2335
+ statement.detectedKeywords,
2336
+ statement.inferredEffect,
2337
+ vocabulary
2338
+ );
2339
+ const relevanceCheck = checkRelevance(statement, vocabulary);
2340
+ const hierarchyCheck = checkHierarchy(statement, requiredZone);
2341
+ const rulesCheck = await checkRules(statement, requiredZone, physics);
2342
+ let status = "VALID";
2343
+ let correction;
2344
+ if (!relevanceCheck.passed) {
2345
+ status = "DRIFT";
2346
+ correction = "Statement lacks relevant keywords for effect detection.";
2347
+ } else if (!hierarchyCheck.passed) {
2348
+ status = "DECEPTIVE";
2349
+ correction = `Zone mismatch: cited "${statement.citedZone}", required "${requiredZone}".`;
2350
+ } else if (!rulesCheck.passed) {
2351
+ status = "DRIFT";
2352
+ correction = `Physics violation: ${rulesCheck.reason}`;
2353
+ }
2354
+ return {
2355
+ status,
2356
+ checks: {
2357
+ relevance: relevanceCheck,
2358
+ hierarchy: hierarchyCheck,
2359
+ rules: rulesCheck
2360
+ },
2361
+ requiredZone,
2362
+ citedZone: statement.citedZone,
2363
+ ...correction !== void 0 && { correction }
2364
+ };
2365
+ }
2366
+ function getExitCode(result) {
2367
+ switch (result.status) {
2368
+ case "VALID":
2369
+ return 0;
2370
+ case "DRIFT":
2371
+ return 1;
2372
+ case "DECEPTIVE":
2373
+ return 2;
2374
+ default:
2375
+ return 3;
2376
+ }
2377
+ }
2378
+
2379
+ // src/warden/adversarial-warden.ts
2380
+ var ZONE_RELEVANCE = {
2381
+ critical: [
2382
+ "button",
2383
+ "form",
2384
+ "modal",
2385
+ "dialog",
2386
+ "action",
2387
+ "input",
2388
+ "transaction",
2389
+ "payment",
2390
+ "claim",
2391
+ "withdraw",
2392
+ "deposit",
2393
+ "transfer",
2394
+ "swap",
2395
+ "stake",
2396
+ "mint",
2397
+ "burn"
2398
+ ],
2399
+ elevated: [
2400
+ "button",
2401
+ "form",
2402
+ "modal",
2403
+ "dialog",
2404
+ "action",
2405
+ "delete",
2406
+ "remove",
2407
+ "revoke",
2408
+ "terminate",
2409
+ "confirmation"
2410
+ ],
2411
+ standard: [
2412
+ "button",
2413
+ "form",
2414
+ "modal",
2415
+ "dialog",
2416
+ "action",
2417
+ "input",
2418
+ "card",
2419
+ "list",
2420
+ "table",
2421
+ "save",
2422
+ "edit",
2423
+ "create",
2424
+ "update"
2425
+ ],
2426
+ local: [
2427
+ "toggle",
2428
+ "switch",
2429
+ "checkbox",
2430
+ "radio",
2431
+ "select",
2432
+ "dropdown",
2433
+ "tooltip",
2434
+ "accordion",
2435
+ "tab",
2436
+ "theme",
2437
+ "preference",
2438
+ "filter",
2439
+ "sort"
2440
+ ]
2441
+ };
2442
+ var AdversarialWarden = class {
2443
+ learnedRules = [];
2444
+ /**
2445
+ * Add a learned rule to the warden
2446
+ *
2447
+ * @param rule - The learned rule to add
2448
+ */
2449
+ addLearnedRule(rule) {
2450
+ this.learnedRules.push(rule);
2451
+ }
2452
+ /**
2453
+ * Clear all learned rules
2454
+ */
2455
+ clearLearnedRules() {
2456
+ this.learnedRules = [];
2457
+ }
2458
+ /**
2459
+ * Get all learned rules
2460
+ */
2461
+ getLearnedRules() {
2462
+ return [...this.learnedRules];
2463
+ }
2464
+ /**
2465
+ * Check if cited zone is relevant to the component type
2466
+ *
2467
+ * Critical zone should only be cited for buttons, forms, modals, actions
2468
+ * Citing critical for a tooltip is suspicious
2469
+ *
2470
+ * @param citedZone - The zone cited by the agent
2471
+ * @param componentName - The component name
2472
+ * @returns Check result
2473
+ */
2474
+ checkRelevance(citedZone, componentName) {
2475
+ const componentLower = componentName.toLowerCase();
2476
+ const relevantTypes = ZONE_RELEVANCE[citedZone];
2477
+ const isRelevant = relevantTypes.some(
2478
+ (type) => componentLower.includes(type)
2479
+ );
2480
+ if (!isRelevant) {
2481
+ const zoneIndex = ZONE_HIERARCHY.indexOf(citedZone);
2482
+ if (zoneIndex <= 1) {
2483
+ return {
2484
+ passed: false,
2485
+ reason: `Zone "${citedZone}" is unnecessarily restrictive for component "${componentName}"`
2486
+ };
2487
+ }
2488
+ }
2489
+ return {
2490
+ passed: true,
2491
+ reason: `Zone "${citedZone}" is appropriate for component "${componentName}"`
2492
+ };
2493
+ }
2494
+ /**
2495
+ * Check learned rules against a grounding statement
2496
+ *
2497
+ * @param statement - Parsed grounding statement
2498
+ * @returns Check result with missing rule citations
2499
+ */
2500
+ checkLearnedRules(statement) {
2501
+ const missingRules = [];
2502
+ for (const rule of this.learnedRules) {
2503
+ if (!rule.grounding_requirement?.must_cite)
2504
+ continue;
2505
+ const triggerMatches = this.checkRuleTrigger(rule, statement);
2506
+ if (!triggerMatches)
2507
+ continue;
2508
+ const citationPresent = this.checkCitationPresent(rule, statement);
2509
+ if (!citationPresent) {
2510
+ missingRules.push(rule.id);
2511
+ }
2512
+ }
2513
+ if (missingRules.length > 0) {
2514
+ return {
2515
+ passed: false,
2516
+ reason: `Missing required rule citations: ${missingRules.join(", ")}`
2517
+ };
2518
+ }
2519
+ return {
2520
+ passed: true,
2521
+ reason: "All required rule citations present"
2522
+ };
2523
+ }
2524
+ /**
2525
+ * Check if a rule's trigger matches the statement
2526
+ */
2527
+ checkRuleTrigger(rule, statement) {
2528
+ const trigger = rule.rule.trigger;
2529
+ if (trigger.component_name_contains) {
2530
+ const matches = trigger.component_name_contains.some(
2531
+ (pattern) => statement.component.toLowerCase().includes(pattern.toLowerCase())
2532
+ );
2533
+ if (!matches)
2534
+ return false;
2535
+ }
2536
+ if (trigger.zone) {
2537
+ if (statement.citedZone !== trigger.zone)
2538
+ return false;
2539
+ }
2540
+ if (trigger.effect && statement.inferredEffect !== trigger.effect) {
2541
+ return false;
2542
+ }
2543
+ return true;
2544
+ }
2545
+ /**
2546
+ * Check if required citation is present in statement
2547
+ */
2548
+ checkCitationPresent(rule, statement) {
2549
+ const mustCite = rule.grounding_requirement?.must_cite;
2550
+ if (!mustCite)
2551
+ return true;
2552
+ const rawLower = statement.raw.toLowerCase();
2553
+ if (mustCite.zone && statement.citedZone !== mustCite.zone) {
2554
+ return false;
2555
+ }
2556
+ if (mustCite.physics) {
2557
+ for (const physics of mustCite.physics) {
2558
+ if (!rawLower.includes(physics.toLowerCase())) {
2559
+ return false;
2560
+ }
2561
+ }
2562
+ }
2563
+ return true;
2564
+ }
2565
+ /**
2566
+ * Run full adversarial validation
2567
+ *
2568
+ * @param input - Raw text or parsed statement
2569
+ * @param options - Validation options
2570
+ * @returns Extended warden result with adversarial checks
2571
+ */
2572
+ async validate(input, options) {
2573
+ const statement = typeof input === "string" ? parseGroundingStatement(input) : input;
2574
+ const baseResult = await validateGrounding(statement, options);
2575
+ const relevanceCheck = statement.component ? this.checkRelevance(
2576
+ statement.citedZone ?? "standard",
2577
+ statement.component
2578
+ ) : { passed: true, reason: "No component to check" };
2579
+ const learnedRulesCheck = this.checkLearnedRules(statement);
2580
+ let status = baseResult.status;
2581
+ let correction = baseResult.correction;
2582
+ if (!relevanceCheck.passed && status === "VALID") {
2583
+ status = "DRIFT";
2584
+ correction = relevanceCheck.reason;
2585
+ }
2586
+ if (!learnedRulesCheck.passed && status === "VALID") {
2587
+ status = "DRIFT";
2588
+ correction = learnedRulesCheck.reason;
2589
+ }
2590
+ return {
2591
+ ...baseResult,
2592
+ status,
2593
+ ...correction !== void 0 && { correction },
2594
+ adversarialChecks: {
2595
+ relevance: relevanceCheck,
2596
+ learnedRules: learnedRulesCheck
2597
+ }
2598
+ };
2599
+ }
2600
+ };
2601
+ function getHierarchyDescription() {
2602
+ return ZONE_HIERARCHY.map((zone, i) => {
2603
+ const restrictiveness = i === 0 ? "most restrictive" : i === ZONE_HIERARCHY.length - 1 ? "least restrictive" : "";
2604
+ return `${zone}${restrictiveness ? ` (${restrictiveness})` : ""}`;
2605
+ }).join(" > ");
2606
+ }
2607
+
2608
+ // src/warden/lens-validator.ts
2609
+ var ZONE_SEVERITY = {
2610
+ critical: "error",
2611
+ elevated: "error",
2612
+ standard: "warning",
2613
+ local: "info"
2614
+ };
2615
+ function checkDataSourceMismatch(context, zone) {
2616
+ const { observedValue, onChainValue, component } = context;
2617
+ if (observedValue === void 0 || onChainValue === void 0) {
2618
+ return null;
2619
+ }
2620
+ const normalizedObserved = normalizeValue(observedValue);
2621
+ const normalizedOnChain = normalizeValue(onChainValue);
2622
+ if (normalizedObserved !== normalizedOnChain) {
2623
+ const severity = zone ? ZONE_SEVERITY[zone] : "warning";
2624
+ return {
2625
+ type: "data_source_mismatch",
2626
+ severity,
2627
+ message: `Displayed value "${observedValue}" doesn't match on-chain value "${onChainValue}"`,
2628
+ component,
2629
+ ...zone !== void 0 && { zone },
2630
+ expected: onChainValue,
2631
+ actual: observedValue,
2632
+ suggestion: "Use on-chain data source for accuracy, or add refresh mechanism"
2633
+ };
2634
+ }
2635
+ return null;
2636
+ }
2637
+ function checkStaleIndexedData(context, zone) {
2638
+ const { indexedValue, onChainValue, component } = context;
2639
+ if (indexedValue === void 0 || onChainValue === void 0) {
2640
+ return null;
2641
+ }
2642
+ const normalizedIndexed = normalizeValue(indexedValue);
2643
+ const normalizedOnChain = normalizeValue(onChainValue);
2644
+ if (normalizedIndexed !== normalizedOnChain) {
2645
+ const severity = zone ? ZONE_SEVERITY[zone] : "warning";
2646
+ return {
2647
+ type: "stale_indexed_data",
2648
+ severity,
2649
+ message: `Indexed value "${indexedValue}" doesn't match on-chain value "${onChainValue}"`,
2650
+ component,
2651
+ ...zone !== void 0 && { zone },
2652
+ expected: onChainValue,
2653
+ actual: indexedValue,
2654
+ suggestion: "Consider using on-chain reads for critical data or reducing indexer lag"
2655
+ };
2656
+ }
2657
+ return null;
2658
+ }
2659
+ function checkLensFinancialSource(context, zone) {
2660
+ const { dataSource, component } = context;
2661
+ if (zone !== "critical") {
2662
+ return null;
2663
+ }
2664
+ if (dataSource === "indexed" || dataSource === "mixed") {
2665
+ return {
2666
+ type: "lens_financial_check",
2667
+ severity: "error",
2668
+ message: `Financial component "${component}" uses ${dataSource} data source`,
2669
+ component,
2670
+ zone,
2671
+ suggestion: "Financial operations must use on-chain data for transaction amounts"
2672
+ };
2673
+ }
2674
+ return null;
2675
+ }
2676
+ function checkImpersonationLeak(context) {
2677
+ const { impersonatedAddress, realAddress, observedValue, component } = context;
2678
+ if (!realAddress || !observedValue) {
2679
+ return null;
2680
+ }
2681
+ const normalizedReal = realAddress.toLowerCase();
2682
+ const normalizedObserved = observedValue.toLowerCase();
2683
+ if (normalizedObserved.includes(normalizedReal) && !normalizedObserved.includes(impersonatedAddress.toLowerCase())) {
2684
+ return {
2685
+ type: "impersonation_leak",
2686
+ severity: "error",
2687
+ message: `Component "${component}" shows real address instead of impersonated address`,
2688
+ component,
2689
+ expected: impersonatedAddress,
2690
+ actual: observedValue,
2691
+ suggestion: "Use lens-aware account hook to get the correct address context"
2692
+ };
2693
+ }
2694
+ return null;
2695
+ }
2696
+ function normalizeValue(value) {
2697
+ let normalized = value.trim();
2698
+ if (normalized.endsWith("n")) {
2699
+ normalized = normalized.slice(0, -1);
2700
+ }
2701
+ if (normalized.startsWith("0x")) {
2702
+ normalized = normalized.toLowerCase();
2703
+ }
2704
+ if (normalized.includes(".")) {
2705
+ normalized = normalized.replace(/\.?0+$/, "");
2706
+ }
2707
+ return normalized;
2708
+ }
2709
+ function validateLensContext(context, zone) {
2710
+ const issues = [];
2711
+ const dataSourceMismatch = checkDataSourceMismatch(context, zone);
2712
+ if (dataSourceMismatch)
2713
+ issues.push(dataSourceMismatch);
2714
+ const staleIndexed = checkStaleIndexedData(context, zone);
2715
+ if (staleIndexed)
2716
+ issues.push(staleIndexed);
2717
+ const financialSource = checkLensFinancialSource(context, zone);
2718
+ if (financialSource)
2719
+ issues.push(financialSource);
2720
+ const impersonationLeak = checkImpersonationLeak(context);
2721
+ if (impersonationLeak)
2722
+ issues.push(impersonationLeak);
2723
+ const hasErrors = issues.some((issue) => issue.severity === "error");
2724
+ const hasWarnings = issues.some((issue) => issue.severity === "warning");
2725
+ let summary;
2726
+ if (issues.length === 0) {
2727
+ summary = "All lens validation checks passed";
2728
+ } else if (hasErrors) {
2729
+ const errorCount = issues.filter((i) => i.severity === "error").length;
2730
+ summary = `${errorCount} error(s) found in lens validation`;
2731
+ } else if (hasWarnings) {
2732
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
2733
+ summary = `${warningCount} warning(s) found in lens validation`;
2734
+ } else {
2735
+ summary = `${issues.length} informational issue(s) found`;
2736
+ }
2737
+ return {
2738
+ valid: !hasErrors,
2739
+ issues,
2740
+ summary
2741
+ };
2742
+ }
2743
+ function getLensExitCode(result) {
2744
+ if (result.valid && result.issues.length === 0) {
2745
+ return LensExitCode.PASS;
2746
+ }
2747
+ const hasErrors = result.issues.some((issue) => issue.severity === "error");
2748
+ if (hasErrors) {
2749
+ return LensExitCode.LENS_ERROR;
2750
+ }
2751
+ const hasWarnings = result.issues.some((issue) => issue.severity === "warning");
2752
+ if (hasWarnings) {
2753
+ return LensExitCode.LENS_WARNING;
2754
+ }
2755
+ return LensExitCode.PASS;
2756
+ }
2757
+
2758
+ // src/cli/index.ts
2759
+ var VERSION = "4.3.1";
2760
+ var NETWORKS = {
2761
+ mainnet: {
2762
+ name: "mainnet",
2763
+ chainId: 1,
2764
+ rpcUrl: process.env["ETH_RPC_URL"] ?? "https://eth.llamarpc.com"
2765
+ },
2766
+ sepolia: {
2767
+ name: "sepolia",
2768
+ chainId: 11155111,
2769
+ rpcUrl: process.env["SEPOLIA_RPC_URL"] ?? "https://rpc.sepolia.org"
2770
+ },
2771
+ arbitrum: {
2772
+ name: "arbitrum",
2773
+ chainId: 42161,
2774
+ rpcUrl: process.env["ARBITRUM_RPC_URL"] ?? "https://arb1.arbitrum.io/rpc"
2775
+ },
2776
+ optimism: {
2777
+ name: "optimism",
2778
+ chainId: 10,
2779
+ rpcUrl: process.env["OPTIMISM_RPC_URL"] ?? "https://mainnet.optimism.io"
2780
+ },
2781
+ polygon: {
2782
+ name: "polygon",
2783
+ chainId: 137,
2784
+ rpcUrl: process.env["POLYGON_RPC_URL"] ?? "https://polygon-rpc.com"
2785
+ },
2786
+ base: {
2787
+ name: "base",
2788
+ chainId: 8453,
2789
+ rpcUrl: process.env["BASE_RPC_URL"] ?? "https://mainnet.base.org"
2790
+ }
2791
+ };
2792
+ var program = new Command();
2793
+ program.name("anchor").description("Ground truth enforcement for Sigil design physics").version(VERSION);
2794
+ program.command("fork <network>").description("Spawn an Anvil fork of the specified network").option("-b, --block <number>", "Block number to fork at").option("-p, --port <number>", "RPC port (auto-assigned if not specified)").option("-s, --session <id>", "Session ID to associate with fork").option("--rpc-url <url>", "Custom RPC URL (overrides network default)").action(async (networkName, options) => {
2795
+ const manager = new ForkManager();
2796
+ await manager.init();
2797
+ let network = NETWORKS[networkName.toLowerCase()];
2798
+ if (!network) {
2799
+ if (options.rpcUrl) {
2800
+ network = {
2801
+ name: networkName,
2802
+ chainId: 1,
2803
+ // Will be detected from RPC
2804
+ rpcUrl: options.rpcUrl
2805
+ };
2806
+ } else {
2807
+ console.error(`Unknown network: ${networkName}`);
2808
+ console.error(`Available networks: ${Object.keys(NETWORKS).join(", ")}`);
2809
+ console.error("Or provide a custom RPC URL with --rpc-url");
2810
+ process.exit(1);
2811
+ }
2812
+ }
2813
+ if (options.rpcUrl) {
2814
+ network = { ...network, rpcUrl: options.rpcUrl };
2815
+ }
2816
+ try {
2817
+ console.log(`Forking ${network.name}...`);
2818
+ const fork = await manager.fork({
2819
+ network,
2820
+ ...options.block !== void 0 && { blockNumber: parseInt(options.block, 10) },
2821
+ ...options.port !== void 0 && { port: parseInt(options.port, 10) },
2822
+ ...options.session !== void 0 && { sessionId: options.session }
2823
+ });
2824
+ console.log("");
2825
+ console.log("Fork created successfully:");
2826
+ console.log("");
2827
+ console.log(` ID: ${fork.id}`);
2828
+ console.log(` Network: ${fork.network.name}`);
2829
+ console.log(` Chain ID: ${fork.network.chainId}`);
2830
+ console.log(` Block: ${fork.blockNumber}`);
2831
+ console.log(` RPC URL: ${fork.rpcUrl}`);
2832
+ console.log(` PID: ${fork.pid}`);
2833
+ console.log("");
2834
+ console.log("Environment variables:");
2835
+ const env = manager.exportEnv(fork.id);
2836
+ for (const [key, value] of Object.entries(env)) {
2837
+ console.log(` export ${key}=${value}`);
2838
+ }
2839
+ } catch (error) {
2840
+ console.error("Failed to create fork:", error instanceof Error ? error.message : error);
2841
+ process.exit(1);
2842
+ }
2843
+ });
2844
+ program.command("forks").description("List all active forks").option("--json", "Output as JSON").action(async (options) => {
2845
+ const manager = new ForkManager();
2846
+ await manager.init();
2847
+ const forks = manager.list();
2848
+ if (options.json) {
2849
+ console.log(JSON.stringify(forks, null, 2));
2850
+ return;
2851
+ }
2852
+ if (forks.length === 0) {
2853
+ console.log("No active forks.");
2854
+ return;
2855
+ }
2856
+ console.log("Active forks:");
2857
+ console.log("");
2858
+ for (const fork of forks) {
2859
+ const age = Math.round((Date.now() - fork.createdAt.getTime()) / 1e3 / 60);
2860
+ console.log(` ${fork.id}`);
2861
+ console.log(` Network: ${fork.network.name} (chain ${fork.network.chainId})`);
2862
+ console.log(` Block: ${fork.blockNumber}`);
2863
+ console.log(` RPC: ${fork.rpcUrl}`);
2864
+ console.log(` Age: ${age} minutes`);
2865
+ if (fork.sessionId) {
2866
+ console.log(` Session: ${fork.sessionId}`);
2867
+ }
2868
+ console.log("");
2869
+ }
2870
+ });
2871
+ program.command("kill <fork-id>").description("Kill a specific fork").action(async (forkId) => {
2872
+ const manager = new ForkManager();
2873
+ await manager.init();
2874
+ const fork = manager.get(forkId);
2875
+ if (!fork) {
2876
+ console.error(`Fork not found: ${forkId}`);
2877
+ process.exit(1);
2878
+ }
2879
+ await manager.kill(forkId);
2880
+ console.log(`Fork ${forkId} terminated.`);
2881
+ });
2882
+ program.command("kill-all").description("Kill all active forks").action(async () => {
2883
+ const manager = new ForkManager();
2884
+ await manager.init();
2885
+ const forks = manager.list();
2886
+ if (forks.length === 0) {
2887
+ console.log("No active forks to kill.");
2888
+ return;
2889
+ }
2890
+ await manager.killAll();
2891
+ console.log(`Killed ${forks.length} fork(s).`);
2892
+ });
2893
+ program.command("env <fork-id>").description("Export environment variables for a fork").option("--format <type>", "Output format: shell, fish, or json", "shell").action(async (forkId, options) => {
2894
+ const manager = new ForkManager();
2895
+ await manager.init();
2896
+ try {
2897
+ const env = manager.exportEnv(forkId);
2898
+ switch (options.format) {
2899
+ case "json":
2900
+ console.log(JSON.stringify(env, null, 2));
2901
+ break;
2902
+ case "fish":
2903
+ for (const [key, value] of Object.entries(env)) {
2904
+ console.log(`set -x ${key} ${value}`);
2905
+ }
2906
+ break;
2907
+ case "shell":
2908
+ default:
2909
+ for (const [key, value] of Object.entries(env)) {
2910
+ console.log(`export ${key}=${value}`);
2911
+ }
2912
+ break;
2913
+ }
2914
+ } catch (error) {
2915
+ console.error(error instanceof Error ? error.message : error);
2916
+ process.exit(1);
2917
+ }
2918
+ });
2919
+ program.command("snapshot").description("Create a new EVM snapshot").requiredOption("-f, --fork <id>", "Fork ID to snapshot").requiredOption("-s, --session <id>", "Session ID").option("-t, --task <id>", "Task ID to associate with snapshot").option("-d, --description <text>", "Description of the snapshot").action(async (options) => {
2920
+ const forkManager = new ForkManager();
2921
+ await forkManager.init();
2922
+ const fork = forkManager.get(options.fork);
2923
+ if (!fork) {
2924
+ console.error(`Fork not found: ${options.fork}`);
2925
+ process.exit(1);
2926
+ }
2927
+ const snapshotManager = new SnapshotManager();
2928
+ await snapshotManager.init(options.session);
2929
+ try {
2930
+ const snapshot = await snapshotManager.create(
2931
+ {
2932
+ forkId: options.fork,
2933
+ sessionId: options.session,
2934
+ ...options.task !== void 0 && { taskId: options.task },
2935
+ ...options.description !== void 0 && { description: options.description }
2936
+ },
2937
+ fork.rpcUrl
2938
+ );
2939
+ console.log("Snapshot created:");
2940
+ console.log("");
2941
+ console.log(` ID: ${snapshot.id}`);
2942
+ console.log(` Block: ${snapshot.blockNumber}`);
2943
+ console.log(` Session: ${snapshot.sessionId}`);
2944
+ if (snapshot.taskId) {
2945
+ console.log(` Task: ${snapshot.taskId}`);
2946
+ }
2947
+ if (snapshot.description) {
2948
+ console.log(` Description: ${snapshot.description}`);
2949
+ }
2950
+ } catch (error) {
2951
+ console.error("Failed to create snapshot:", error instanceof Error ? error.message : error);
2952
+ process.exit(1);
2953
+ }
2954
+ });
2955
+ program.command("revert <snapshot-id>").description("Revert to a previous EVM snapshot").requiredOption("-f, --fork <id>", "Fork ID to revert").requiredOption("-s, --session <id>", "Session ID").action(async (snapshotId, options) => {
2956
+ const forkManager = new ForkManager();
2957
+ await forkManager.init();
2958
+ const fork = forkManager.get(options.fork);
2959
+ if (!fork) {
2960
+ console.error(`Fork not found: ${options.fork}`);
2961
+ process.exit(1);
2962
+ }
2963
+ const snapshotManager = new SnapshotManager();
2964
+ await snapshotManager.init(options.session);
2965
+ const snapshot = snapshotManager.get(snapshotId);
2966
+ if (!snapshot) {
2967
+ console.error(`Snapshot not found: ${snapshotId}`);
2968
+ process.exit(1);
2969
+ }
2970
+ try {
2971
+ const success = await snapshotManager.revert(fork.rpcUrl, snapshotId);
2972
+ if (success) {
2973
+ console.log(`Reverted to snapshot ${snapshotId}`);
2974
+ console.log(` Block: ${snapshot.blockNumber}`);
2975
+ } else {
2976
+ console.error("Revert failed");
2977
+ process.exit(1);
2978
+ }
2979
+ } catch (error) {
2980
+ console.error("Failed to revert:", error instanceof Error ? error.message : error);
2981
+ process.exit(1);
2982
+ }
2983
+ });
2984
+ program.command("snapshots").description("List all snapshots for a session").requiredOption("-s, --session <id>", "Session ID").option("--json", "Output as JSON").action(async (options) => {
2985
+ const snapshotManager = new SnapshotManager();
2986
+ await snapshotManager.init(options.session);
2987
+ const snapshots = snapshotManager.list();
2988
+ if (options.json) {
2989
+ console.log(JSON.stringify(snapshots, null, 2));
2990
+ return;
2991
+ }
2992
+ if (snapshots.length === 0) {
2993
+ console.log("No snapshots for this session.");
2994
+ return;
2995
+ }
2996
+ console.log(`Snapshots for session ${options.session}:`);
2997
+ console.log("");
2998
+ for (const snap of snapshots) {
2999
+ const age = Math.round((Date.now() - snap.createdAt.getTime()) / 1e3 / 60);
3000
+ console.log(` ${snap.id}`);
3001
+ console.log(` Block: ${snap.blockNumber}`);
3002
+ console.log(` Fork: ${snap.forkId}`);
3003
+ console.log(` Age: ${age} minutes`);
3004
+ if (snap.taskId) {
3005
+ console.log(` Task: ${snap.taskId}`);
3006
+ }
3007
+ console.log("");
3008
+ }
3009
+ });
3010
+ program.command("session <network>").description("Create a new Anchor session").option("-b, --block <number>", "Block number to fork at").option("--rpc-url <url>", "Custom RPC URL (overrides network default)").action(async (networkName, options) => {
3011
+ let network = NETWORKS[networkName.toLowerCase()];
3012
+ if (!network) {
3013
+ if (options.rpcUrl) {
3014
+ network = {
3015
+ name: networkName,
3016
+ chainId: 1,
3017
+ rpcUrl: options.rpcUrl
3018
+ };
3019
+ } else {
3020
+ console.error(`Unknown network: ${networkName}`);
3021
+ console.error(`Available networks: ${Object.keys(NETWORKS).join(", ")}`);
3022
+ process.exit(1);
3023
+ }
3024
+ }
3025
+ if (options.rpcUrl) {
3026
+ network = { ...network, rpcUrl: options.rpcUrl };
3027
+ }
3028
+ const sessionManager = new SessionManager();
3029
+ await sessionManager.init();
3030
+ try {
3031
+ console.log(`Creating session on ${network.name}...`);
3032
+ const session = await sessionManager.create(
3033
+ network,
3034
+ options.block ? { blockNumber: parseInt(options.block, 10) } : void 0
3035
+ );
3036
+ console.log("");
3037
+ console.log("Session created:");
3038
+ console.log("");
3039
+ console.log(` Session ID: ${session.metadata.id}`);
3040
+ console.log(` Network: ${session.metadata.network.name}`);
3041
+ console.log(` Fork ID: ${session.fork.id}`);
3042
+ console.log(` Block: ${session.fork.blockNumber}`);
3043
+ console.log(` RPC URL: ${session.fork.rpcUrl}`);
3044
+ console.log(` Status: ${session.metadata.status}`);
3045
+ console.log("");
3046
+ console.log("To resume this session later:");
3047
+ console.log(` anchor resume ${session.metadata.id}`);
3048
+ } catch (error) {
3049
+ console.error("Failed to create session:", error instanceof Error ? error.message : error);
3050
+ process.exit(1);
3051
+ }
3052
+ });
3053
+ program.command("sessions").description("List all sessions").option("--status <status>", "Filter by status (active, suspended, complete, failed)").option("--json", "Output as JSON").action(async (options) => {
3054
+ const sessionManager = new SessionManager();
3055
+ await sessionManager.init();
3056
+ const filter = options.status ? { status: options.status } : void 0;
3057
+ const sessions = sessionManager.list(filter);
3058
+ if (options.json) {
3059
+ console.log(JSON.stringify(sessions, null, 2));
3060
+ return;
3061
+ }
3062
+ if (sessions.length === 0) {
3063
+ console.log("No sessions found.");
3064
+ return;
3065
+ }
3066
+ console.log("Sessions:");
3067
+ console.log("");
3068
+ for (const session of sessions) {
3069
+ const age = Math.round((Date.now() - session.lastActivity.getTime()) / 1e3 / 60);
3070
+ console.log(` ${session.id}`);
3071
+ console.log(` Network: ${session.network.name}`);
3072
+ console.log(` Status: ${session.status}`);
3073
+ console.log(` Block: ${session.initialBlock}`);
3074
+ console.log(` Age: ${age} minutes`);
3075
+ console.log("");
3076
+ }
3077
+ });
3078
+ program.command("resume <session-id>").description("Resume an existing session").action(async (sessionId) => {
3079
+ const sessionManager = new SessionManager();
3080
+ await sessionManager.init();
3081
+ try {
3082
+ console.log(`Resuming session ${sessionId}...`);
3083
+ const session = await sessionManager.resume(sessionId);
3084
+ console.log("");
3085
+ console.log("Session resumed:");
3086
+ console.log("");
3087
+ console.log(` Session ID: ${session.metadata.id}`);
3088
+ console.log(` Network: ${session.metadata.network.name}`);
3089
+ console.log(` Fork ID: ${session.fork.id}`);
3090
+ console.log(` Block: ${session.fork.blockNumber}`);
3091
+ console.log(` RPC URL: ${session.fork.rpcUrl}`);
3092
+ console.log(` Status: ${session.metadata.status}`);
3093
+ const pending = session.taskGraph.getTasksByStatus("pending").length;
3094
+ const running = session.taskGraph.getTasksByStatus("running").length;
3095
+ const complete = session.taskGraph.getTasksByStatus("complete").length;
3096
+ const blocked = session.taskGraph.getTasksByStatus("blocked").length;
3097
+ console.log("");
3098
+ console.log("Task Graph:");
3099
+ console.log(` Pending: ${pending}`);
3100
+ console.log(` Running: ${running}`);
3101
+ console.log(` Complete: ${complete}`);
3102
+ console.log(` Blocked: ${blocked}`);
3103
+ } catch (error) {
3104
+ console.error("Failed to resume session:", error instanceof Error ? error.message : error);
3105
+ process.exit(1);
3106
+ }
3107
+ });
3108
+ program.command("status <session-id>").description("Show session status").option("--json", "Output as JSON").action(async (sessionId, options) => {
3109
+ const sessionManager = new SessionManager();
3110
+ await sessionManager.init();
3111
+ const metadata = sessionManager.get(sessionId);
3112
+ if (!metadata) {
3113
+ console.error(`Session not found: ${sessionId}`);
3114
+ process.exit(1);
3115
+ }
3116
+ if (options.json) {
3117
+ console.log(JSON.stringify(metadata, null, 2));
3118
+ return;
3119
+ }
3120
+ console.log("Session Status:");
3121
+ console.log("");
3122
+ console.log(` ID: ${metadata.id}`);
3123
+ console.log(` Network: ${metadata.network.name}`);
3124
+ console.log(` Status: ${metadata.status}`);
3125
+ console.log(` Initial Block: ${metadata.initialBlock}`);
3126
+ console.log(` Current Fork: ${metadata.forkId}`);
3127
+ console.log(` Created: ${metadata.createdAt.toISOString()}`);
3128
+ console.log(` Last Activity: ${metadata.lastActivity.toISOString()}`);
3129
+ });
3130
+ program.command("graph <session-id>").description("Show task graph for a session").option("--json", "Output as JSON").action(async (sessionId, options) => {
3131
+ const taskGraph = new TaskGraph({ sessionId, autoSave: false });
3132
+ await taskGraph.init();
3133
+ const data = taskGraph.toJSON();
3134
+ if (options.json) {
3135
+ console.log(JSON.stringify(data, null, 2));
3136
+ return;
3137
+ }
3138
+ if (data.tasks.length === 0) {
3139
+ console.log("No tasks in graph.");
3140
+ return;
3141
+ }
3142
+ console.log(`Task Graph for session ${sessionId}:`);
3143
+ console.log("");
3144
+ const byStatus = /* @__PURE__ */ new Map();
3145
+ for (const task of data.tasks) {
3146
+ const existing = byStatus.get(task.status) ?? [];
3147
+ existing.push(task);
3148
+ byStatus.set(task.status, existing);
3149
+ }
3150
+ const statusOrder = ["running", "pending", "complete", "blocked", "failed"];
3151
+ for (const status of statusOrder) {
3152
+ const tasks = byStatus.get(status);
3153
+ if (!tasks || tasks.length === 0)
3154
+ continue;
3155
+ console.log(` ${status.toUpperCase()} (${tasks.length}):`);
3156
+ for (const task of tasks) {
3157
+ console.log(` ${task.id}`);
3158
+ console.log(` Type: ${task.type}`);
3159
+ if (task.snapshotId) {
3160
+ console.log(` Snapshot: ${task.snapshotId}`);
3161
+ }
3162
+ if (task.dependencies.length > 0) {
3163
+ console.log(` Dependencies: ${task.dependencies.join(", ")}`);
3164
+ }
3165
+ }
3166
+ console.log("");
3167
+ }
3168
+ });
3169
+ program.command("checkpoint <session-id>").description("Create a checkpoint for a session").requiredOption("-f, --fork <id>", "Fork ID to checkpoint").action(async (sessionId, options) => {
3170
+ const forkManager = new ForkManager();
3171
+ await forkManager.init();
3172
+ const fork = forkManager.get(options.fork);
3173
+ if (!fork) {
3174
+ console.error(`Fork not found: ${options.fork}`);
3175
+ process.exit(1);
3176
+ }
3177
+ const checkpointManager = new CheckpointManager();
3178
+ await checkpointManager.init(sessionId, options.fork);
3179
+ try {
3180
+ console.log("Creating checkpoint...");
3181
+ const checkpoint = await checkpointManager.create(fork.rpcUrl);
3182
+ console.log("");
3183
+ console.log("Checkpoint created:");
3184
+ console.log("");
3185
+ console.log(` ID: ${checkpoint.id}`);
3186
+ console.log(` Session: ${checkpoint.sessionId}`);
3187
+ console.log(` Fork: ${checkpoint.forkId}`);
3188
+ console.log(` Block: ${checkpoint.blockNumber}`);
3189
+ console.log(` Snapshot Range: ${checkpoint.snapshotRange.first} - ${checkpoint.snapshotRange.last}`);
3190
+ console.log(` Snapshot Count: ${checkpoint.snapshotCount}`);
3191
+ } catch (error) {
3192
+ console.error("Failed to create checkpoint:", error instanceof Error ? error.message : error);
3193
+ process.exit(1);
3194
+ }
3195
+ });
3196
+ program.command("checkpoints <session-id>").description("List checkpoints for a session").option("--json", "Output as JSON").action(async (sessionId, options) => {
3197
+ const checkpointManager = new CheckpointManager();
3198
+ await checkpointManager.init(sessionId, "list-only");
3199
+ const checkpoints = checkpointManager.list();
3200
+ if (options.json) {
3201
+ console.log(JSON.stringify(checkpoints, null, 2));
3202
+ return;
3203
+ }
3204
+ if (checkpoints.length === 0) {
3205
+ console.log("No checkpoints for this session.");
3206
+ return;
3207
+ }
3208
+ console.log(`Checkpoints for session ${sessionId}:`);
3209
+ console.log("");
3210
+ for (const cp of checkpoints) {
3211
+ const age = Math.round((Date.now() - cp.createdAt.getTime()) / 1e3 / 60);
3212
+ console.log(` ${cp.id}`);
3213
+ console.log(` Block: ${cp.blockNumber}`);
3214
+ console.log(` Fork: ${cp.forkId}`);
3215
+ console.log(` Snapshot Range: ${cp.snapshotRange.first} - ${cp.snapshotRange.last}`);
3216
+ console.log(` Age: ${age} minutes`);
3217
+ console.log("");
3218
+ }
3219
+ });
3220
+ program.command("restore <checkpoint-id>").description("Restore session from a checkpoint").requiredOption("-s, --session <id>", "Session ID").action(async (checkpointId, options) => {
3221
+ const sessionManager = new SessionManager();
3222
+ await sessionManager.init();
3223
+ const metadata = sessionManager.get(options.session);
3224
+ if (!metadata) {
3225
+ console.error(`Session not found: ${options.session}`);
3226
+ process.exit(1);
3227
+ }
3228
+ const forkManager = new ForkManager();
3229
+ await forkManager.init();
3230
+ const checkpointManager = new CheckpointManager();
3231
+ await checkpointManager.init(options.session, metadata.forkId);
3232
+ const checkpoint = checkpointManager.get(checkpointId);
3233
+ if (!checkpoint) {
3234
+ console.error(`Checkpoint not found: ${checkpointId}`);
3235
+ process.exit(1);
3236
+ }
3237
+ try {
3238
+ console.log(`Restoring from checkpoint ${checkpointId}...`);
3239
+ const fork = await checkpointManager.restore(checkpointId, forkManager, metadata.network);
3240
+ console.log("");
3241
+ console.log("Restored successfully:");
3242
+ console.log("");
3243
+ console.log(` New Fork ID: ${fork.id}`);
3244
+ console.log(` Block: ${fork.blockNumber}`);
3245
+ console.log(` RPC URL: ${fork.rpcUrl}`);
3246
+ } catch (error) {
3247
+ console.error("Failed to restore:", error instanceof Error ? error.message : error);
3248
+ process.exit(1);
3249
+ }
3250
+ });
3251
+ program.command("validate").description("Validate a grounding statement against Sigil physics").option("-f, --file <path>", "Read statement from file").option("-t, --text <statement>", "Statement text to validate").option("--physics <path>", "Path to physics rules file").option("--vocabulary <path>", "Path to vocabulary file").option("--lens <json>", "Lens context JSON for data source validation").option("--lens-file <path>", "Read lens context from JSON file").option("--zone <zone>", "Zone for lens validation (critical, elevated, standard, local)").option("--json", "Output as JSON").option("--exit-code", "Exit with validation status code").action(async (options) => {
3252
+ let statement;
3253
+ let lensContext;
3254
+ let zone;
3255
+ if (options.zone) {
3256
+ const validZones = ["critical", "elevated", "standard", "local"];
3257
+ if (!validZones.includes(options.zone)) {
3258
+ console.error(`Invalid zone: ${options.zone}. Must be one of: ${validZones.join(", ")}`);
3259
+ process.exit(1);
3260
+ }
3261
+ zone = options.zone;
3262
+ }
3263
+ if (options.lens) {
3264
+ try {
3265
+ const parsed = JSON.parse(options.lens);
3266
+ const validated = LensContextSchema.safeParse(parsed);
3267
+ if (!validated.success) {
3268
+ console.error("Invalid lens context:", validated.error.message);
3269
+ process.exit(1);
3270
+ }
3271
+ lensContext = validated.data;
3272
+ } catch {
3273
+ console.error("Failed to parse lens context JSON");
3274
+ process.exit(1);
3275
+ }
3276
+ } else if (options.lensFile) {
3277
+ try {
3278
+ const content = await readFile(options.lensFile, "utf-8");
3279
+ const parsed = JSON.parse(content);
3280
+ const validated = LensContextSchema.safeParse(parsed);
3281
+ if (!validated.success) {
3282
+ console.error("Invalid lens context:", validated.error.message);
3283
+ process.exit(1);
3284
+ }
3285
+ lensContext = validated.data;
3286
+ } catch {
3287
+ console.error(`Failed to read lens context file: ${options.lensFile}`);
3288
+ process.exit(1);
3289
+ }
3290
+ }
3291
+ if (options.file) {
3292
+ try {
3293
+ statement = await readFile(options.file, "utf-8");
3294
+ } catch {
3295
+ console.error(`Failed to read file: ${options.file}`);
3296
+ process.exit(1);
3297
+ }
3298
+ } else if (options.text) {
3299
+ statement = options.text;
3300
+ }
3301
+ if (!statement && !lensContext) {
3302
+ console.error("Provide a statement with -f (file) or -t (text), or lens context with --lens or --lens-file");
3303
+ process.exit(1);
3304
+ }
3305
+ try {
3306
+ const validateOptions = {};
3307
+ if (options.physics)
3308
+ validateOptions.physicsPath = options.physics;
3309
+ if (options.vocabulary)
3310
+ validateOptions.vocabularyPath = options.vocabulary;
3311
+ let groundingResult;
3312
+ if (statement) {
3313
+ groundingResult = await validateGrounding(statement, validateOptions);
3314
+ }
3315
+ let lensResult;
3316
+ if (lensContext) {
3317
+ const effectiveZone = zone ?? groundingResult?.requiredZone ?? "standard";
3318
+ lensResult = validateLensContext(lensContext, effectiveZone);
3319
+ }
3320
+ const combinedResult = {
3321
+ ...groundingResult && {
3322
+ status: groundingResult.status,
3323
+ checks: groundingResult.checks,
3324
+ requiredZone: groundingResult.requiredZone,
3325
+ citedZone: groundingResult.citedZone,
3326
+ correction: groundingResult.correction
3327
+ },
3328
+ ...lensResult && {
3329
+ lens_validation: {
3330
+ valid: lensResult.valid,
3331
+ issues: lensResult.issues,
3332
+ summary: lensResult.summary
3333
+ }
3334
+ }
3335
+ };
3336
+ if (options.json) {
3337
+ console.log(JSON.stringify(combinedResult, null, 2));
3338
+ } else {
3339
+ if (groundingResult) {
3340
+ const statusEmoji = groundingResult.status === "VALID" ? "\u2713" : groundingResult.status === "DRIFT" ? "\u26A0" : "\u2717";
3341
+ console.log(`${statusEmoji} Status: ${groundingResult.status}`);
3342
+ console.log("");
3343
+ console.log("Grounding Checks:");
3344
+ console.log(` Relevance: ${groundingResult.checks.relevance.passed ? "\u2713" : "\u2717"} ${groundingResult.checks.relevance.reason}`);
3345
+ console.log(` Hierarchy: ${groundingResult.checks.hierarchy.passed ? "\u2713" : "\u2717"} ${groundingResult.checks.hierarchy.reason}`);
3346
+ console.log(` Rules: ${groundingResult.checks.rules.passed ? "\u2713" : "\u2717"} ${groundingResult.checks.rules.reason}`);
3347
+ console.log("");
3348
+ console.log(`Required Zone: ${groundingResult.requiredZone}`);
3349
+ console.log(`Cited Zone: ${groundingResult.citedZone ?? "(none)"}`);
3350
+ if (groundingResult.correction) {
3351
+ console.log("");
3352
+ console.log(`Correction: ${groundingResult.correction}`);
3353
+ }
3354
+ }
3355
+ if (lensResult) {
3356
+ if (groundingResult)
3357
+ console.log("");
3358
+ const lensEmoji = lensResult.valid ? "\u2713" : "\u2717";
3359
+ console.log(`${lensEmoji} Lens Validation: ${lensResult.summary}`);
3360
+ if (lensResult.issues.length > 0) {
3361
+ console.log("");
3362
+ console.log("Lens Issues:");
3363
+ for (const issue of lensResult.issues) {
3364
+ const severityEmoji = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
3365
+ console.log(` ${severityEmoji} [${issue.type}] ${issue.message}`);
3366
+ if (issue.suggestion) {
3367
+ console.log(` \u2192 ${issue.suggestion}`);
3368
+ }
3369
+ }
3370
+ }
3371
+ }
3372
+ }
3373
+ if (options.exitCode) {
3374
+ let exitCode = 0;
3375
+ if (groundingResult) {
3376
+ exitCode = Math.max(exitCode, getExitCode(groundingResult));
3377
+ }
3378
+ if (lensResult) {
3379
+ exitCode = Math.max(exitCode, getLensExitCode(lensResult));
3380
+ }
3381
+ process.exit(exitCode);
3382
+ }
3383
+ } catch (error) {
3384
+ console.error("Validation error:", error instanceof Error ? error.message : error);
3385
+ process.exit(1);
3386
+ }
3387
+ });
3388
+ program.command("physics").description("Show loaded physics rules").option("-p, --path <path>", "Path to physics file").option("--json", "Output as JSON").action(async (options) => {
3389
+ try {
3390
+ const physics = await loadPhysics(options.path);
3391
+ if (options.json) {
3392
+ const obj = {};
3393
+ for (const [key, value] of physics) {
3394
+ obj[key] = value;
3395
+ }
3396
+ console.log(JSON.stringify(obj, null, 2));
3397
+ return;
3398
+ }
3399
+ console.log("Physics Rules:");
3400
+ console.log("");
3401
+ console.log(" Effect Sync Timing Confirmation");
3402
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3403
+ for (const [effect, rule] of physics) {
3404
+ const effectPad = effect.padEnd(14);
3405
+ const syncPad = rule.sync.padEnd(12);
3406
+ const timingPad = `${rule.timing}ms`.padEnd(7);
3407
+ console.log(` ${effectPad} ${syncPad} ${timingPad} ${rule.confirmation}`);
3408
+ }
3409
+ } catch (error) {
3410
+ console.error("Failed to load physics:", error instanceof Error ? error.message : error);
3411
+ process.exit(1);
3412
+ }
3413
+ });
3414
+ program.command("vocabulary").description("Show loaded vocabulary").option("-p, --path <path>", "Path to vocabulary file").option("--json", "Output as JSON").action(async (options) => {
3415
+ try {
3416
+ const vocabulary = await loadVocabulary(options.path);
3417
+ if (options.json) {
3418
+ const obj = {
3419
+ effects: Object.fromEntries(vocabulary.effects),
3420
+ typeOverrides: Object.fromEntries(vocabulary.typeOverrides),
3421
+ domainDefaults: Object.fromEntries(vocabulary.domainDefaults)
3422
+ };
3423
+ console.log(JSON.stringify(obj, null, 2));
3424
+ return;
3425
+ }
3426
+ console.log("Vocabulary:");
3427
+ console.log("");
3428
+ console.log("Effect Keywords:");
3429
+ for (const [effect, entry] of vocabulary.effects) {
3430
+ console.log(` ${effect}: ${entry.keywords.slice(0, 8).join(", ")}${entry.keywords.length > 8 ? "..." : ""}`);
3431
+ }
3432
+ console.log("");
3433
+ console.log("Type Overrides:");
3434
+ for (const [type, effect] of vocabulary.typeOverrides) {
3435
+ console.log(` ${type} \u2192 ${effect}`);
3436
+ }
3437
+ } catch (error) {
3438
+ console.error("Failed to load vocabulary:", error instanceof Error ? error.message : error);
3439
+ process.exit(1);
3440
+ }
3441
+ });
3442
+ program.command("warden").description("Run adversarial warden validation").option("-f, --file <path>", "Read statement from file").option("-t, --text <statement>", "Statement text to test").option("--hierarchy", "Show zone hierarchy").option("--physics <path>", "Path to physics rules file").option("--vocabulary <path>", "Path to vocabulary file").option("--json", "Output as JSON").option("--exit-code", "Exit with validation status code").action(async (options) => {
3443
+ if (options.hierarchy) {
3444
+ console.log("Zone Hierarchy:");
3445
+ console.log("");
3446
+ console.log(` ${getHierarchyDescription()}`);
3447
+ console.log("");
3448
+ console.log("Zones:");
3449
+ for (const zone of ZONE_HIERARCHY) {
3450
+ const index = ZONE_HIERARCHY.indexOf(zone);
3451
+ const restriction = index === 0 ? "(most restrictive)" : index === ZONE_HIERARCHY.length - 1 ? "(least restrictive)" : "";
3452
+ console.log(` ${index + 1}. ${zone} ${restriction}`);
3453
+ }
3454
+ return;
3455
+ }
3456
+ let statement;
3457
+ if (options.file) {
3458
+ try {
3459
+ statement = await readFile(options.file, "utf-8");
3460
+ } catch {
3461
+ console.error(`Failed to read file: ${options.file}`);
3462
+ process.exit(1);
3463
+ }
3464
+ } else if (options.text) {
3465
+ statement = options.text;
3466
+ } else {
3467
+ console.error("Provide a statement with -f (file) or -t (text), or use --hierarchy");
3468
+ process.exit(1);
3469
+ }
3470
+ try {
3471
+ const warden = new AdversarialWarden();
3472
+ const validateOptions = {};
3473
+ if (options.physics)
3474
+ validateOptions.physicsPath = options.physics;
3475
+ if (options.vocabulary)
3476
+ validateOptions.vocabularyPath = options.vocabulary;
3477
+ const result = await warden.validate(statement, validateOptions);
3478
+ if (options.json) {
3479
+ console.log(JSON.stringify(result, null, 2));
3480
+ } else {
3481
+ const statusEmoji = result.status === "VALID" ? "\u2713" : result.status === "DRIFT" ? "\u26A0" : "\u2717";
3482
+ console.log(`${statusEmoji} Status: ${result.status}`);
3483
+ console.log("");
3484
+ console.log("Grounding Checks:");
3485
+ console.log(` Relevance: ${result.checks.relevance.passed ? "\u2713" : "\u2717"} ${result.checks.relevance.reason}`);
3486
+ console.log(` Hierarchy: ${result.checks.hierarchy.passed ? "\u2713" : "\u2717"} ${result.checks.hierarchy.reason}`);
3487
+ console.log(` Rules: ${result.checks.rules.passed ? "\u2713" : "\u2717"} ${result.checks.rules.reason}`);
3488
+ console.log("");
3489
+ console.log("Adversarial Checks:");
3490
+ console.log(` Relevance: ${result.adversarialChecks.relevance.passed ? "\u2713" : "\u2717"} ${result.adversarialChecks.relevance.reason}`);
3491
+ console.log(` Learned Rules: ${result.adversarialChecks.learnedRules.passed ? "\u2713" : "\u2717"} ${result.adversarialChecks.learnedRules.reason}`);
3492
+ console.log("");
3493
+ console.log(`Required Zone: ${result.requiredZone}`);
3494
+ console.log(`Cited Zone: ${result.citedZone ?? "(none)"}`);
3495
+ if (result.correction) {
3496
+ console.log("");
3497
+ console.log(`Correction: ${result.correction}`);
3498
+ }
3499
+ }
3500
+ if (options.exitCode) {
3501
+ process.exit(getExitCode(result));
3502
+ }
3503
+ } catch (error) {
3504
+ console.error("Warden error:", error instanceof Error ? error.message : error);
3505
+ process.exit(1);
3506
+ }
3507
+ });
3508
+ program.command("version").description("Show Anchor version").action(() => {
3509
+ console.log(`Anchor v${VERSION}`);
3510
+ console.log("Ground truth enforcement for Sigil design physics.");
3511
+ });
3512
+ program.parse();
3513
+ //# sourceMappingURL=out.js.map
3514
+ //# sourceMappingURL=index.js.map