@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.
- package/LICENSE.md +660 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +3514 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1230 -0
- package/dist/index.js +2449 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
|
@@ -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
|