@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2449 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFile, mkdir, writeFile, readdir, unlink, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { EventEmitter } from 'eventemitter3';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
// src/lifecycle/fork-manager.ts
|
|
9
|
+
var RpcError = class extends Error {
|
|
10
|
+
constructor(code, message, data) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.data = data;
|
|
14
|
+
this.name = "RpcError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var RpcTimeoutError = class extends Error {
|
|
18
|
+
constructor(method, timeoutMs) {
|
|
19
|
+
super(`RPC call '${method}' timed out after ${timeoutMs}ms`);
|
|
20
|
+
this.method = method;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
this.name = "RpcTimeoutError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
26
|
+
var requestId = 0;
|
|
27
|
+
async function rpcCall(url, method, params = [], timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
30
|
+
const request = {
|
|
31
|
+
jsonrpc: "2.0",
|
|
32
|
+
id: ++requestId,
|
|
33
|
+
method,
|
|
34
|
+
params
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json"
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(request),
|
|
43
|
+
signal: controller.signal
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new RpcError(-32e3, `HTTP error: ${response.status} ${response.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
if (data.error) {
|
|
50
|
+
throw new RpcError(data.error.code, data.error.message, data.error.data);
|
|
51
|
+
}
|
|
52
|
+
if (data.result === void 0) {
|
|
53
|
+
throw new RpcError(-32e3, "RPC response missing result");
|
|
54
|
+
}
|
|
55
|
+
return data.result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
58
|
+
throw new RpcTimeoutError(method, timeoutMs);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function isRpcReady(url, timeoutMs = 5e3) {
|
|
66
|
+
try {
|
|
67
|
+
await rpcCall(url, "eth_chainId", [], timeoutMs);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function waitForRpc(url, maxAttempts = 30, intervalMs = 1e3) {
|
|
74
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
75
|
+
if (await isRpcReady(url)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`RPC at ${url} not ready after ${maxAttempts} attempts`);
|
|
81
|
+
}
|
|
82
|
+
var hexStringSchema = z.string().regex(/^0x[0-9a-fA-F]*$/);
|
|
83
|
+
z.union([hexStringSchema, z.literal("latest"), z.literal("pending"), z.literal("earliest")]);
|
|
84
|
+
var ForkSchema = z.object({
|
|
85
|
+
id: z.string(),
|
|
86
|
+
network: z.object({
|
|
87
|
+
name: z.string(),
|
|
88
|
+
chainId: z.number(),
|
|
89
|
+
rpcUrl: z.string()
|
|
90
|
+
}),
|
|
91
|
+
blockNumber: z.number(),
|
|
92
|
+
rpcUrl: z.string(),
|
|
93
|
+
port: z.number(),
|
|
94
|
+
pid: z.number(),
|
|
95
|
+
createdAt: z.string().transform((s) => new Date(s)),
|
|
96
|
+
sessionId: z.string().optional()
|
|
97
|
+
});
|
|
98
|
+
var ForkRegistrySchema = z.object({
|
|
99
|
+
forks: z.array(ForkSchema),
|
|
100
|
+
lastUpdated: z.string().transform((s) => new Date(s))
|
|
101
|
+
});
|
|
102
|
+
var ZONE_HIERARCHY = ["critical", "elevated", "standard", "local"];
|
|
103
|
+
var ExitCode = {
|
|
104
|
+
PASS: 0,
|
|
105
|
+
DRIFT: 1,
|
|
106
|
+
DECEPTIVE: 2,
|
|
107
|
+
VIOLATION: 3,
|
|
108
|
+
REVERT: 4,
|
|
109
|
+
CORRUPT: 5,
|
|
110
|
+
SCHEMA: 6
|
|
111
|
+
};
|
|
112
|
+
z.object({
|
|
113
|
+
impersonatedAddress: z.string(),
|
|
114
|
+
realAddress: z.string().optional(),
|
|
115
|
+
component: z.string(),
|
|
116
|
+
observedValue: z.string().optional(),
|
|
117
|
+
onChainValue: z.string().optional(),
|
|
118
|
+
indexedValue: z.string().optional(),
|
|
119
|
+
dataSource: z.enum(["on-chain", "indexed", "mixed", "unknown"]).optional()
|
|
120
|
+
});
|
|
121
|
+
z.object({
|
|
122
|
+
type: z.enum(["data_source_mismatch", "stale_indexed_data", "lens_financial_check", "impersonation_leak"]),
|
|
123
|
+
severity: z.enum(["error", "warning", "info"]),
|
|
124
|
+
message: z.string(),
|
|
125
|
+
component: z.string(),
|
|
126
|
+
zone: z.enum(["critical", "elevated", "standard", "local"]).optional(),
|
|
127
|
+
expected: z.string().optional(),
|
|
128
|
+
actual: z.string().optional(),
|
|
129
|
+
suggestion: z.string().optional()
|
|
130
|
+
});
|
|
131
|
+
({
|
|
132
|
+
...ExitCode});
|
|
133
|
+
|
|
134
|
+
// src/lifecycle/fork-manager.ts
|
|
135
|
+
var DEFAULT_PORT_START = 8545;
|
|
136
|
+
var DEFAULT_PORT_END = 8600;
|
|
137
|
+
var DEFAULT_REGISTRY_PATH = "grimoires/anchor/forks.json";
|
|
138
|
+
var ForkManager = class extends EventEmitter {
|
|
139
|
+
forks = /* @__PURE__ */ new Map();
|
|
140
|
+
processes = /* @__PURE__ */ new Map();
|
|
141
|
+
usedPorts = /* @__PURE__ */ new Set();
|
|
142
|
+
registryPath;
|
|
143
|
+
constructor(options) {
|
|
144
|
+
super();
|
|
145
|
+
this.registryPath = options?.registryPath ?? DEFAULT_REGISTRY_PATH;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Initialize the ForkManager by loading persisted registry
|
|
149
|
+
*/
|
|
150
|
+
async init() {
|
|
151
|
+
await this.loadRegistry();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Spawn a new Anvil fork
|
|
155
|
+
*
|
|
156
|
+
* @param config - Fork configuration
|
|
157
|
+
* @returns Promise resolving to the created Fork
|
|
158
|
+
*/
|
|
159
|
+
async fork(config) {
|
|
160
|
+
const port = config.port ?? this.findAvailablePort();
|
|
161
|
+
const forkId = this.generateForkId();
|
|
162
|
+
const args = [
|
|
163
|
+
"--fork-url",
|
|
164
|
+
config.network.rpcUrl,
|
|
165
|
+
"--port",
|
|
166
|
+
port.toString(),
|
|
167
|
+
"--chain-id",
|
|
168
|
+
config.network.chainId.toString()
|
|
169
|
+
];
|
|
170
|
+
if (config.blockNumber !== void 0) {
|
|
171
|
+
args.push("--fork-block-number", config.blockNumber.toString());
|
|
172
|
+
}
|
|
173
|
+
const process2 = spawn("anvil", args, {
|
|
174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
175
|
+
detached: false
|
|
176
|
+
});
|
|
177
|
+
const pid = process2.pid;
|
|
178
|
+
if (!pid) {
|
|
179
|
+
throw new Error("Failed to spawn Anvil process");
|
|
180
|
+
}
|
|
181
|
+
const rpcUrl = `http://127.0.0.1:${port}`;
|
|
182
|
+
try {
|
|
183
|
+
await waitForRpc(rpcUrl, 30, 500);
|
|
184
|
+
} catch {
|
|
185
|
+
process2.kill();
|
|
186
|
+
throw new Error(`Anvil fork failed to become ready at ${rpcUrl}`);
|
|
187
|
+
}
|
|
188
|
+
const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
|
|
189
|
+
const blockNumber = config.blockNumber ?? parseInt(blockNumberHex, 16);
|
|
190
|
+
const fork = {
|
|
191
|
+
id: forkId,
|
|
192
|
+
network: config.network,
|
|
193
|
+
blockNumber,
|
|
194
|
+
rpcUrl,
|
|
195
|
+
port,
|
|
196
|
+
pid,
|
|
197
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
198
|
+
...config.sessionId !== void 0 && { sessionId: config.sessionId }
|
|
199
|
+
};
|
|
200
|
+
this.forks.set(forkId, fork);
|
|
201
|
+
this.processes.set(forkId, process2);
|
|
202
|
+
this.usedPorts.add(port);
|
|
203
|
+
process2.on("exit", (code) => {
|
|
204
|
+
this.handleProcessExit(forkId, code);
|
|
205
|
+
});
|
|
206
|
+
process2.on("error", (error) => {
|
|
207
|
+
this.emit("fork:error", forkId, error);
|
|
208
|
+
});
|
|
209
|
+
await this.saveRegistry();
|
|
210
|
+
this.emit("fork:created", fork);
|
|
211
|
+
return fork;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Wait for a fork to be ready
|
|
215
|
+
*
|
|
216
|
+
* @param forkId - Fork ID to wait for
|
|
217
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
218
|
+
*/
|
|
219
|
+
async waitForReady(forkId, timeoutMs = 3e4) {
|
|
220
|
+
const fork = this.forks.get(forkId);
|
|
221
|
+
if (!fork) {
|
|
222
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
223
|
+
}
|
|
224
|
+
await waitForRpc(fork.rpcUrl, Math.ceil(timeoutMs / 500), 500);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Kill a specific fork
|
|
228
|
+
*
|
|
229
|
+
* @param forkId - Fork ID to kill
|
|
230
|
+
*/
|
|
231
|
+
async kill(forkId) {
|
|
232
|
+
const process2 = this.processes.get(forkId);
|
|
233
|
+
const fork = this.forks.get(forkId);
|
|
234
|
+
if (process2) {
|
|
235
|
+
process2.kill("SIGTERM");
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
237
|
+
if (!process2.killed) {
|
|
238
|
+
process2.kill("SIGKILL");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (fork) {
|
|
242
|
+
this.usedPorts.delete(fork.port);
|
|
243
|
+
}
|
|
244
|
+
this.forks.delete(forkId);
|
|
245
|
+
this.processes.delete(forkId);
|
|
246
|
+
await this.saveRegistry();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Kill all forks
|
|
250
|
+
*/
|
|
251
|
+
async killAll() {
|
|
252
|
+
const forkIds = Array.from(this.forks.keys());
|
|
253
|
+
await Promise.all(forkIds.map((id) => this.kill(id)));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* List all active forks
|
|
257
|
+
*
|
|
258
|
+
* @returns Array of active forks
|
|
259
|
+
*/
|
|
260
|
+
list() {
|
|
261
|
+
return Array.from(this.forks.values());
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get a fork by ID
|
|
265
|
+
*
|
|
266
|
+
* @param forkId - Fork ID
|
|
267
|
+
* @returns Fork if found, undefined otherwise
|
|
268
|
+
*/
|
|
269
|
+
get(forkId) {
|
|
270
|
+
return this.forks.get(forkId);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Export environment variables for a fork
|
|
274
|
+
*
|
|
275
|
+
* @param forkId - Fork ID
|
|
276
|
+
* @returns Environment variables object
|
|
277
|
+
*/
|
|
278
|
+
exportEnv(forkId) {
|
|
279
|
+
const fork = this.forks.get(forkId);
|
|
280
|
+
if (!fork) {
|
|
281
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
RPC_URL: fork.rpcUrl,
|
|
285
|
+
CHAIN_ID: fork.network.chainId.toString(),
|
|
286
|
+
FORK_BLOCK: fork.blockNumber.toString(),
|
|
287
|
+
FORK_ID: fork.id
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Load fork registry from disk
|
|
292
|
+
*/
|
|
293
|
+
async loadRegistry() {
|
|
294
|
+
try {
|
|
295
|
+
if (!existsSync(this.registryPath)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const content = await readFile(this.registryPath, "utf-8");
|
|
299
|
+
const data = JSON.parse(content);
|
|
300
|
+
const registry = ForkRegistrySchema.parse(data);
|
|
301
|
+
for (const registryFork of registry.forks) {
|
|
302
|
+
try {
|
|
303
|
+
process.kill(registryFork.pid, 0);
|
|
304
|
+
const fork = {
|
|
305
|
+
id: registryFork.id,
|
|
306
|
+
network: registryFork.network,
|
|
307
|
+
blockNumber: registryFork.blockNumber,
|
|
308
|
+
rpcUrl: registryFork.rpcUrl,
|
|
309
|
+
port: registryFork.port,
|
|
310
|
+
pid: registryFork.pid,
|
|
311
|
+
createdAt: registryFork.createdAt,
|
|
312
|
+
...registryFork.sessionId !== void 0 && { sessionId: registryFork.sessionId }
|
|
313
|
+
};
|
|
314
|
+
const ready = await this.checkForkHealth(fork);
|
|
315
|
+
if (ready) {
|
|
316
|
+
this.forks.set(fork.id, fork);
|
|
317
|
+
this.usedPorts.add(fork.port);
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Save fork registry to disk
|
|
327
|
+
*/
|
|
328
|
+
async saveRegistry() {
|
|
329
|
+
const registry = {
|
|
330
|
+
forks: Array.from(this.forks.values()).map((fork) => ({
|
|
331
|
+
...fork,
|
|
332
|
+
createdAt: fork.createdAt.toISOString()
|
|
333
|
+
})),
|
|
334
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
335
|
+
};
|
|
336
|
+
const dir = dirname(this.registryPath);
|
|
337
|
+
if (!existsSync(dir)) {
|
|
338
|
+
await mkdir(dir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
await writeFile(this.registryPath, JSON.stringify(registry, null, 2));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Check if a fork is still healthy
|
|
344
|
+
*/
|
|
345
|
+
async checkForkHealth(fork) {
|
|
346
|
+
try {
|
|
347
|
+
await rpcCall(fork.rpcUrl, "eth_chainId", [], 2e3);
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Handle process exit
|
|
355
|
+
*/
|
|
356
|
+
handleProcessExit(forkId, code) {
|
|
357
|
+
const fork = this.forks.get(forkId);
|
|
358
|
+
if (fork) {
|
|
359
|
+
this.usedPorts.delete(fork.port);
|
|
360
|
+
}
|
|
361
|
+
this.forks.delete(forkId);
|
|
362
|
+
this.processes.delete(forkId);
|
|
363
|
+
this.emit("fork:exit", forkId, code);
|
|
364
|
+
void this.saveRegistry();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Find an available port
|
|
368
|
+
*/
|
|
369
|
+
findAvailablePort() {
|
|
370
|
+
for (let port = DEFAULT_PORT_START; port <= DEFAULT_PORT_END; port++) {
|
|
371
|
+
if (!this.usedPorts.has(port)) {
|
|
372
|
+
return port;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
throw new Error("No available ports in range");
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Generate a unique fork ID
|
|
379
|
+
*/
|
|
380
|
+
generateForkId() {
|
|
381
|
+
const timestamp = Date.now().toString(36);
|
|
382
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
383
|
+
return `fork-${timestamp}-${random}`;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var defaultManager = null;
|
|
387
|
+
function getForkManager() {
|
|
388
|
+
if (!defaultManager) {
|
|
389
|
+
defaultManager = new ForkManager();
|
|
390
|
+
}
|
|
391
|
+
return defaultManager;
|
|
392
|
+
}
|
|
393
|
+
function resetForkManager() {
|
|
394
|
+
defaultManager = null;
|
|
395
|
+
}
|
|
396
|
+
var DEFAULT_BASE_PATH = "grimoires/anchor/sessions";
|
|
397
|
+
var SnapshotManager = class {
|
|
398
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
399
|
+
taskToSnapshot = /* @__PURE__ */ new Map();
|
|
400
|
+
basePath;
|
|
401
|
+
sessionId = null;
|
|
402
|
+
constructor(config) {
|
|
403
|
+
this.basePath = config?.basePath ?? DEFAULT_BASE_PATH;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Initialize the manager for a session
|
|
407
|
+
*
|
|
408
|
+
* @param sessionId - Session ID to manage snapshots for
|
|
409
|
+
*/
|
|
410
|
+
async init(sessionId) {
|
|
411
|
+
this.sessionId = sessionId;
|
|
412
|
+
await this.loadSnapshots();
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Create a new snapshot
|
|
416
|
+
*
|
|
417
|
+
* @param config - Snapshot configuration
|
|
418
|
+
* @param rpcUrl - RPC URL of the fork
|
|
419
|
+
* @returns Promise resolving to snapshot metadata
|
|
420
|
+
*/
|
|
421
|
+
async create(config, rpcUrl) {
|
|
422
|
+
const snapshotId = await rpcCall(rpcUrl, "evm_snapshot");
|
|
423
|
+
const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
|
|
424
|
+
const blockNumber = parseInt(blockNumberHex, 16);
|
|
425
|
+
const metadata = {
|
|
426
|
+
id: snapshotId,
|
|
427
|
+
forkId: config.forkId,
|
|
428
|
+
sessionId: config.sessionId,
|
|
429
|
+
blockNumber,
|
|
430
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
431
|
+
...config.taskId !== void 0 && { taskId: config.taskId },
|
|
432
|
+
...config.description !== void 0 && { description: config.description }
|
|
433
|
+
};
|
|
434
|
+
this.snapshots.set(snapshotId, metadata);
|
|
435
|
+
if (config.taskId) {
|
|
436
|
+
this.taskToSnapshot.set(config.taskId, snapshotId);
|
|
437
|
+
}
|
|
438
|
+
await this.saveSnapshot(metadata);
|
|
439
|
+
return metadata;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Revert to a snapshot
|
|
443
|
+
*
|
|
444
|
+
* @param rpcUrl - RPC URL of the fork
|
|
445
|
+
* @param snapshotId - Snapshot ID to revert to
|
|
446
|
+
* @returns Promise resolving to true if successful
|
|
447
|
+
*/
|
|
448
|
+
async revert(rpcUrl, snapshotId) {
|
|
449
|
+
const result = await rpcCall(rpcUrl, "evm_revert", [snapshotId]);
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get snapshot metadata by ID
|
|
454
|
+
*
|
|
455
|
+
* @param snapshotId - Snapshot ID
|
|
456
|
+
* @returns Snapshot metadata if found
|
|
457
|
+
*/
|
|
458
|
+
get(snapshotId) {
|
|
459
|
+
return this.snapshots.get(snapshotId);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* List all snapshots sorted by creation time
|
|
463
|
+
*
|
|
464
|
+
* @returns Array of snapshot metadata
|
|
465
|
+
*/
|
|
466
|
+
list() {
|
|
467
|
+
return Array.from(this.snapshots.values()).sort(
|
|
468
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get snapshot for a specific task
|
|
473
|
+
*
|
|
474
|
+
* @param taskId - Task ID
|
|
475
|
+
* @returns Snapshot metadata if found
|
|
476
|
+
*/
|
|
477
|
+
getForTask(taskId) {
|
|
478
|
+
const snapshotId = this.taskToSnapshot.get(taskId);
|
|
479
|
+
if (!snapshotId)
|
|
480
|
+
return void 0;
|
|
481
|
+
return this.snapshots.get(snapshotId);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get the count of snapshots
|
|
485
|
+
*
|
|
486
|
+
* @returns Number of snapshots
|
|
487
|
+
*/
|
|
488
|
+
count() {
|
|
489
|
+
return this.snapshots.size;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Cleanup old snapshots, keeping the most recent
|
|
493
|
+
*
|
|
494
|
+
* @param keepLast - Number of recent snapshots to keep
|
|
495
|
+
*/
|
|
496
|
+
async cleanup(keepLast) {
|
|
497
|
+
const sorted = this.list();
|
|
498
|
+
const toDelete = sorted.slice(0, -keepLast);
|
|
499
|
+
for (const snapshot of toDelete) {
|
|
500
|
+
await this.deleteSnapshot(snapshot.id);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get snapshot directory path for current session
|
|
505
|
+
*/
|
|
506
|
+
getSnapshotDir() {
|
|
507
|
+
if (!this.sessionId) {
|
|
508
|
+
throw new Error("SnapshotManager not initialized with session ID");
|
|
509
|
+
}
|
|
510
|
+
return join(this.basePath, this.sessionId, "snapshots");
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get file path for a snapshot
|
|
514
|
+
*/
|
|
515
|
+
getSnapshotPath(snapshotId) {
|
|
516
|
+
return join(this.getSnapshotDir(), `${snapshotId}.json`);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Load existing snapshots from disk
|
|
520
|
+
*/
|
|
521
|
+
async loadSnapshots() {
|
|
522
|
+
const dir = this.getSnapshotDir();
|
|
523
|
+
if (!existsSync(dir)) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const files = await readdir(dir);
|
|
528
|
+
for (const file of files) {
|
|
529
|
+
if (!file.endsWith(".json"))
|
|
530
|
+
continue;
|
|
531
|
+
try {
|
|
532
|
+
const content = await readFile(join(dir, file), "utf-8");
|
|
533
|
+
const data = JSON.parse(content);
|
|
534
|
+
data.createdAt = new Date(data.createdAt);
|
|
535
|
+
this.snapshots.set(data.id, data);
|
|
536
|
+
if (data.taskId) {
|
|
537
|
+
this.taskToSnapshot.set(data.taskId, data.id);
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Save snapshot metadata to disk
|
|
547
|
+
*/
|
|
548
|
+
async saveSnapshot(metadata) {
|
|
549
|
+
const dir = this.getSnapshotDir();
|
|
550
|
+
if (!existsSync(dir)) {
|
|
551
|
+
await mkdir(dir, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
await writeFile(
|
|
554
|
+
this.getSnapshotPath(metadata.id),
|
|
555
|
+
JSON.stringify(metadata, null, 2)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Delete a snapshot from memory and disk
|
|
560
|
+
*/
|
|
561
|
+
async deleteSnapshot(snapshotId) {
|
|
562
|
+
const metadata = this.snapshots.get(snapshotId);
|
|
563
|
+
this.snapshots.delete(snapshotId);
|
|
564
|
+
if (metadata?.taskId) {
|
|
565
|
+
this.taskToSnapshot.delete(metadata.taskId);
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
await unlink(this.getSnapshotPath(snapshotId));
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var defaultManager2 = null;
|
|
574
|
+
function getSnapshotManager() {
|
|
575
|
+
if (!defaultManager2) {
|
|
576
|
+
defaultManager2 = new SnapshotManager();
|
|
577
|
+
}
|
|
578
|
+
return defaultManager2;
|
|
579
|
+
}
|
|
580
|
+
function resetSnapshotManager() {
|
|
581
|
+
defaultManager2 = null;
|
|
582
|
+
}
|
|
583
|
+
var DEFAULT_BASE_PATH2 = "grimoires/anchor/checkpoints";
|
|
584
|
+
var DEFAULT_SNAPSHOT_INTERVAL = 10;
|
|
585
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
586
|
+
var CheckpointManager = class {
|
|
587
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
588
|
+
snapshotCount = 0;
|
|
589
|
+
firstSnapshotId = null;
|
|
590
|
+
lastSnapshotId = null;
|
|
591
|
+
basePath;
|
|
592
|
+
snapshotInterval;
|
|
593
|
+
maxCheckpoints;
|
|
594
|
+
sessionId = null;
|
|
595
|
+
forkId = null;
|
|
596
|
+
constructor(config) {
|
|
597
|
+
this.basePath = config?.basePath ?? DEFAULT_BASE_PATH2;
|
|
598
|
+
this.snapshotInterval = config?.snapshotInterval ?? DEFAULT_SNAPSHOT_INTERVAL;
|
|
599
|
+
this.maxCheckpoints = config?.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Initialize the manager for a session
|
|
603
|
+
*
|
|
604
|
+
* @param sessionId - Session ID
|
|
605
|
+
* @param forkId - Fork ID
|
|
606
|
+
*/
|
|
607
|
+
async init(sessionId, forkId) {
|
|
608
|
+
this.sessionId = sessionId;
|
|
609
|
+
this.forkId = forkId;
|
|
610
|
+
await this.loadCheckpoints();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Called when a snapshot is created. May trigger checkpoint.
|
|
614
|
+
*
|
|
615
|
+
* @param snapshotId - ID of the created snapshot
|
|
616
|
+
* @param rpcUrl - RPC URL of the fork
|
|
617
|
+
* @returns True if checkpoint was created
|
|
618
|
+
*/
|
|
619
|
+
async onSnapshot(snapshotId, rpcUrl) {
|
|
620
|
+
this.snapshotCount++;
|
|
621
|
+
if (!this.firstSnapshotId) {
|
|
622
|
+
this.firstSnapshotId = snapshotId;
|
|
623
|
+
}
|
|
624
|
+
this.lastSnapshotId = snapshotId;
|
|
625
|
+
if (this.snapshotCount >= this.snapshotInterval) {
|
|
626
|
+
await this.create(rpcUrl);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Create a checkpoint by exporting state
|
|
633
|
+
*
|
|
634
|
+
* @param rpcUrl - RPC URL of the fork
|
|
635
|
+
* @returns Checkpoint metadata
|
|
636
|
+
*/
|
|
637
|
+
async create(rpcUrl) {
|
|
638
|
+
if (!this.sessionId || !this.forkId) {
|
|
639
|
+
throw new Error("CheckpointManager not initialized");
|
|
640
|
+
}
|
|
641
|
+
const state = await rpcCall(rpcUrl, "anvil_dumpState");
|
|
642
|
+
const blockNumberHex = await rpcCall(rpcUrl, "eth_blockNumber");
|
|
643
|
+
const blockNumber = parseInt(blockNumberHex, 16);
|
|
644
|
+
const checkpointId = this.generateCheckpointId();
|
|
645
|
+
const metadata = {
|
|
646
|
+
id: checkpointId,
|
|
647
|
+
sessionId: this.sessionId,
|
|
648
|
+
forkId: this.forkId,
|
|
649
|
+
snapshotRange: {
|
|
650
|
+
first: this.firstSnapshotId ?? "",
|
|
651
|
+
last: this.lastSnapshotId ?? ""
|
|
652
|
+
},
|
|
653
|
+
blockNumber,
|
|
654
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
655
|
+
snapshotCount: this.snapshotCount
|
|
656
|
+
};
|
|
657
|
+
await this.saveCheckpoint(checkpointId, state, metadata);
|
|
658
|
+
this.checkpoints.set(checkpointId, metadata);
|
|
659
|
+
this.snapshotCount = 0;
|
|
660
|
+
this.firstSnapshotId = null;
|
|
661
|
+
this.lastSnapshotId = null;
|
|
662
|
+
await this.cleanup();
|
|
663
|
+
return metadata;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Restore from a checkpoint
|
|
667
|
+
*
|
|
668
|
+
* @param checkpointId - Checkpoint ID to restore
|
|
669
|
+
* @param forkManager - ForkManager instance
|
|
670
|
+
* @param network - Network configuration
|
|
671
|
+
* @returns New fork with restored state
|
|
672
|
+
*/
|
|
673
|
+
async restore(checkpointId, forkManager, network) {
|
|
674
|
+
if (!this.sessionId) {
|
|
675
|
+
throw new Error("CheckpointManager not initialized");
|
|
676
|
+
}
|
|
677
|
+
const checkpoint = this.checkpoints.get(checkpointId);
|
|
678
|
+
if (!checkpoint) {
|
|
679
|
+
throw new Error(`Checkpoint ${checkpointId} not found`);
|
|
680
|
+
}
|
|
681
|
+
const statePath = this.getStatePath(checkpointId);
|
|
682
|
+
const state = await readFile(statePath, "utf-8");
|
|
683
|
+
await forkManager.killAll();
|
|
684
|
+
const fork = await forkManager.fork({
|
|
685
|
+
network,
|
|
686
|
+
blockNumber: checkpoint.blockNumber,
|
|
687
|
+
sessionId: this.sessionId
|
|
688
|
+
});
|
|
689
|
+
await rpcCall(fork.rpcUrl, "anvil_loadState", [state]);
|
|
690
|
+
this.forkId = fork.id;
|
|
691
|
+
return fork;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Find the checkpoint containing a specific snapshot
|
|
695
|
+
*
|
|
696
|
+
* @param snapshotId - Snapshot ID to find
|
|
697
|
+
* @returns Checkpoint metadata if found
|
|
698
|
+
*/
|
|
699
|
+
findCheckpointForSnapshot(snapshotId) {
|
|
700
|
+
const sorted = this.list().sort(
|
|
701
|
+
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
|
702
|
+
);
|
|
703
|
+
for (const checkpoint of sorted) {
|
|
704
|
+
if (checkpoint.snapshotRange.first <= snapshotId && checkpoint.snapshotRange.last >= snapshotId) {
|
|
705
|
+
return checkpoint;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return sorted[0];
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Get checkpoint by ID
|
|
712
|
+
*
|
|
713
|
+
* @param checkpointId - Checkpoint ID
|
|
714
|
+
* @returns Checkpoint metadata if found
|
|
715
|
+
*/
|
|
716
|
+
get(checkpointId) {
|
|
717
|
+
return this.checkpoints.get(checkpointId);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* List all checkpoints sorted by time
|
|
721
|
+
*
|
|
722
|
+
* @returns Array of checkpoint metadata
|
|
723
|
+
*/
|
|
724
|
+
list() {
|
|
725
|
+
return Array.from(this.checkpoints.values()).sort(
|
|
726
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get the latest checkpoint
|
|
731
|
+
*
|
|
732
|
+
* @returns Latest checkpoint metadata
|
|
733
|
+
*/
|
|
734
|
+
latest() {
|
|
735
|
+
const sorted = this.list();
|
|
736
|
+
return sorted[sorted.length - 1];
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Cleanup old checkpoints, keeping only the most recent
|
|
740
|
+
*/
|
|
741
|
+
async cleanup() {
|
|
742
|
+
const sorted = this.list();
|
|
743
|
+
if (sorted.length <= this.maxCheckpoints) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const toDelete = sorted.slice(0, sorted.length - this.maxCheckpoints);
|
|
747
|
+
for (const checkpoint of toDelete) {
|
|
748
|
+
await this.deleteCheckpoint(checkpoint.id);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Get session directory path
|
|
753
|
+
*/
|
|
754
|
+
getSessionDir() {
|
|
755
|
+
if (!this.sessionId) {
|
|
756
|
+
throw new Error("Session ID not set");
|
|
757
|
+
}
|
|
758
|
+
return join(this.basePath, this.sessionId);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Get checkpoint directory path
|
|
762
|
+
*/
|
|
763
|
+
getCheckpointDir(checkpointId) {
|
|
764
|
+
return join(this.getSessionDir(), checkpointId);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get state file path
|
|
768
|
+
*/
|
|
769
|
+
getStatePath(checkpointId) {
|
|
770
|
+
return join(this.getCheckpointDir(checkpointId), "state.json");
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Get metadata file path
|
|
774
|
+
*/
|
|
775
|
+
getMetaPath(checkpointId) {
|
|
776
|
+
return join(this.getCheckpointDir(checkpointId), "meta.json");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Load checkpoints from disk
|
|
780
|
+
*/
|
|
781
|
+
async loadCheckpoints() {
|
|
782
|
+
const dir = this.getSessionDir();
|
|
783
|
+
if (!existsSync(dir)) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
788
|
+
for (const entry of entries) {
|
|
789
|
+
if (!entry.isDirectory())
|
|
790
|
+
continue;
|
|
791
|
+
const metaPath = this.getMetaPath(entry.name);
|
|
792
|
+
if (!existsSync(metaPath))
|
|
793
|
+
continue;
|
|
794
|
+
try {
|
|
795
|
+
const content = await readFile(metaPath, "utf-8");
|
|
796
|
+
const data = JSON.parse(content);
|
|
797
|
+
data.createdAt = new Date(data.createdAt);
|
|
798
|
+
this.checkpoints.set(data.id, data);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Save checkpoint to disk
|
|
807
|
+
*/
|
|
808
|
+
async saveCheckpoint(checkpointId, state, metadata) {
|
|
809
|
+
const dir = this.getCheckpointDir(checkpointId);
|
|
810
|
+
if (!existsSync(dir)) {
|
|
811
|
+
await mkdir(dir, { recursive: true });
|
|
812
|
+
}
|
|
813
|
+
await writeFile(this.getStatePath(checkpointId), state);
|
|
814
|
+
await writeFile(this.getMetaPath(checkpointId), JSON.stringify(metadata, null, 2));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Delete a checkpoint from disk
|
|
818
|
+
*/
|
|
819
|
+
async deleteCheckpoint(checkpointId) {
|
|
820
|
+
this.checkpoints.delete(checkpointId);
|
|
821
|
+
const dir = this.getCheckpointDir(checkpointId);
|
|
822
|
+
if (existsSync(dir)) {
|
|
823
|
+
await rm(dir, { recursive: true });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Generate a unique checkpoint ID
|
|
828
|
+
*/
|
|
829
|
+
generateCheckpointId() {
|
|
830
|
+
const timestamp = Date.now().toString(36);
|
|
831
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
832
|
+
return `cp-${timestamp}-${random}`;
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var defaultManager3 = null;
|
|
836
|
+
function getCheckpointManager() {
|
|
837
|
+
if (!defaultManager3) {
|
|
838
|
+
defaultManager3 = new CheckpointManager();
|
|
839
|
+
}
|
|
840
|
+
return defaultManager3;
|
|
841
|
+
}
|
|
842
|
+
function resetCheckpointManager() {
|
|
843
|
+
defaultManager3 = null;
|
|
844
|
+
}
|
|
845
|
+
var TaskSchema = z.object({
|
|
846
|
+
id: z.string(),
|
|
847
|
+
type: z.enum(["fork", "ground", "warden", "generate", "validate", "write"]),
|
|
848
|
+
status: z.enum(["pending", "running", "complete", "blocked", "failed"]),
|
|
849
|
+
snapshotId: z.string().optional(),
|
|
850
|
+
checkpointId: z.string().optional(),
|
|
851
|
+
dependencies: z.array(z.string()),
|
|
852
|
+
input: z.unknown(),
|
|
853
|
+
output: z.unknown().optional(),
|
|
854
|
+
error: z.string().optional(),
|
|
855
|
+
createdAt: z.string().transform((s) => new Date(s)),
|
|
856
|
+
completedAt: z.string().transform((s) => new Date(s)).optional()
|
|
857
|
+
});
|
|
858
|
+
var TaskGraphDataSchema = z.object({
|
|
859
|
+
sessionId: z.string(),
|
|
860
|
+
tasks: z.array(TaskSchema),
|
|
861
|
+
headTaskId: z.string().optional(),
|
|
862
|
+
lastUpdated: z.string().transform((s) => new Date(s))
|
|
863
|
+
});
|
|
864
|
+
var DEFAULT_BASE_PATH3 = "grimoires/anchor/sessions";
|
|
865
|
+
var taskCounter = 0;
|
|
866
|
+
function generateTaskId(type) {
|
|
867
|
+
return `${type}-${Date.now().toString(36)}-${(++taskCounter).toString(36)}`;
|
|
868
|
+
}
|
|
869
|
+
function resetTaskCounter() {
|
|
870
|
+
taskCounter = 0;
|
|
871
|
+
}
|
|
872
|
+
var TaskGraph = class {
|
|
873
|
+
tasks = /* @__PURE__ */ new Map();
|
|
874
|
+
dependents = /* @__PURE__ */ new Map();
|
|
875
|
+
sessionId;
|
|
876
|
+
basePath;
|
|
877
|
+
autoSave;
|
|
878
|
+
headTaskId;
|
|
879
|
+
constructor(config) {
|
|
880
|
+
this.sessionId = config.sessionId;
|
|
881
|
+
this.basePath = config.basePath ?? DEFAULT_BASE_PATH3;
|
|
882
|
+
this.autoSave = config.autoSave ?? true;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Initialize the graph by loading persisted state
|
|
886
|
+
*/
|
|
887
|
+
async init() {
|
|
888
|
+
await this.load();
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Add a task to the graph
|
|
892
|
+
*
|
|
893
|
+
* @param task - Task to add
|
|
894
|
+
*/
|
|
895
|
+
async addTask(task) {
|
|
896
|
+
this.validateNoCycle(task);
|
|
897
|
+
this.tasks.set(task.id, task);
|
|
898
|
+
for (const depId of task.dependencies) {
|
|
899
|
+
if (!this.dependents.has(depId)) {
|
|
900
|
+
this.dependents.set(depId, /* @__PURE__ */ new Set());
|
|
901
|
+
}
|
|
902
|
+
this.dependents.get(depId).add(task.id);
|
|
903
|
+
}
|
|
904
|
+
this.headTaskId = task.id;
|
|
905
|
+
if (this.autoSave) {
|
|
906
|
+
await this.save();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Update task status
|
|
911
|
+
*
|
|
912
|
+
* @param taskId - Task ID
|
|
913
|
+
* @param status - New status
|
|
914
|
+
*/
|
|
915
|
+
async updateStatus(taskId, status) {
|
|
916
|
+
const task = this.tasks.get(taskId);
|
|
917
|
+
if (!task) {
|
|
918
|
+
throw new Error(`Task ${taskId} not found`);
|
|
919
|
+
}
|
|
920
|
+
task.status = status;
|
|
921
|
+
if (status === "complete" || status === "failed") {
|
|
922
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
923
|
+
}
|
|
924
|
+
if (this.autoSave) {
|
|
925
|
+
await this.save();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Set the snapshot binding for a task
|
|
930
|
+
*
|
|
931
|
+
* @param taskId - Task ID
|
|
932
|
+
* @param snapshotId - Snapshot ID
|
|
933
|
+
*/
|
|
934
|
+
async setSnapshot(taskId, snapshotId) {
|
|
935
|
+
const task = this.tasks.get(taskId);
|
|
936
|
+
if (!task) {
|
|
937
|
+
throw new Error(`Task ${taskId} not found`);
|
|
938
|
+
}
|
|
939
|
+
task.snapshotId = snapshotId;
|
|
940
|
+
if (this.autoSave) {
|
|
941
|
+
await this.save();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Set the checkpoint binding for a task
|
|
946
|
+
*
|
|
947
|
+
* @param taskId - Task ID
|
|
948
|
+
* @param checkpointId - Checkpoint ID
|
|
949
|
+
*/
|
|
950
|
+
async setCheckpoint(taskId, checkpointId) {
|
|
951
|
+
const task = this.tasks.get(taskId);
|
|
952
|
+
if (!task) {
|
|
953
|
+
throw new Error(`Task ${taskId} not found`);
|
|
954
|
+
}
|
|
955
|
+
task.checkpointId = checkpointId;
|
|
956
|
+
if (this.autoSave) {
|
|
957
|
+
await this.save();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Set task output
|
|
962
|
+
*
|
|
963
|
+
* @param taskId - Task ID
|
|
964
|
+
* @param output - Task output
|
|
965
|
+
*/
|
|
966
|
+
async setOutput(taskId, output) {
|
|
967
|
+
const task = this.tasks.get(taskId);
|
|
968
|
+
if (!task) {
|
|
969
|
+
throw new Error(`Task ${taskId} not found`);
|
|
970
|
+
}
|
|
971
|
+
task.output = output;
|
|
972
|
+
if (this.autoSave) {
|
|
973
|
+
await this.save();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Set task error
|
|
978
|
+
*
|
|
979
|
+
* @param taskId - Task ID
|
|
980
|
+
* @param error - Error message
|
|
981
|
+
*/
|
|
982
|
+
async setError(taskId, error) {
|
|
983
|
+
const task = this.tasks.get(taskId);
|
|
984
|
+
if (!task) {
|
|
985
|
+
throw new Error(`Task ${taskId} not found`);
|
|
986
|
+
}
|
|
987
|
+
task.error = error;
|
|
988
|
+
task.status = "failed";
|
|
989
|
+
if (this.autoSave) {
|
|
990
|
+
await this.save();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get a task by ID
|
|
995
|
+
*
|
|
996
|
+
* @param taskId - Task ID
|
|
997
|
+
* @returns Task if found
|
|
998
|
+
*/
|
|
999
|
+
getTask(taskId) {
|
|
1000
|
+
return this.tasks.get(taskId);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Get all tasks
|
|
1004
|
+
*
|
|
1005
|
+
* @returns Array of all tasks
|
|
1006
|
+
*/
|
|
1007
|
+
getAllTasks() {
|
|
1008
|
+
return Array.from(this.tasks.values());
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Get tasks by status
|
|
1012
|
+
*
|
|
1013
|
+
* @param status - Status to filter by
|
|
1014
|
+
* @returns Array of matching tasks
|
|
1015
|
+
*/
|
|
1016
|
+
getTasksByStatus(status) {
|
|
1017
|
+
return Array.from(this.tasks.values()).filter((t) => t.status === status);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Check if a task can run (all dependencies complete)
|
|
1021
|
+
*
|
|
1022
|
+
* @param taskId - Task ID
|
|
1023
|
+
* @returns True if all dependencies are complete
|
|
1024
|
+
*/
|
|
1025
|
+
canRun(taskId) {
|
|
1026
|
+
const task = this.tasks.get(taskId);
|
|
1027
|
+
if (!task)
|
|
1028
|
+
return false;
|
|
1029
|
+
if (task.status !== "pending")
|
|
1030
|
+
return false;
|
|
1031
|
+
for (const depId of task.dependencies) {
|
|
1032
|
+
const dep = this.tasks.get(depId);
|
|
1033
|
+
if (!dep || dep.status !== "complete") {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get the next runnable task (pending with all deps complete)
|
|
1041
|
+
*
|
|
1042
|
+
* @returns Next runnable task or undefined
|
|
1043
|
+
*/
|
|
1044
|
+
getNextRunnable() {
|
|
1045
|
+
for (const task of this.tasks.values()) {
|
|
1046
|
+
if (this.canRun(task.id)) {
|
|
1047
|
+
return task;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Propagate blocked status to all dependents of a failed task
|
|
1054
|
+
*
|
|
1055
|
+
* @param taskId - ID of the failed task
|
|
1056
|
+
*/
|
|
1057
|
+
async propagateBlocked(taskId) {
|
|
1058
|
+
const dependentIds = this.dependents.get(taskId);
|
|
1059
|
+
if (!dependentIds)
|
|
1060
|
+
return;
|
|
1061
|
+
for (const depId of dependentIds) {
|
|
1062
|
+
const task = this.tasks.get(depId);
|
|
1063
|
+
if (task && task.status === "pending") {
|
|
1064
|
+
task.status = "blocked";
|
|
1065
|
+
await this.propagateBlocked(depId);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (this.autoSave) {
|
|
1069
|
+
await this.save();
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Find the recovery point for a failed task
|
|
1074
|
+
*
|
|
1075
|
+
* @param taskId - ID of the task needing recovery
|
|
1076
|
+
* @returns Last complete task with snapshot, or undefined
|
|
1077
|
+
*/
|
|
1078
|
+
findRecoveryPoint(taskId) {
|
|
1079
|
+
const task = this.tasks.get(taskId);
|
|
1080
|
+
if (!task)
|
|
1081
|
+
return void 0;
|
|
1082
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1083
|
+
const queue = [...task.dependencies];
|
|
1084
|
+
let bestRecovery;
|
|
1085
|
+
while (queue.length > 0) {
|
|
1086
|
+
const id = queue.pop();
|
|
1087
|
+
if (visited.has(id))
|
|
1088
|
+
continue;
|
|
1089
|
+
visited.add(id);
|
|
1090
|
+
const dep = this.tasks.get(id);
|
|
1091
|
+
if (!dep)
|
|
1092
|
+
continue;
|
|
1093
|
+
if (dep.status === "complete" && dep.snapshotId) {
|
|
1094
|
+
if (!bestRecovery || dep.createdAt > bestRecovery.createdAt) {
|
|
1095
|
+
bestRecovery = dep;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
queue.push(...dep.dependencies);
|
|
1099
|
+
}
|
|
1100
|
+
return bestRecovery;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Check if there are any blocked tasks
|
|
1104
|
+
*
|
|
1105
|
+
* @returns True if any tasks are blocked
|
|
1106
|
+
*/
|
|
1107
|
+
hasBlocked() {
|
|
1108
|
+
for (const task of this.tasks.values()) {
|
|
1109
|
+
if (task.status === "blocked") {
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Check if all tasks are complete
|
|
1117
|
+
*
|
|
1118
|
+
* @returns True if all tasks are complete
|
|
1119
|
+
*/
|
|
1120
|
+
isComplete() {
|
|
1121
|
+
for (const task of this.tasks.values()) {
|
|
1122
|
+
if (task.status !== "complete") {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return this.tasks.size > 0;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Get the graph file path
|
|
1130
|
+
*/
|
|
1131
|
+
getGraphPath() {
|
|
1132
|
+
return join(this.basePath, this.sessionId, "graph.json");
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Export graph data as JSON-serializable object
|
|
1136
|
+
*
|
|
1137
|
+
* @returns Task graph data
|
|
1138
|
+
*/
|
|
1139
|
+
toJSON() {
|
|
1140
|
+
return {
|
|
1141
|
+
sessionId: this.sessionId,
|
|
1142
|
+
tasks: Array.from(this.tasks.values()),
|
|
1143
|
+
lastUpdated: /* @__PURE__ */ new Date(),
|
|
1144
|
+
...this.headTaskId !== void 0 && { headTaskId: this.headTaskId }
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Save the graph to disk
|
|
1149
|
+
*/
|
|
1150
|
+
async save() {
|
|
1151
|
+
const data = this.toJSON();
|
|
1152
|
+
const path = this.getGraphPath();
|
|
1153
|
+
const dir = dirname(path);
|
|
1154
|
+
if (!existsSync(dir)) {
|
|
1155
|
+
await mkdir(dir, { recursive: true });
|
|
1156
|
+
}
|
|
1157
|
+
await writeFile(path, JSON.stringify(data, null, 2));
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Load the graph from disk
|
|
1161
|
+
*/
|
|
1162
|
+
async load() {
|
|
1163
|
+
const path = this.getGraphPath();
|
|
1164
|
+
if (!existsSync(path)) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
const content = await readFile(path, "utf-8");
|
|
1169
|
+
const raw = JSON.parse(content);
|
|
1170
|
+
const data = TaskGraphDataSchema.parse(raw);
|
|
1171
|
+
this.tasks.clear();
|
|
1172
|
+
this.dependents.clear();
|
|
1173
|
+
for (const task of data.tasks) {
|
|
1174
|
+
this.tasks.set(task.id, task);
|
|
1175
|
+
for (const depId of task.dependencies) {
|
|
1176
|
+
if (!this.dependents.has(depId)) {
|
|
1177
|
+
this.dependents.set(depId, /* @__PURE__ */ new Set());
|
|
1178
|
+
}
|
|
1179
|
+
this.dependents.get(depId).add(task.id);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
this.headTaskId = data.headTaskId;
|
|
1183
|
+
} catch {
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Validate that adding a task doesn't create a cycle
|
|
1188
|
+
*/
|
|
1189
|
+
validateNoCycle(newTask) {
|
|
1190
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1191
|
+
const stack = /* @__PURE__ */ new Set();
|
|
1192
|
+
const hasCycle = (taskId) => {
|
|
1193
|
+
if (stack.has(taskId))
|
|
1194
|
+
return true;
|
|
1195
|
+
if (visited.has(taskId))
|
|
1196
|
+
return false;
|
|
1197
|
+
visited.add(taskId);
|
|
1198
|
+
stack.add(taskId);
|
|
1199
|
+
const task = taskId === newTask.id ? newTask : this.tasks.get(taskId);
|
|
1200
|
+
if (task) {
|
|
1201
|
+
for (const depId of task.dependencies) {
|
|
1202
|
+
if (hasCycle(depId))
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
stack.delete(taskId);
|
|
1207
|
+
return false;
|
|
1208
|
+
};
|
|
1209
|
+
if (hasCycle(newTask.id)) {
|
|
1210
|
+
throw new Error(`Adding task ${newTask.id} would create a circular dependency`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// src/lifecycle/session-manager.ts
|
|
1216
|
+
var DEFAULT_BASE_PATH4 = "grimoires/anchor/sessions";
|
|
1217
|
+
var SessionManager = class {
|
|
1218
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1219
|
+
currentSession = null;
|
|
1220
|
+
basePath;
|
|
1221
|
+
constructor(config) {
|
|
1222
|
+
this.basePath = config?.basePath ?? DEFAULT_BASE_PATH4;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Initialize the manager by loading session index
|
|
1226
|
+
*/
|
|
1227
|
+
async init() {
|
|
1228
|
+
await this.loadSessionIndex();
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Create a new session
|
|
1232
|
+
*
|
|
1233
|
+
* @param network - Network to fork
|
|
1234
|
+
* @param options - Session options
|
|
1235
|
+
* @returns Created session
|
|
1236
|
+
*/
|
|
1237
|
+
async create(network, options) {
|
|
1238
|
+
const sessionId = this.generateSessionId();
|
|
1239
|
+
const forkManager = new ForkManager();
|
|
1240
|
+
await forkManager.init();
|
|
1241
|
+
const fork = await forkManager.fork({
|
|
1242
|
+
network,
|
|
1243
|
+
sessionId,
|
|
1244
|
+
...options?.blockNumber !== void 0 && { blockNumber: options.blockNumber }
|
|
1245
|
+
});
|
|
1246
|
+
const snapshotManager = new SnapshotManager();
|
|
1247
|
+
await snapshotManager.init(sessionId);
|
|
1248
|
+
const checkpointManager = new CheckpointManager();
|
|
1249
|
+
await checkpointManager.init(sessionId, fork.id);
|
|
1250
|
+
const taskGraph = new TaskGraph({
|
|
1251
|
+
sessionId,
|
|
1252
|
+
basePath: this.basePath,
|
|
1253
|
+
autoSave: true
|
|
1254
|
+
});
|
|
1255
|
+
await taskGraph.init();
|
|
1256
|
+
const initialSnapshot = await snapshotManager.create(
|
|
1257
|
+
{
|
|
1258
|
+
forkId: fork.id,
|
|
1259
|
+
sessionId,
|
|
1260
|
+
description: "Initial session snapshot"
|
|
1261
|
+
},
|
|
1262
|
+
fork.rpcUrl
|
|
1263
|
+
);
|
|
1264
|
+
const forkTask = {
|
|
1265
|
+
id: `fork-${fork.id}`,
|
|
1266
|
+
type: "fork",
|
|
1267
|
+
status: "complete",
|
|
1268
|
+
snapshotId: initialSnapshot.id,
|
|
1269
|
+
dependencies: [],
|
|
1270
|
+
input: { network, blockNumber: fork.blockNumber },
|
|
1271
|
+
output: { forkId: fork.id, rpcUrl: fork.rpcUrl },
|
|
1272
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1273
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
1274
|
+
};
|
|
1275
|
+
await taskGraph.addTask(forkTask);
|
|
1276
|
+
const metadata = {
|
|
1277
|
+
id: sessionId,
|
|
1278
|
+
network,
|
|
1279
|
+
forkId: fork.id,
|
|
1280
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1281
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
1282
|
+
status: "active",
|
|
1283
|
+
initialBlock: fork.blockNumber
|
|
1284
|
+
};
|
|
1285
|
+
this.sessions.set(sessionId, metadata);
|
|
1286
|
+
await this.saveSession(metadata);
|
|
1287
|
+
await this.saveSessionIndex();
|
|
1288
|
+
this.currentSession = {
|
|
1289
|
+
metadata,
|
|
1290
|
+
fork,
|
|
1291
|
+
forkManager,
|
|
1292
|
+
snapshotManager,
|
|
1293
|
+
checkpointManager,
|
|
1294
|
+
taskGraph
|
|
1295
|
+
};
|
|
1296
|
+
return this.currentSession;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Resume an existing session
|
|
1300
|
+
*
|
|
1301
|
+
* @param sessionId - Session ID to resume
|
|
1302
|
+
* @returns Resumed session
|
|
1303
|
+
*/
|
|
1304
|
+
async resume(sessionId) {
|
|
1305
|
+
const metadata = this.sessions.get(sessionId);
|
|
1306
|
+
if (!metadata) {
|
|
1307
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1308
|
+
}
|
|
1309
|
+
const forkManager = new ForkManager();
|
|
1310
|
+
await forkManager.init();
|
|
1311
|
+
const snapshotManager = new SnapshotManager();
|
|
1312
|
+
await snapshotManager.init(sessionId);
|
|
1313
|
+
const checkpointManager = new CheckpointManager();
|
|
1314
|
+
const taskGraph = new TaskGraph({
|
|
1315
|
+
sessionId,
|
|
1316
|
+
basePath: this.basePath,
|
|
1317
|
+
autoSave: true
|
|
1318
|
+
});
|
|
1319
|
+
await taskGraph.init();
|
|
1320
|
+
let fork = forkManager.get(metadata.forkId);
|
|
1321
|
+
if (!fork || taskGraph.hasBlocked()) {
|
|
1322
|
+
fork = await this.recover(
|
|
1323
|
+
sessionId,
|
|
1324
|
+
metadata,
|
|
1325
|
+
forkManager,
|
|
1326
|
+
snapshotManager,
|
|
1327
|
+
checkpointManager,
|
|
1328
|
+
taskGraph
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (!fork) {
|
|
1332
|
+
throw new Error(`Failed to restore fork for session ${sessionId}`);
|
|
1333
|
+
}
|
|
1334
|
+
await checkpointManager.init(sessionId, fork.id);
|
|
1335
|
+
metadata.lastActivity = /* @__PURE__ */ new Date();
|
|
1336
|
+
metadata.forkId = fork.id;
|
|
1337
|
+
metadata.status = "active";
|
|
1338
|
+
await this.saveSession(metadata);
|
|
1339
|
+
this.currentSession = {
|
|
1340
|
+
metadata,
|
|
1341
|
+
fork,
|
|
1342
|
+
forkManager,
|
|
1343
|
+
snapshotManager,
|
|
1344
|
+
checkpointManager,
|
|
1345
|
+
taskGraph
|
|
1346
|
+
};
|
|
1347
|
+
return this.currentSession;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Recover a session from checkpoint or snapshot
|
|
1351
|
+
*/
|
|
1352
|
+
async recover(sessionId, metadata, forkManager, snapshotManager, checkpointManager, taskGraph) {
|
|
1353
|
+
const latestCheckpoint = checkpointManager.latest();
|
|
1354
|
+
if (latestCheckpoint) {
|
|
1355
|
+
console.log(`Recovering session ${sessionId} from checkpoint ${latestCheckpoint.id}`);
|
|
1356
|
+
return await checkpointManager.restore(
|
|
1357
|
+
latestCheckpoint.id,
|
|
1358
|
+
forkManager,
|
|
1359
|
+
metadata.network
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const blockedTasks = taskGraph.getTasksByStatus("blocked");
|
|
1363
|
+
const failedTasks = taskGraph.getTasksByStatus("failed");
|
|
1364
|
+
const problematicTask = blockedTasks[0] ?? failedTasks[0];
|
|
1365
|
+
if (problematicTask) {
|
|
1366
|
+
const recoveryPoint = taskGraph.findRecoveryPoint(problematicTask.id);
|
|
1367
|
+
if (recoveryPoint?.snapshotId) {
|
|
1368
|
+
const fork = await forkManager.fork({
|
|
1369
|
+
network: metadata.network,
|
|
1370
|
+
blockNumber: metadata.initialBlock,
|
|
1371
|
+
sessionId
|
|
1372
|
+
});
|
|
1373
|
+
const success = await snapshotManager.revert(fork.rpcUrl, recoveryPoint.snapshotId);
|
|
1374
|
+
if (!success) {
|
|
1375
|
+
throw new Error(`Failed to revert to snapshot ${recoveryPoint.snapshotId}`);
|
|
1376
|
+
}
|
|
1377
|
+
for (const task of [...blockedTasks, ...failedTasks]) {
|
|
1378
|
+
await taskGraph.updateStatus(task.id, "pending");
|
|
1379
|
+
}
|
|
1380
|
+
return fork;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
console.log(`No recovery point found, creating fresh fork for session ${sessionId}`);
|
|
1384
|
+
return await forkManager.fork({
|
|
1385
|
+
network: metadata.network,
|
|
1386
|
+
blockNumber: metadata.initialBlock,
|
|
1387
|
+
sessionId
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Get current session
|
|
1392
|
+
*
|
|
1393
|
+
* @returns Current session or null
|
|
1394
|
+
*/
|
|
1395
|
+
current() {
|
|
1396
|
+
return this.currentSession;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* List all sessions
|
|
1400
|
+
*
|
|
1401
|
+
* @param filter - Optional filter for status
|
|
1402
|
+
* @returns Array of session metadata
|
|
1403
|
+
*/
|
|
1404
|
+
list(filter) {
|
|
1405
|
+
let sessions = Array.from(this.sessions.values());
|
|
1406
|
+
if (filter?.status) {
|
|
1407
|
+
sessions = sessions.filter((s) => s.status === filter.status);
|
|
1408
|
+
}
|
|
1409
|
+
return sessions.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime());
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Get session by ID
|
|
1413
|
+
*
|
|
1414
|
+
* @param sessionId - Session ID
|
|
1415
|
+
* @returns Session metadata if found
|
|
1416
|
+
*/
|
|
1417
|
+
get(sessionId) {
|
|
1418
|
+
return this.sessions.get(sessionId);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Update session status
|
|
1422
|
+
*
|
|
1423
|
+
* @param sessionId - Session ID
|
|
1424
|
+
* @param status - New status
|
|
1425
|
+
*/
|
|
1426
|
+
async updateStatus(sessionId, status) {
|
|
1427
|
+
const metadata = this.sessions.get(sessionId);
|
|
1428
|
+
if (!metadata) {
|
|
1429
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1430
|
+
}
|
|
1431
|
+
metadata.status = status;
|
|
1432
|
+
metadata.lastActivity = /* @__PURE__ */ new Date();
|
|
1433
|
+
await this.saveSession(metadata);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Get session directory path
|
|
1437
|
+
*/
|
|
1438
|
+
getSessionDir(sessionId) {
|
|
1439
|
+
return join(this.basePath, sessionId);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Get session metadata path
|
|
1443
|
+
*/
|
|
1444
|
+
getSessionPath(sessionId) {
|
|
1445
|
+
return join(this.getSessionDir(sessionId), "session.json");
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Load session index
|
|
1449
|
+
*/
|
|
1450
|
+
async loadSessionIndex() {
|
|
1451
|
+
if (!existsSync(this.basePath)) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const entries = await readdir(this.basePath, { withFileTypes: true });
|
|
1456
|
+
for (const entry of entries) {
|
|
1457
|
+
if (!entry.isDirectory())
|
|
1458
|
+
continue;
|
|
1459
|
+
const sessionPath = this.getSessionPath(entry.name);
|
|
1460
|
+
if (!existsSync(sessionPath))
|
|
1461
|
+
continue;
|
|
1462
|
+
try {
|
|
1463
|
+
const content = await readFile(sessionPath, "utf-8");
|
|
1464
|
+
const data = JSON.parse(content);
|
|
1465
|
+
data.createdAt = new Date(data.createdAt);
|
|
1466
|
+
data.lastActivity = new Date(data.lastActivity);
|
|
1467
|
+
this.sessions.set(data.id, data);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Save session index
|
|
1476
|
+
*/
|
|
1477
|
+
async saveSessionIndex() {
|
|
1478
|
+
if (!existsSync(this.basePath)) {
|
|
1479
|
+
await mkdir(this.basePath, { recursive: true });
|
|
1480
|
+
}
|
|
1481
|
+
const index = Array.from(this.sessions.values()).map((s) => ({
|
|
1482
|
+
id: s.id,
|
|
1483
|
+
status: s.status,
|
|
1484
|
+
lastActivity: s.lastActivity
|
|
1485
|
+
}));
|
|
1486
|
+
await writeFile(
|
|
1487
|
+
join(this.basePath, "index.json"),
|
|
1488
|
+
JSON.stringify(index, null, 2)
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Save session metadata
|
|
1493
|
+
*/
|
|
1494
|
+
async saveSession(metadata) {
|
|
1495
|
+
const dir = this.getSessionDir(metadata.id);
|
|
1496
|
+
if (!existsSync(dir)) {
|
|
1497
|
+
await mkdir(dir, { recursive: true });
|
|
1498
|
+
}
|
|
1499
|
+
await writeFile(this.getSessionPath(metadata.id), JSON.stringify(metadata, null, 2));
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Generate unique session ID
|
|
1503
|
+
*/
|
|
1504
|
+
generateSessionId() {
|
|
1505
|
+
const timestamp = Date.now().toString(36);
|
|
1506
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
1507
|
+
return `session-${timestamp}-${random}`;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
var defaultManager4 = null;
|
|
1511
|
+
function getSessionManager() {
|
|
1512
|
+
if (!defaultManager4) {
|
|
1513
|
+
defaultManager4 = new SessionManager();
|
|
1514
|
+
}
|
|
1515
|
+
return defaultManager4;
|
|
1516
|
+
}
|
|
1517
|
+
function resetSessionManager() {
|
|
1518
|
+
defaultManager4 = null;
|
|
1519
|
+
}
|
|
1520
|
+
var DEFAULT_PHYSICS_PATH = ".claude/rules/01-sigil-physics.md";
|
|
1521
|
+
var cachedPhysics = null;
|
|
1522
|
+
var cachedPath = null;
|
|
1523
|
+
function parseSyncStrategy(value) {
|
|
1524
|
+
const normalized = value.toLowerCase().trim();
|
|
1525
|
+
if (normalized === "pessimistic")
|
|
1526
|
+
return "pessimistic";
|
|
1527
|
+
if (normalized === "optimistic")
|
|
1528
|
+
return "optimistic";
|
|
1529
|
+
if (normalized === "immediate")
|
|
1530
|
+
return "immediate";
|
|
1531
|
+
return "optimistic";
|
|
1532
|
+
}
|
|
1533
|
+
function parseTiming(value) {
|
|
1534
|
+
const match = value.match(/(\d+)\s*ms/i);
|
|
1535
|
+
if (match && match[1]) {
|
|
1536
|
+
return parseInt(match[1], 10);
|
|
1537
|
+
}
|
|
1538
|
+
const num = parseInt(value, 10);
|
|
1539
|
+
return isNaN(num) ? 200 : num;
|
|
1540
|
+
}
|
|
1541
|
+
function parseConfirmation(value) {
|
|
1542
|
+
const normalized = value.toLowerCase().trim();
|
|
1543
|
+
if (normalized === "required" || normalized === "yes")
|
|
1544
|
+
return "required";
|
|
1545
|
+
if (normalized.includes("toast") || normalized.includes("undo"))
|
|
1546
|
+
return "toast_undo";
|
|
1547
|
+
if (normalized === "none" || normalized === "no")
|
|
1548
|
+
return "none";
|
|
1549
|
+
return "none";
|
|
1550
|
+
}
|
|
1551
|
+
function parseEffectType(value) {
|
|
1552
|
+
const normalized = value.toLowerCase().replace(/[\s-]/g, "_").trim();
|
|
1553
|
+
const mapping = {
|
|
1554
|
+
financial: "financial",
|
|
1555
|
+
destructive: "destructive",
|
|
1556
|
+
soft_delete: "soft_delete",
|
|
1557
|
+
"soft delete": "soft_delete",
|
|
1558
|
+
standard: "standard",
|
|
1559
|
+
navigation: "navigation",
|
|
1560
|
+
query: "query",
|
|
1561
|
+
local_state: "local",
|
|
1562
|
+
"local state": "local",
|
|
1563
|
+
local: "local",
|
|
1564
|
+
high_freq: "high_freq",
|
|
1565
|
+
"high-freq": "high_freq",
|
|
1566
|
+
highfreq: "high_freq"
|
|
1567
|
+
};
|
|
1568
|
+
return mapping[normalized] ?? null;
|
|
1569
|
+
}
|
|
1570
|
+
function parsePhysicsTable(content) {
|
|
1571
|
+
const physics = /* @__PURE__ */ new Map();
|
|
1572
|
+
const tableMatch = content.match(
|
|
1573
|
+
/<physics_table>[\s\S]*?\|[\s\S]*?<\/physics_table>/
|
|
1574
|
+
);
|
|
1575
|
+
if (!tableMatch) {
|
|
1576
|
+
console.warn("Physics table not found in content");
|
|
1577
|
+
return getDefaultPhysics();
|
|
1578
|
+
}
|
|
1579
|
+
const tableContent = tableMatch[0];
|
|
1580
|
+
const lines = tableContent.split("\n");
|
|
1581
|
+
for (const line of lines) {
|
|
1582
|
+
if (!line.includes("|") || line.includes("---") || line.includes("Effect")) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1586
|
+
if (cells.length < 4)
|
|
1587
|
+
continue;
|
|
1588
|
+
const effectStr = cells[0];
|
|
1589
|
+
const syncStr = cells[1];
|
|
1590
|
+
const timingStr = cells[2];
|
|
1591
|
+
const confirmStr = cells[3];
|
|
1592
|
+
const whyParts = cells.slice(4);
|
|
1593
|
+
if (!effectStr || !syncStr || !timingStr || !confirmStr)
|
|
1594
|
+
continue;
|
|
1595
|
+
const effect = parseEffectType(effectStr);
|
|
1596
|
+
if (!effect)
|
|
1597
|
+
continue;
|
|
1598
|
+
const rule = {
|
|
1599
|
+
effect,
|
|
1600
|
+
sync: parseSyncStrategy(syncStr),
|
|
1601
|
+
timing: parseTiming(timingStr),
|
|
1602
|
+
confirmation: parseConfirmation(confirmStr),
|
|
1603
|
+
rationale: whyParts.join(" ").trim()
|
|
1604
|
+
};
|
|
1605
|
+
physics.set(effect, rule);
|
|
1606
|
+
}
|
|
1607
|
+
return physics;
|
|
1608
|
+
}
|
|
1609
|
+
function getDefaultPhysics() {
|
|
1610
|
+
const physics = /* @__PURE__ */ new Map();
|
|
1611
|
+
physics.set("financial", {
|
|
1612
|
+
effect: "financial",
|
|
1613
|
+
sync: "pessimistic",
|
|
1614
|
+
timing: 800,
|
|
1615
|
+
confirmation: "required",
|
|
1616
|
+
rationale: "Money can't roll back. Users need time to verify."
|
|
1617
|
+
});
|
|
1618
|
+
physics.set("destructive", {
|
|
1619
|
+
effect: "destructive",
|
|
1620
|
+
sync: "pessimistic",
|
|
1621
|
+
timing: 600,
|
|
1622
|
+
confirmation: "required",
|
|
1623
|
+
rationale: "Permanent actions need deliberation."
|
|
1624
|
+
});
|
|
1625
|
+
physics.set("soft_delete", {
|
|
1626
|
+
effect: "soft_delete",
|
|
1627
|
+
sync: "optimistic",
|
|
1628
|
+
timing: 200,
|
|
1629
|
+
confirmation: "toast_undo",
|
|
1630
|
+
rationale: "Undo exists, so we can be fast."
|
|
1631
|
+
});
|
|
1632
|
+
physics.set("standard", {
|
|
1633
|
+
effect: "standard",
|
|
1634
|
+
sync: "optimistic",
|
|
1635
|
+
timing: 200,
|
|
1636
|
+
confirmation: "none",
|
|
1637
|
+
rationale: "Low stakes = snappy feedback."
|
|
1638
|
+
});
|
|
1639
|
+
physics.set("navigation", {
|
|
1640
|
+
effect: "navigation",
|
|
1641
|
+
sync: "immediate",
|
|
1642
|
+
timing: 150,
|
|
1643
|
+
confirmation: "none",
|
|
1644
|
+
rationale: "URL changes feel instant."
|
|
1645
|
+
});
|
|
1646
|
+
physics.set("query", {
|
|
1647
|
+
effect: "query",
|
|
1648
|
+
sync: "optimistic",
|
|
1649
|
+
timing: 150,
|
|
1650
|
+
confirmation: "none",
|
|
1651
|
+
rationale: "Data retrieval, no state change."
|
|
1652
|
+
});
|
|
1653
|
+
physics.set("local", {
|
|
1654
|
+
effect: "local",
|
|
1655
|
+
sync: "immediate",
|
|
1656
|
+
timing: 100,
|
|
1657
|
+
confirmation: "none",
|
|
1658
|
+
rationale: "No server = instant expected."
|
|
1659
|
+
});
|
|
1660
|
+
physics.set("high_freq", {
|
|
1661
|
+
effect: "high_freq",
|
|
1662
|
+
sync: "immediate",
|
|
1663
|
+
timing: 0,
|
|
1664
|
+
confirmation: "none",
|
|
1665
|
+
rationale: "Animation becomes friction."
|
|
1666
|
+
});
|
|
1667
|
+
return physics;
|
|
1668
|
+
}
|
|
1669
|
+
async function loadPhysics(path) {
|
|
1670
|
+
const physicsPath = path ?? DEFAULT_PHYSICS_PATH;
|
|
1671
|
+
if (cachedPhysics && cachedPath === physicsPath) {
|
|
1672
|
+
return cachedPhysics;
|
|
1673
|
+
}
|
|
1674
|
+
if (!existsSync(physicsPath)) {
|
|
1675
|
+
console.warn(`Physics file not found at ${physicsPath}, using defaults`);
|
|
1676
|
+
cachedPhysics = getDefaultPhysics();
|
|
1677
|
+
cachedPath = physicsPath;
|
|
1678
|
+
return cachedPhysics;
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
const content = await readFile(physicsPath, "utf-8");
|
|
1682
|
+
cachedPhysics = parsePhysicsTable(content);
|
|
1683
|
+
cachedPath = physicsPath;
|
|
1684
|
+
if (cachedPhysics.size === 0) {
|
|
1685
|
+
console.warn("No physics rules parsed, using defaults");
|
|
1686
|
+
cachedPhysics = getDefaultPhysics();
|
|
1687
|
+
}
|
|
1688
|
+
return cachedPhysics;
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
console.warn(`Error loading physics from ${physicsPath}:`, error);
|
|
1691
|
+
cachedPhysics = getDefaultPhysics();
|
|
1692
|
+
cachedPath = physicsPath;
|
|
1693
|
+
return cachedPhysics;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async function getPhysicsRule(effect, physics) {
|
|
1697
|
+
const table = physics ?? await loadPhysics();
|
|
1698
|
+
return table.get(effect);
|
|
1699
|
+
}
|
|
1700
|
+
function clearPhysicsCache() {
|
|
1701
|
+
cachedPhysics = null;
|
|
1702
|
+
cachedPath = null;
|
|
1703
|
+
}
|
|
1704
|
+
function isPhysicsCached() {
|
|
1705
|
+
return cachedPhysics !== null;
|
|
1706
|
+
}
|
|
1707
|
+
var DEFAULT_VOCABULARY_PATH = ".claude/rules/08-sigil-lexicon.md";
|
|
1708
|
+
var cachedVocabulary = null;
|
|
1709
|
+
var cachedPath2 = null;
|
|
1710
|
+
function parseKeywordsFromBlock(block) {
|
|
1711
|
+
const keywords = [];
|
|
1712
|
+
const lines = block.split("\n");
|
|
1713
|
+
for (const line of lines) {
|
|
1714
|
+
const colonIndex = line.indexOf(":");
|
|
1715
|
+
const content = colonIndex >= 0 ? line.slice(colonIndex + 1) : line;
|
|
1716
|
+
const words = content.split(/[,\s]+/).map((w) => w.trim().toLowerCase()).filter((w) => w.length > 0 && !w.includes("```"));
|
|
1717
|
+
keywords.push(...words);
|
|
1718
|
+
}
|
|
1719
|
+
return [...new Set(keywords)];
|
|
1720
|
+
}
|
|
1721
|
+
function parseEffectKeywords(content) {
|
|
1722
|
+
const effects = /* @__PURE__ */ new Map();
|
|
1723
|
+
const sectionMatch = content.match(
|
|
1724
|
+
/<effect_keywords>[\s\S]*?<\/effect_keywords>/
|
|
1725
|
+
);
|
|
1726
|
+
if (!sectionMatch) {
|
|
1727
|
+
return effects;
|
|
1728
|
+
}
|
|
1729
|
+
const section = sectionMatch[0];
|
|
1730
|
+
const effectPatterns = [
|
|
1731
|
+
{
|
|
1732
|
+
effect: "financial",
|
|
1733
|
+
pattern: /###\s*Financial[\s\S]*?```([\s\S]*?)```/i
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
effect: "destructive",
|
|
1737
|
+
pattern: /###\s*Destructive[\s\S]*?```([\s\S]*?)```/i
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
effect: "soft_delete",
|
|
1741
|
+
pattern: /###\s*Soft\s*Delete[\s\S]*?```([\s\S]*?)```/i
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
effect: "standard",
|
|
1745
|
+
pattern: /###\s*Standard[\s\S]*?```([\s\S]*?)```/i
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
effect: "local",
|
|
1749
|
+
pattern: /###\s*Local\s*State[\s\S]*?```([\s\S]*?)```/i
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
effect: "navigation",
|
|
1753
|
+
pattern: /###\s*Navigation[\s\S]*?```([\s\S]*?)```/i
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
effect: "query",
|
|
1757
|
+
pattern: /###\s*Query[\s\S]*?```([\s\S]*?)```/i
|
|
1758
|
+
}
|
|
1759
|
+
];
|
|
1760
|
+
for (const { effect, pattern } of effectPatterns) {
|
|
1761
|
+
const match = section.match(pattern);
|
|
1762
|
+
if (match && match[1]) {
|
|
1763
|
+
const keywords = parseKeywordsFromBlock(match[1]);
|
|
1764
|
+
if (keywords.length > 0) {
|
|
1765
|
+
effects.set(effect, {
|
|
1766
|
+
keywords,
|
|
1767
|
+
effect,
|
|
1768
|
+
category: "lexicon"
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return effects;
|
|
1774
|
+
}
|
|
1775
|
+
function parseTypeOverrides(content) {
|
|
1776
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1777
|
+
const sectionMatch = content.match(
|
|
1778
|
+
/<type_overrides>[\s\S]*?<\/type_overrides>/
|
|
1779
|
+
);
|
|
1780
|
+
if (!sectionMatch) {
|
|
1781
|
+
return overrides;
|
|
1782
|
+
}
|
|
1783
|
+
const section = sectionMatch[0];
|
|
1784
|
+
const lines = section.split("\n");
|
|
1785
|
+
for (const line of lines) {
|
|
1786
|
+
if (!line.includes("|") || line.includes("---") || line.includes("Type Pattern")) {
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1790
|
+
if (cells.length < 2)
|
|
1791
|
+
continue;
|
|
1792
|
+
const typePattern = cells[0];
|
|
1793
|
+
const forcedEffect = cells[1];
|
|
1794
|
+
if (!typePattern || !forcedEffect)
|
|
1795
|
+
continue;
|
|
1796
|
+
const types = typePattern.replace(/`/g, "").split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
|
|
1797
|
+
const effect = mapEffectString(forcedEffect);
|
|
1798
|
+
if (effect) {
|
|
1799
|
+
for (const type of types) {
|
|
1800
|
+
overrides.set(type, effect);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return overrides;
|
|
1805
|
+
}
|
|
1806
|
+
function parseDomainDefaults(content) {
|
|
1807
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
1808
|
+
const sectionMatch = content.match(
|
|
1809
|
+
/<domain_context>[\s\S]*?<\/domain_context>/
|
|
1810
|
+
);
|
|
1811
|
+
if (!sectionMatch) {
|
|
1812
|
+
return defaults;
|
|
1813
|
+
}
|
|
1814
|
+
const section = sectionMatch[0];
|
|
1815
|
+
const domainHeaderPattern = /###\s*([\w\/]+)\s*\n```([\s\S]*?)```/gi;
|
|
1816
|
+
let match;
|
|
1817
|
+
while ((match = domainHeaderPattern.exec(section)) !== null) {
|
|
1818
|
+
const domainName = match[1];
|
|
1819
|
+
const domainContent = match[2];
|
|
1820
|
+
if (!domainName || !domainContent)
|
|
1821
|
+
continue;
|
|
1822
|
+
const defaultMatch = domainContent.match(/Default:\s*([\w\s()]+)/i);
|
|
1823
|
+
if (defaultMatch && defaultMatch[1]) {
|
|
1824
|
+
const effect = mapEffectString(defaultMatch[1]);
|
|
1825
|
+
if (effect) {
|
|
1826
|
+
const keywordMatch = domainContent.match(/Keywords:\s*([\w,\s]+)/i);
|
|
1827
|
+
if (keywordMatch && keywordMatch[1]) {
|
|
1828
|
+
const keywords = keywordMatch[1].split(",").map((k) => k.trim().toLowerCase()).filter((k) => k.length > 0);
|
|
1829
|
+
for (const keyword of keywords) {
|
|
1830
|
+
defaults.set(keyword, effect);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
defaults.set(domainName.toLowerCase().replace("/", "_"), effect);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return defaults;
|
|
1838
|
+
}
|
|
1839
|
+
function mapEffectString(value) {
|
|
1840
|
+
const normalized = value.toLowerCase().trim();
|
|
1841
|
+
if (normalized.includes("financial"))
|
|
1842
|
+
return "financial";
|
|
1843
|
+
if (normalized.includes("destructive"))
|
|
1844
|
+
return "destructive";
|
|
1845
|
+
if (normalized.includes("soft") && normalized.includes("delete"))
|
|
1846
|
+
return "soft_delete";
|
|
1847
|
+
if (normalized.includes("standard"))
|
|
1848
|
+
return "standard";
|
|
1849
|
+
if (normalized.includes("local"))
|
|
1850
|
+
return "local";
|
|
1851
|
+
if (normalized.includes("navigation"))
|
|
1852
|
+
return "navigation";
|
|
1853
|
+
if (normalized.includes("query"))
|
|
1854
|
+
return "query";
|
|
1855
|
+
if (normalized.includes("immediate"))
|
|
1856
|
+
return "local";
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function getDefaultVocabulary() {
|
|
1860
|
+
const effects = /* @__PURE__ */ new Map();
|
|
1861
|
+
effects.set("financial", {
|
|
1862
|
+
keywords: [
|
|
1863
|
+
"claim",
|
|
1864
|
+
"deposit",
|
|
1865
|
+
"withdraw",
|
|
1866
|
+
"transfer",
|
|
1867
|
+
"swap",
|
|
1868
|
+
"send",
|
|
1869
|
+
"pay",
|
|
1870
|
+
"purchase",
|
|
1871
|
+
"mint",
|
|
1872
|
+
"burn",
|
|
1873
|
+
"stake",
|
|
1874
|
+
"unstake",
|
|
1875
|
+
"bridge",
|
|
1876
|
+
"approve",
|
|
1877
|
+
"redeem",
|
|
1878
|
+
"harvest"
|
|
1879
|
+
],
|
|
1880
|
+
effect: "financial",
|
|
1881
|
+
category: "default"
|
|
1882
|
+
});
|
|
1883
|
+
effects.set("destructive", {
|
|
1884
|
+
keywords: [
|
|
1885
|
+
"delete",
|
|
1886
|
+
"remove",
|
|
1887
|
+
"destroy",
|
|
1888
|
+
"revoke",
|
|
1889
|
+
"terminate",
|
|
1890
|
+
"purge",
|
|
1891
|
+
"erase",
|
|
1892
|
+
"wipe"
|
|
1893
|
+
],
|
|
1894
|
+
effect: "destructive",
|
|
1895
|
+
category: "default"
|
|
1896
|
+
});
|
|
1897
|
+
effects.set("soft_delete", {
|
|
1898
|
+
keywords: ["archive", "hide", "trash", "dismiss", "snooze", "mute"],
|
|
1899
|
+
effect: "soft_delete",
|
|
1900
|
+
category: "default"
|
|
1901
|
+
});
|
|
1902
|
+
effects.set("standard", {
|
|
1903
|
+
keywords: [
|
|
1904
|
+
"save",
|
|
1905
|
+
"update",
|
|
1906
|
+
"edit",
|
|
1907
|
+
"create",
|
|
1908
|
+
"add",
|
|
1909
|
+
"like",
|
|
1910
|
+
"follow",
|
|
1911
|
+
"bookmark"
|
|
1912
|
+
],
|
|
1913
|
+
effect: "standard",
|
|
1914
|
+
category: "default"
|
|
1915
|
+
});
|
|
1916
|
+
effects.set("local", {
|
|
1917
|
+
keywords: ["toggle", "switch", "expand", "collapse", "select", "focus"],
|
|
1918
|
+
effect: "local",
|
|
1919
|
+
category: "default"
|
|
1920
|
+
});
|
|
1921
|
+
effects.set("navigation", {
|
|
1922
|
+
keywords: ["navigate", "go", "back", "forward", "link", "route"],
|
|
1923
|
+
effect: "navigation",
|
|
1924
|
+
category: "default"
|
|
1925
|
+
});
|
|
1926
|
+
effects.set("query", {
|
|
1927
|
+
keywords: ["fetch", "load", "get", "list", "search", "find"],
|
|
1928
|
+
effect: "query",
|
|
1929
|
+
category: "default"
|
|
1930
|
+
});
|
|
1931
|
+
const typeOverrides = /* @__PURE__ */ new Map([
|
|
1932
|
+
["currency", "financial"],
|
|
1933
|
+
["money", "financial"],
|
|
1934
|
+
["amount", "financial"],
|
|
1935
|
+
["wei", "financial"],
|
|
1936
|
+
["bigint", "financial"],
|
|
1937
|
+
["token", "financial"],
|
|
1938
|
+
["balance", "financial"],
|
|
1939
|
+
["price", "financial"],
|
|
1940
|
+
["fee", "financial"],
|
|
1941
|
+
["password", "destructive"],
|
|
1942
|
+
["secret", "destructive"],
|
|
1943
|
+
["key", "destructive"],
|
|
1944
|
+
["permission", "destructive"],
|
|
1945
|
+
["role", "destructive"],
|
|
1946
|
+
["access", "destructive"],
|
|
1947
|
+
["theme", "local"],
|
|
1948
|
+
["preference", "local"],
|
|
1949
|
+
["setting", "local"],
|
|
1950
|
+
["filter", "local"],
|
|
1951
|
+
["sort", "local"],
|
|
1952
|
+
["view", "local"]
|
|
1953
|
+
]);
|
|
1954
|
+
const domainDefaults = /* @__PURE__ */ new Map([
|
|
1955
|
+
["wallet", "financial"],
|
|
1956
|
+
["token", "financial"],
|
|
1957
|
+
["nft", "financial"],
|
|
1958
|
+
["contract", "financial"],
|
|
1959
|
+
["chain", "financial"],
|
|
1960
|
+
["gas", "financial"],
|
|
1961
|
+
["cart", "standard"],
|
|
1962
|
+
["checkout", "financial"],
|
|
1963
|
+
["payment", "financial"]
|
|
1964
|
+
]);
|
|
1965
|
+
return { effects, typeOverrides, domainDefaults };
|
|
1966
|
+
}
|
|
1967
|
+
async function loadVocabulary(path) {
|
|
1968
|
+
const vocabPath = path ?? DEFAULT_VOCABULARY_PATH;
|
|
1969
|
+
if (cachedVocabulary && cachedPath2 === vocabPath) {
|
|
1970
|
+
return cachedVocabulary;
|
|
1971
|
+
}
|
|
1972
|
+
if (!existsSync(vocabPath)) {
|
|
1973
|
+
console.warn(`Vocabulary file not found at ${vocabPath}, using defaults`);
|
|
1974
|
+
cachedVocabulary = getDefaultVocabulary();
|
|
1975
|
+
cachedPath2 = vocabPath;
|
|
1976
|
+
return cachedVocabulary;
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
const content = await readFile(vocabPath, "utf-8");
|
|
1980
|
+
const effects = parseEffectKeywords(content);
|
|
1981
|
+
const typeOverrides = parseTypeOverrides(content);
|
|
1982
|
+
const domainDefaults = parseDomainDefaults(content);
|
|
1983
|
+
if (effects.size === 0) {
|
|
1984
|
+
console.warn("No vocabulary parsed, using defaults");
|
|
1985
|
+
cachedVocabulary = getDefaultVocabulary();
|
|
1986
|
+
} else {
|
|
1987
|
+
cachedVocabulary = { effects, typeOverrides, domainDefaults };
|
|
1988
|
+
}
|
|
1989
|
+
cachedPath2 = vocabPath;
|
|
1990
|
+
return cachedVocabulary;
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
console.warn(`Error loading vocabulary from ${vocabPath}:`, error);
|
|
1993
|
+
cachedVocabulary = getDefaultVocabulary();
|
|
1994
|
+
cachedPath2 = vocabPath;
|
|
1995
|
+
return cachedVocabulary;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async function resolveEffectFromKeywords(keywords, vocabulary) {
|
|
1999
|
+
const vocab = vocabulary ?? await loadVocabulary();
|
|
2000
|
+
const normalizedKeywords = keywords.map((k) => k.toLowerCase().trim());
|
|
2001
|
+
const priorityOrder = [
|
|
2002
|
+
"financial",
|
|
2003
|
+
"destructive",
|
|
2004
|
+
"soft_delete",
|
|
2005
|
+
"standard",
|
|
2006
|
+
"local",
|
|
2007
|
+
"navigation",
|
|
2008
|
+
"query",
|
|
2009
|
+
"high_freq"
|
|
2010
|
+
];
|
|
2011
|
+
for (const effect of priorityOrder) {
|
|
2012
|
+
const entry = vocab.effects.get(effect);
|
|
2013
|
+
if (entry) {
|
|
2014
|
+
for (const keyword of normalizedKeywords) {
|
|
2015
|
+
if (entry.keywords.includes(keyword)) {
|
|
2016
|
+
return effect;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
for (const keyword of normalizedKeywords) {
|
|
2022
|
+
const override = vocab.typeOverrides.get(keyword);
|
|
2023
|
+
if (override) {
|
|
2024
|
+
return override;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
for (const keyword of normalizedKeywords) {
|
|
2028
|
+
const domainDefault = vocab.domainDefaults.get(keyword);
|
|
2029
|
+
if (domainDefault) {
|
|
2030
|
+
return domainDefault;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
function clearVocabularyCache() {
|
|
2036
|
+
cachedVocabulary = null;
|
|
2037
|
+
cachedPath2 = null;
|
|
2038
|
+
}
|
|
2039
|
+
function isVocabularyCached() {
|
|
2040
|
+
return cachedVocabulary !== null;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/warden/grounding-gate.ts
|
|
2044
|
+
var ZONE_TO_EFFECT = {
|
|
2045
|
+
critical: "financial",
|
|
2046
|
+
elevated: "destructive",
|
|
2047
|
+
standard: "standard",
|
|
2048
|
+
local: "local"
|
|
2049
|
+
};
|
|
2050
|
+
var EFFECT_TO_ZONE = {
|
|
2051
|
+
financial: "critical",
|
|
2052
|
+
destructive: "elevated",
|
|
2053
|
+
soft_delete: "standard",
|
|
2054
|
+
standard: "standard",
|
|
2055
|
+
navigation: "local",
|
|
2056
|
+
query: "local",
|
|
2057
|
+
local: "local",
|
|
2058
|
+
high_freq: "local"
|
|
2059
|
+
};
|
|
2060
|
+
function parseZone(value) {
|
|
2061
|
+
const normalized = value.toLowerCase().trim();
|
|
2062
|
+
if (normalized === "critical")
|
|
2063
|
+
return "critical";
|
|
2064
|
+
if (normalized === "elevated")
|
|
2065
|
+
return "elevated";
|
|
2066
|
+
if (normalized === "standard")
|
|
2067
|
+
return "standard";
|
|
2068
|
+
if (normalized === "local")
|
|
2069
|
+
return "local";
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
function parseSyncStrategy2(value) {
|
|
2073
|
+
const normalized = value.toLowerCase().trim();
|
|
2074
|
+
if (normalized.includes("pessimistic"))
|
|
2075
|
+
return "pessimistic";
|
|
2076
|
+
if (normalized.includes("optimistic"))
|
|
2077
|
+
return "optimistic";
|
|
2078
|
+
if (normalized.includes("immediate"))
|
|
2079
|
+
return "immediate";
|
|
2080
|
+
return void 0;
|
|
2081
|
+
}
|
|
2082
|
+
function parseTiming2(value) {
|
|
2083
|
+
const match = value.match(/(\d+)\s*ms/i);
|
|
2084
|
+
if (match && match[1]) {
|
|
2085
|
+
return parseInt(match[1], 10);
|
|
2086
|
+
}
|
|
2087
|
+
return void 0;
|
|
2088
|
+
}
|
|
2089
|
+
function parseConfirmation2(value) {
|
|
2090
|
+
const normalized = value.toLowerCase().trim();
|
|
2091
|
+
if (normalized.includes("required") || normalized === "yes")
|
|
2092
|
+
return "required";
|
|
2093
|
+
if (normalized.includes("toast") || normalized.includes("undo"))
|
|
2094
|
+
return "toast_undo";
|
|
2095
|
+
if (normalized.includes("none") || normalized === "no")
|
|
2096
|
+
return "none";
|
|
2097
|
+
return void 0;
|
|
2098
|
+
}
|
|
2099
|
+
function extractKeywords(text) {
|
|
2100
|
+
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, "");
|
|
2101
|
+
const keywordPatterns = [
|
|
2102
|
+
// Financial
|
|
2103
|
+
"claim",
|
|
2104
|
+
"deposit",
|
|
2105
|
+
"withdraw",
|
|
2106
|
+
"transfer",
|
|
2107
|
+
"swap",
|
|
2108
|
+
"send",
|
|
2109
|
+
"pay",
|
|
2110
|
+
"purchase",
|
|
2111
|
+
"mint",
|
|
2112
|
+
"burn",
|
|
2113
|
+
"stake",
|
|
2114
|
+
"unstake",
|
|
2115
|
+
"bridge",
|
|
2116
|
+
"approve",
|
|
2117
|
+
"redeem",
|
|
2118
|
+
"harvest",
|
|
2119
|
+
// Destructive
|
|
2120
|
+
"delete",
|
|
2121
|
+
"remove",
|
|
2122
|
+
"destroy",
|
|
2123
|
+
"revoke",
|
|
2124
|
+
"terminate",
|
|
2125
|
+
"purge",
|
|
2126
|
+
"erase",
|
|
2127
|
+
"wipe",
|
|
2128
|
+
// Soft delete
|
|
2129
|
+
"archive",
|
|
2130
|
+
"hide",
|
|
2131
|
+
"trash",
|
|
2132
|
+
"dismiss",
|
|
2133
|
+
"snooze",
|
|
2134
|
+
"mute",
|
|
2135
|
+
// Standard
|
|
2136
|
+
"save",
|
|
2137
|
+
"update",
|
|
2138
|
+
"edit",
|
|
2139
|
+
"create",
|
|
2140
|
+
"add",
|
|
2141
|
+
"like",
|
|
2142
|
+
"follow",
|
|
2143
|
+
"bookmark",
|
|
2144
|
+
// Local
|
|
2145
|
+
"toggle",
|
|
2146
|
+
"switch",
|
|
2147
|
+
"expand",
|
|
2148
|
+
"collapse",
|
|
2149
|
+
"select",
|
|
2150
|
+
"focus",
|
|
2151
|
+
// Navigation
|
|
2152
|
+
"navigate",
|
|
2153
|
+
"go",
|
|
2154
|
+
"back",
|
|
2155
|
+
"forward",
|
|
2156
|
+
"link",
|
|
2157
|
+
"route",
|
|
2158
|
+
// Query
|
|
2159
|
+
"fetch",
|
|
2160
|
+
"load",
|
|
2161
|
+
"get",
|
|
2162
|
+
"list",
|
|
2163
|
+
"search",
|
|
2164
|
+
"find",
|
|
2165
|
+
// Domain/type hints
|
|
2166
|
+
"wallet",
|
|
2167
|
+
"token",
|
|
2168
|
+
"nft",
|
|
2169
|
+
"contract",
|
|
2170
|
+
"chain",
|
|
2171
|
+
"gas",
|
|
2172
|
+
"currency",
|
|
2173
|
+
"money",
|
|
2174
|
+
"amount",
|
|
2175
|
+
"balance",
|
|
2176
|
+
"price",
|
|
2177
|
+
"fee",
|
|
2178
|
+
// Effect type names (for inline detection in prose)
|
|
2179
|
+
"financial",
|
|
2180
|
+
"destructive"
|
|
2181
|
+
];
|
|
2182
|
+
const words = cleanedText.toLowerCase().split(/[\s,.:;!?()\[\]{}]+/);
|
|
2183
|
+
const foundKeywords = [];
|
|
2184
|
+
for (const word of words) {
|
|
2185
|
+
if (keywordPatterns.includes(word)) {
|
|
2186
|
+
foundKeywords.push(word);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return [...new Set(foundKeywords)];
|
|
2190
|
+
}
|
|
2191
|
+
function parseGroundingStatement(text) {
|
|
2192
|
+
const statement = {
|
|
2193
|
+
component: "",
|
|
2194
|
+
citedZone: null,
|
|
2195
|
+
detectedKeywords: [],
|
|
2196
|
+
inferredEffect: null,
|
|
2197
|
+
claimedPhysics: {},
|
|
2198
|
+
raw: text
|
|
2199
|
+
};
|
|
2200
|
+
const componentMatch = text.match(/(?:Component|Button|Modal|Form|Dialog):\s*["']?([^\s"'│|]+)/i);
|
|
2201
|
+
if (componentMatch && componentMatch[1]) {
|
|
2202
|
+
statement.component = componentMatch[1].trim();
|
|
2203
|
+
} else {
|
|
2204
|
+
const buttonMatch = text.match(/["']?(\w+(?:Button|Modal|Form|Dialog|Card|Input))["']?/);
|
|
2205
|
+
if (buttonMatch && buttonMatch[1]) {
|
|
2206
|
+
statement.component = buttonMatch[1];
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
const zoneMatch = text.match(/Zone:\s*(\w+)/i);
|
|
2210
|
+
if (zoneMatch && zoneMatch[1]) {
|
|
2211
|
+
statement.citedZone = parseZone(zoneMatch[1]);
|
|
2212
|
+
}
|
|
2213
|
+
const effectMatch = text.match(/Effect:\s*(\w+(?:\s+\w+)?)/i);
|
|
2214
|
+
if (effectMatch && effectMatch[1]) {
|
|
2215
|
+
const effectStr = effectMatch[1].toLowerCase();
|
|
2216
|
+
if (effectStr.includes("financial"))
|
|
2217
|
+
statement.inferredEffect = "financial";
|
|
2218
|
+
else if (effectStr.includes("destructive"))
|
|
2219
|
+
statement.inferredEffect = "destructive";
|
|
2220
|
+
else if (effectStr.includes("soft"))
|
|
2221
|
+
statement.inferredEffect = "soft_delete";
|
|
2222
|
+
else if (effectStr.includes("standard"))
|
|
2223
|
+
statement.inferredEffect = "standard";
|
|
2224
|
+
else if (effectStr.includes("local"))
|
|
2225
|
+
statement.inferredEffect = "local";
|
|
2226
|
+
else if (effectStr.includes("navigation"))
|
|
2227
|
+
statement.inferredEffect = "navigation";
|
|
2228
|
+
else if (effectStr.includes("query"))
|
|
2229
|
+
statement.inferredEffect = "query";
|
|
2230
|
+
}
|
|
2231
|
+
const syncMatch = text.match(/Sync:\s*(\w+)/i);
|
|
2232
|
+
if (syncMatch && syncMatch[1]) {
|
|
2233
|
+
const parsed = parseSyncStrategy2(syncMatch[1]);
|
|
2234
|
+
if (parsed)
|
|
2235
|
+
statement.claimedPhysics.sync = parsed;
|
|
2236
|
+
} else {
|
|
2237
|
+
if (text.toLowerCase().includes("pessimistic")) {
|
|
2238
|
+
statement.claimedPhysics.sync = "pessimistic";
|
|
2239
|
+
} else if (text.toLowerCase().includes("optimistic")) {
|
|
2240
|
+
statement.claimedPhysics.sync = "optimistic";
|
|
2241
|
+
} else if (text.toLowerCase().includes("immediate")) {
|
|
2242
|
+
statement.claimedPhysics.sync = "immediate";
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
const timingMatch = text.match(/Timing:\s*(\d+\s*ms)/i);
|
|
2246
|
+
if (timingMatch && timingMatch[1]) {
|
|
2247
|
+
const parsed = parseTiming2(timingMatch[1]);
|
|
2248
|
+
if (parsed !== void 0)
|
|
2249
|
+
statement.claimedPhysics.timing = parsed;
|
|
2250
|
+
} else {
|
|
2251
|
+
const inlineTiming = text.match(/(\d+)\s*ms/);
|
|
2252
|
+
if (inlineTiming && inlineTiming[1]) {
|
|
2253
|
+
statement.claimedPhysics.timing = parseInt(inlineTiming[1], 10);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const confirmMatch = text.match(/Confirm(?:ation)?:\s*(\w+(?:\s*\+\s*\w+)?)/i);
|
|
2257
|
+
if (confirmMatch && confirmMatch[1]) {
|
|
2258
|
+
const parsed = parseConfirmation2(confirmMatch[1]);
|
|
2259
|
+
if (parsed)
|
|
2260
|
+
statement.claimedPhysics.confirmation = parsed;
|
|
2261
|
+
} else {
|
|
2262
|
+
if (text.toLowerCase().includes("confirmation required")) {
|
|
2263
|
+
statement.claimedPhysics.confirmation = "required";
|
|
2264
|
+
} else if (text.toLowerCase().includes("toast") && text.toLowerCase().includes("undo")) {
|
|
2265
|
+
statement.claimedPhysics.confirmation = "toast_undo";
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
statement.detectedKeywords = extractKeywords(text);
|
|
2269
|
+
return statement;
|
|
2270
|
+
}
|
|
2271
|
+
function determineRequiredZone(keywords, effect, vocabulary) {
|
|
2272
|
+
if (effect) {
|
|
2273
|
+
return EFFECT_TO_ZONE[effect];
|
|
2274
|
+
}
|
|
2275
|
+
for (const keyword of keywords) {
|
|
2276
|
+
const override = vocabulary.typeOverrides.get(keyword.toLowerCase());
|
|
2277
|
+
if (override) {
|
|
2278
|
+
return EFFECT_TO_ZONE[override];
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const financialKeywords = vocabulary.effects.get("financial")?.keywords ?? [];
|
|
2282
|
+
for (const keyword of keywords) {
|
|
2283
|
+
if (financialKeywords.includes(keyword.toLowerCase())) {
|
|
2284
|
+
return "critical";
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
const destructiveKeywords = vocabulary.effects.get("destructive")?.keywords ?? [];
|
|
2288
|
+
for (const keyword of keywords) {
|
|
2289
|
+
if (destructiveKeywords.includes(keyword.toLowerCase())) {
|
|
2290
|
+
return "elevated";
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return "standard";
|
|
2294
|
+
}
|
|
2295
|
+
function checkRelevance(statement, vocabulary) {
|
|
2296
|
+
const { detectedKeywords, component } = statement;
|
|
2297
|
+
if (detectedKeywords.length === 0) {
|
|
2298
|
+
const componentLower = component.toLowerCase();
|
|
2299
|
+
const allKeywords = [];
|
|
2300
|
+
for (const entry of vocabulary.effects.values()) {
|
|
2301
|
+
allKeywords.push(...entry.keywords);
|
|
2302
|
+
}
|
|
2303
|
+
const hasRelevantComponent = allKeywords.some((k) => componentLower.includes(k));
|
|
2304
|
+
if (!hasRelevantComponent) {
|
|
2305
|
+
return {
|
|
2306
|
+
passed: false,
|
|
2307
|
+
reason: "No relevant keywords detected in statement or component name"
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return {
|
|
2312
|
+
passed: true,
|
|
2313
|
+
reason: `Keywords detected: ${detectedKeywords.join(", ") || "from component name"}`
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
function checkHierarchy(statement, requiredZone) {
|
|
2317
|
+
const { citedZone } = statement;
|
|
2318
|
+
if (!citedZone) {
|
|
2319
|
+
return {
|
|
2320
|
+
passed: false,
|
|
2321
|
+
reason: "No zone cited in statement"
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
const requiredIndex = ZONE_HIERARCHY.indexOf(requiredZone);
|
|
2325
|
+
const citedIndex = ZONE_HIERARCHY.indexOf(citedZone);
|
|
2326
|
+
if (citedIndex > requiredIndex) {
|
|
2327
|
+
return {
|
|
2328
|
+
passed: false,
|
|
2329
|
+
reason: `Zone "${citedZone}" is less restrictive than required "${requiredZone}"`
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
if (citedIndex < requiredIndex) {
|
|
2333
|
+
return {
|
|
2334
|
+
passed: true,
|
|
2335
|
+
reason: `Zone "${citedZone}" is more restrictive than required "${requiredZone}" (OK)`
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
return {
|
|
2339
|
+
passed: true,
|
|
2340
|
+
reason: `Zone "${citedZone}" matches required zone`
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
async function checkRules(statement, requiredZone, physics) {
|
|
2344
|
+
const { claimedPhysics } = statement;
|
|
2345
|
+
const requiredEffect = ZONE_TO_EFFECT[requiredZone];
|
|
2346
|
+
const rule = physics.get(requiredEffect);
|
|
2347
|
+
if (!rule) {
|
|
2348
|
+
return {
|
|
2349
|
+
passed: false,
|
|
2350
|
+
reason: `No physics rule found for effect "${requiredEffect}"`
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
const violations = [];
|
|
2354
|
+
if (claimedPhysics.sync && claimedPhysics.sync !== rule.sync) {
|
|
2355
|
+
if (claimedPhysics.sync !== "pessimistic") {
|
|
2356
|
+
violations.push(`Sync: claimed "${claimedPhysics.sync}", required "${rule.sync}"`);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
if (claimedPhysics.timing !== void 0) {
|
|
2360
|
+
const timingDiff = Math.abs(claimedPhysics.timing - rule.timing);
|
|
2361
|
+
if (timingDiff > 100 && claimedPhysics.timing < rule.timing) {
|
|
2362
|
+
violations.push(
|
|
2363
|
+
`Timing: claimed ${claimedPhysics.timing}ms, required minimum ${rule.timing}ms`
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (claimedPhysics.confirmation) {
|
|
2368
|
+
if (rule.confirmation === "required" && claimedPhysics.confirmation !== "required") {
|
|
2369
|
+
violations.push(
|
|
2370
|
+
`Confirmation: claimed "${claimedPhysics.confirmation}", required "${rule.confirmation}"`
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (violations.length > 0) {
|
|
2375
|
+
return {
|
|
2376
|
+
passed: false,
|
|
2377
|
+
reason: violations.join("; ")
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
return {
|
|
2381
|
+
passed: true,
|
|
2382
|
+
reason: "Physics rules validated"
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
async function validateGrounding(input, options) {
|
|
2386
|
+
const physics = await loadPhysics(options?.physicsPath);
|
|
2387
|
+
const vocabulary = await loadVocabulary(options?.vocabularyPath);
|
|
2388
|
+
const statement = typeof input === "string" ? parseGroundingStatement(input) : input;
|
|
2389
|
+
if (!statement.inferredEffect && statement.detectedKeywords.length > 0) {
|
|
2390
|
+
statement.inferredEffect = await resolveEffectFromKeywords(
|
|
2391
|
+
statement.detectedKeywords,
|
|
2392
|
+
vocabulary
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
const requiredZone = determineRequiredZone(
|
|
2396
|
+
statement.detectedKeywords,
|
|
2397
|
+
statement.inferredEffect,
|
|
2398
|
+
vocabulary
|
|
2399
|
+
);
|
|
2400
|
+
const relevanceCheck = checkRelevance(statement, vocabulary);
|
|
2401
|
+
const hierarchyCheck = checkHierarchy(statement, requiredZone);
|
|
2402
|
+
const rulesCheck = await checkRules(statement, requiredZone, physics);
|
|
2403
|
+
let status = "VALID";
|
|
2404
|
+
let correction;
|
|
2405
|
+
if (!relevanceCheck.passed) {
|
|
2406
|
+
status = "DRIFT";
|
|
2407
|
+
correction = "Statement lacks relevant keywords for effect detection.";
|
|
2408
|
+
} else if (!hierarchyCheck.passed) {
|
|
2409
|
+
status = "DECEPTIVE";
|
|
2410
|
+
correction = `Zone mismatch: cited "${statement.citedZone}", required "${requiredZone}".`;
|
|
2411
|
+
} else if (!rulesCheck.passed) {
|
|
2412
|
+
status = "DRIFT";
|
|
2413
|
+
correction = `Physics violation: ${rulesCheck.reason}`;
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
status,
|
|
2417
|
+
checks: {
|
|
2418
|
+
relevance: relevanceCheck,
|
|
2419
|
+
hierarchy: hierarchyCheck,
|
|
2420
|
+
rules: rulesCheck
|
|
2421
|
+
},
|
|
2422
|
+
requiredZone,
|
|
2423
|
+
citedZone: statement.citedZone,
|
|
2424
|
+
...correction !== void 0 && { correction }
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
async function isGroundingValid(input, options) {
|
|
2428
|
+
const result = await validateGrounding(input, options);
|
|
2429
|
+
return result.status === "VALID";
|
|
2430
|
+
}
|
|
2431
|
+
function getExitCode(result) {
|
|
2432
|
+
switch (result.status) {
|
|
2433
|
+
case "VALID":
|
|
2434
|
+
return 0;
|
|
2435
|
+
case "DRIFT":
|
|
2436
|
+
return 1;
|
|
2437
|
+
case "DECEPTIVE":
|
|
2438
|
+
return 2;
|
|
2439
|
+
default:
|
|
2440
|
+
return 3;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/index.ts
|
|
2445
|
+
var VERSION = "4.3.1";
|
|
2446
|
+
|
|
2447
|
+
export { CheckpointManager, ExitCode, ForkManager, ForkRegistrySchema, ForkSchema, RpcError, RpcTimeoutError, SessionManager, SnapshotManager, TaskGraph, VERSION, ZONE_HIERARCHY, clearPhysicsCache, clearVocabularyCache, generateTaskId, getCheckpointManager, getDefaultPhysics, getDefaultVocabulary, getExitCode, getForkManager, getPhysicsRule, getSessionManager, getSnapshotManager, isGroundingValid, isPhysicsCached, isRpcReady, isVocabularyCached, loadPhysics, loadVocabulary, parseGroundingStatement, resetCheckpointManager, resetForkManager, resetSessionManager, resetSnapshotManager, resetTaskCounter, resolveEffectFromKeywords, rpcCall, validateGrounding, waitForRpc };
|
|
2448
|
+
//# sourceMappingURL=out.js.map
|
|
2449
|
+
//# sourceMappingURL=index.js.map
|