ctx-mcp 0.1.0
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/README.md +169 -0
- package/migrations/001_init.sql +52 -0
- package/migrations/002_fts.sql +27 -0
- package/package.json +33 -0
- package/scripts/risk-check.ts +46 -0
- package/scripts/seed-trace.ts +144 -0
- package/scripts/similarity.ts +35 -0
- package/src/explain/explain.ts +125 -0
- package/src/graph/graph.ts +153 -0
- package/src/mcp/resources.ts +234 -0
- package/src/mcp/tools.ts +522 -0
- package/src/schemas.ts +238 -0
- package/src/server.ts +38 -0
- package/src/similarity/similarity.ts +299 -0
- package/src/storage/db.ts +17 -0
- package/src/storage/engine.ts +14 -0
- package/src/storage/migrations.ts +19 -0
- package/src/storage/queries.ts +294 -0
- package/src/storage/sqljsEngine.ts +207 -0
- package/src/util/fs.ts +15 -0
- package/src/util/hashing.ts +5 -0
- package/src/util/ids.ts +8 -0
- package/src/util/redact.ts +26 -0
- package/tests/explain.test.ts +81 -0
- package/tests/graph.test.ts +85 -0
- package/tests/query.test.ts +275 -0
- package/tests/similarity.test.ts +103 -0
- package/tests/storage.test.ts +91 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildGraph, extractSubgraph, findPaths } from "../src/graph/graph.js";
|
|
3
|
+
import type { EdgeRow, NodeRow } from "../src/storage/queries.js";
|
|
4
|
+
|
|
5
|
+
const nodes: NodeRow[] = [
|
|
6
|
+
{
|
|
7
|
+
node_id: "a",
|
|
8
|
+
trace_id: "t1",
|
|
9
|
+
type: "Intent",
|
|
10
|
+
summary: "Start",
|
|
11
|
+
data_json: "{}",
|
|
12
|
+
confidence: null,
|
|
13
|
+
metadata_json: "{}",
|
|
14
|
+
created_at: 1,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
node_id: "b",
|
|
18
|
+
trace_id: "t1",
|
|
19
|
+
type: "Decision",
|
|
20
|
+
summary: "Decide",
|
|
21
|
+
data_json: "{}",
|
|
22
|
+
confidence: null,
|
|
23
|
+
metadata_json: "{}",
|
|
24
|
+
created_at: 2,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
node_id: "c",
|
|
28
|
+
trace_id: "t1",
|
|
29
|
+
type: "Action",
|
|
30
|
+
summary: "Act",
|
|
31
|
+
data_json: "{}",
|
|
32
|
+
confidence: null,
|
|
33
|
+
metadata_json: "{}",
|
|
34
|
+
created_at: 3,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const edges: EdgeRow[] = [
|
|
39
|
+
{
|
|
40
|
+
edge_id: "e1",
|
|
41
|
+
trace_id: "t1",
|
|
42
|
+
from_node_id: "a",
|
|
43
|
+
to_node_id: "b",
|
|
44
|
+
relation_type: "justified_by",
|
|
45
|
+
data_json: "{}",
|
|
46
|
+
created_at: 1,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
edge_id: "e2",
|
|
50
|
+
trace_id: "t1",
|
|
51
|
+
from_node_id: "b",
|
|
52
|
+
to_node_id: "c",
|
|
53
|
+
relation_type: "causes",
|
|
54
|
+
data_json: "{}",
|
|
55
|
+
created_at: 2,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
describe("graph", () => {
|
|
60
|
+
it("extracts a subgraph around a node", () => {
|
|
61
|
+
const graph = buildGraph(nodes, edges);
|
|
62
|
+
const subgraph = extractSubgraph(graph, {
|
|
63
|
+
center: "b",
|
|
64
|
+
depth: 1,
|
|
65
|
+
direction: "both",
|
|
66
|
+
maxNodes: 10,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(subgraph.nodes.map((n) => n.node_id).sort()).toEqual(["a", "b", "c"]);
|
|
70
|
+
expect(subgraph.edges).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("finds paths between nodes", () => {
|
|
74
|
+
const graph = buildGraph(nodes, edges);
|
|
75
|
+
const paths = findPaths(graph, {
|
|
76
|
+
from: "a",
|
|
77
|
+
to: "c",
|
|
78
|
+
maxDepth: 3,
|
|
79
|
+
maxPaths: 5,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(paths).toHaveLength(1);
|
|
83
|
+
expect(paths[0].node_ids).toEqual(["a", "b", "c"]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { SqlJsEngine } from "../src/storage/sqljsEngine.js";
|
|
7
|
+
import { insertNode, insertTrace, type NodeRow, type TraceRow } from "../src/storage/queries.js";
|
|
8
|
+
import { registerTools } from "../src/mcp/tools.js";
|
|
9
|
+
|
|
10
|
+
function makeTempDbPath(): string {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ctx-query-"));
|
|
12
|
+
return path.join(dir, "trace.db");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("trace.query tool", () => {
|
|
16
|
+
it("filters by tag and text across traces", async () => {
|
|
17
|
+
const dbPath = makeTempDbPath();
|
|
18
|
+
const engine = new SqlJsEngine({ dbPath, onLog: () => undefined });
|
|
19
|
+
await engine.init();
|
|
20
|
+
|
|
21
|
+
const traceWithTag: TraceRow = {
|
|
22
|
+
trace_id: "trace-1",
|
|
23
|
+
workflow_id: null,
|
|
24
|
+
actor: "tester",
|
|
25
|
+
intent: "demo",
|
|
26
|
+
tags_json: JSON.stringify(["alpha", "beta"]),
|
|
27
|
+
metadata_json: "{}",
|
|
28
|
+
started_at: Date.now(),
|
|
29
|
+
finished_at: null,
|
|
30
|
+
status: null,
|
|
31
|
+
outcome_json: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const traceWithoutTag: TraceRow = {
|
|
35
|
+
trace_id: "trace-2",
|
|
36
|
+
workflow_id: null,
|
|
37
|
+
actor: "tester",
|
|
38
|
+
intent: "demo",
|
|
39
|
+
tags_json: JSON.stringify(["gamma"]),
|
|
40
|
+
metadata_json: "{}",
|
|
41
|
+
started_at: Date.now(),
|
|
42
|
+
finished_at: null,
|
|
43
|
+
status: null,
|
|
44
|
+
outcome_json: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const nodeMatch: NodeRow = {
|
|
48
|
+
node_id: "node-1",
|
|
49
|
+
trace_id: "trace-1",
|
|
50
|
+
type: "Observation",
|
|
51
|
+
summary: "Searchable text",
|
|
52
|
+
data_json: JSON.stringify({ note: "hit" }),
|
|
53
|
+
confidence: null,
|
|
54
|
+
metadata_json: "{}",
|
|
55
|
+
created_at: Date.now(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const nodeNoMatch: NodeRow = {
|
|
59
|
+
node_id: "node-2",
|
|
60
|
+
trace_id: "trace-2",
|
|
61
|
+
type: "Observation",
|
|
62
|
+
summary: "Other text",
|
|
63
|
+
data_json: "{}",
|
|
64
|
+
confidence: null,
|
|
65
|
+
metadata_json: "{}",
|
|
66
|
+
created_at: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
engine.transaction(() => {
|
|
70
|
+
insertTrace(engine, traceWithTag);
|
|
71
|
+
insertTrace(engine, traceWithoutTag);
|
|
72
|
+
insertNode(engine, nodeMatch);
|
|
73
|
+
insertNode(engine, nodeNoMatch);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const handlers = new Map<string, (input: unknown) => Promise<unknown>>();
|
|
77
|
+
const fakeServer = {
|
|
78
|
+
tool: (name: string, _schema: unknown, handler: (input: unknown) => Promise<unknown>) => {
|
|
79
|
+
handlers.set(name, handler);
|
|
80
|
+
},
|
|
81
|
+
} as any;
|
|
82
|
+
|
|
83
|
+
registerTools(fakeServer, engine);
|
|
84
|
+
|
|
85
|
+
const handler = handlers.get("trace.query");
|
|
86
|
+
if (!handler) throw new Error("trace.query not registered");
|
|
87
|
+
|
|
88
|
+
const result = (await handler({ tags: ["alpha"], text: "Searchable" })) as {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
nodes: Array<{ node_id: string; data: Record<string, unknown> }>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(result.ok).toBe(true);
|
|
94
|
+
expect(result.nodes).toHaveLength(1);
|
|
95
|
+
expect(result.nodes[0].node_id).toBe("node-1");
|
|
96
|
+
expect(result.nodes[0].data.note).toBe("hit");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("respects tags when trace_id is provided", async () => {
|
|
100
|
+
const dbPath = makeTempDbPath();
|
|
101
|
+
const engine = new SqlJsEngine({ dbPath, onLog: () => undefined });
|
|
102
|
+
await engine.init();
|
|
103
|
+
|
|
104
|
+
const traceWithTag: TraceRow = {
|
|
105
|
+
trace_id: "trace-1",
|
|
106
|
+
workflow_id: null,
|
|
107
|
+
actor: "tester",
|
|
108
|
+
intent: "demo",
|
|
109
|
+
tags_json: JSON.stringify(["alpha"]),
|
|
110
|
+
metadata_json: "{}",
|
|
111
|
+
started_at: Date.now(),
|
|
112
|
+
finished_at: null,
|
|
113
|
+
status: null,
|
|
114
|
+
outcome_json: null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const traceWithoutTag: TraceRow = {
|
|
118
|
+
trace_id: "trace-2",
|
|
119
|
+
workflow_id: null,
|
|
120
|
+
actor: "tester",
|
|
121
|
+
intent: "demo",
|
|
122
|
+
tags_json: JSON.stringify(["beta"]),
|
|
123
|
+
metadata_json: "{}",
|
|
124
|
+
started_at: Date.now(),
|
|
125
|
+
finished_at: null,
|
|
126
|
+
status: null,
|
|
127
|
+
outcome_json: null,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const nodeMatch: NodeRow = {
|
|
131
|
+
node_id: "node-1",
|
|
132
|
+
trace_id: "trace-1",
|
|
133
|
+
type: "Observation",
|
|
134
|
+
summary: "Searchable text",
|
|
135
|
+
data_json: JSON.stringify({ note: "hit" }),
|
|
136
|
+
confidence: null,
|
|
137
|
+
metadata_json: "{}",
|
|
138
|
+
created_at: Date.now(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
engine.transaction(() => {
|
|
142
|
+
insertTrace(engine, traceWithTag);
|
|
143
|
+
insertTrace(engine, traceWithoutTag);
|
|
144
|
+
insertNode(engine, nodeMatch);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const handlers = new Map<string, (input: unknown) => Promise<unknown>>();
|
|
148
|
+
const fakeServer = {
|
|
149
|
+
tool: (name: string, _schema: unknown, handler: (input: unknown) => Promise<unknown>) => {
|
|
150
|
+
handlers.set(name, handler);
|
|
151
|
+
},
|
|
152
|
+
} as any;
|
|
153
|
+
|
|
154
|
+
registerTools(fakeServer, engine);
|
|
155
|
+
|
|
156
|
+
const handler = handlers.get("trace.query");
|
|
157
|
+
if (!handler) throw new Error("trace.query not registered");
|
|
158
|
+
|
|
159
|
+
const matchResult = (await handler({
|
|
160
|
+
trace_id: "trace-1",
|
|
161
|
+
tags: ["alpha"],
|
|
162
|
+
})) as {
|
|
163
|
+
ok: boolean;
|
|
164
|
+
nodes: Array<{ node_id: string }>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
expect(matchResult.ok).toBe(true);
|
|
168
|
+
expect(matchResult.nodes).toHaveLength(1);
|
|
169
|
+
expect(matchResult.nodes[0].node_id).toBe("node-1");
|
|
170
|
+
|
|
171
|
+
const noMatchResult = (await handler({
|
|
172
|
+
trace_id: "trace-1",
|
|
173
|
+
tags: ["beta"],
|
|
174
|
+
})) as {
|
|
175
|
+
ok: boolean;
|
|
176
|
+
nodes: Array<{ node_id: string }>;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(noMatchResult.ok).toBe(true);
|
|
180
|
+
expect(noMatchResult.nodes).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("paginates tag queries across traces with global ordering", async () => {
|
|
184
|
+
const dbPath = makeTempDbPath();
|
|
185
|
+
const engine = new SqlJsEngine({ dbPath, onLog: () => undefined });
|
|
186
|
+
await engine.init();
|
|
187
|
+
|
|
188
|
+
const traceA: TraceRow = {
|
|
189
|
+
trace_id: "trace-a",
|
|
190
|
+
workflow_id: null,
|
|
191
|
+
actor: "tester",
|
|
192
|
+
intent: "demo",
|
|
193
|
+
tags_json: JSON.stringify(["alpha"]),
|
|
194
|
+
metadata_json: "{}",
|
|
195
|
+
started_at: 1,
|
|
196
|
+
finished_at: null,
|
|
197
|
+
status: null,
|
|
198
|
+
outcome_json: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const traceB: TraceRow = {
|
|
202
|
+
trace_id: "trace-b",
|
|
203
|
+
workflow_id: null,
|
|
204
|
+
actor: "tester",
|
|
205
|
+
intent: "demo",
|
|
206
|
+
tags_json: JSON.stringify(["alpha"]),
|
|
207
|
+
metadata_json: "{}",
|
|
208
|
+
started_at: 2,
|
|
209
|
+
finished_at: null,
|
|
210
|
+
status: null,
|
|
211
|
+
outcome_json: null,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const olderNode: NodeRow = {
|
|
215
|
+
node_id: "node-old",
|
|
216
|
+
trace_id: "trace-a",
|
|
217
|
+
type: "Observation",
|
|
218
|
+
summary: "Older",
|
|
219
|
+
data_json: "{}",
|
|
220
|
+
confidence: null,
|
|
221
|
+
metadata_json: "{}",
|
|
222
|
+
created_at: 100,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const newerNode: NodeRow = {
|
|
226
|
+
node_id: "node-new",
|
|
227
|
+
trace_id: "trace-b",
|
|
228
|
+
type: "Observation",
|
|
229
|
+
summary: "Newer",
|
|
230
|
+
data_json: "{}",
|
|
231
|
+
confidence: null,
|
|
232
|
+
metadata_json: "{}",
|
|
233
|
+
created_at: 200,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
engine.transaction(() => {
|
|
237
|
+
insertTrace(engine, traceA);
|
|
238
|
+
insertTrace(engine, traceB);
|
|
239
|
+
insertNode(engine, olderNode);
|
|
240
|
+
insertNode(engine, newerNode);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const handlers = new Map<string, (input: unknown) => Promise<unknown>>();
|
|
244
|
+
const fakeServer = {
|
|
245
|
+
tool: (name: string, _schema: unknown, handler: (input: unknown) => Promise<unknown>) => {
|
|
246
|
+
handlers.set(name, handler);
|
|
247
|
+
},
|
|
248
|
+
} as any;
|
|
249
|
+
|
|
250
|
+
registerTools(fakeServer, engine);
|
|
251
|
+
|
|
252
|
+
const handler = handlers.get("trace.query");
|
|
253
|
+
if (!handler) throw new Error("trace.query not registered");
|
|
254
|
+
|
|
255
|
+
const firstPage = (await handler({
|
|
256
|
+
tags: ["alpha"],
|
|
257
|
+
limit: 1,
|
|
258
|
+
offset: 0,
|
|
259
|
+
})) as { ok: boolean; nodes: Array<{ node_id: string }> };
|
|
260
|
+
|
|
261
|
+
const secondPage = (await handler({
|
|
262
|
+
tags: ["alpha"],
|
|
263
|
+
limit: 1,
|
|
264
|
+
offset: 1,
|
|
265
|
+
})) as { ok: boolean; nodes: Array<{ node_id: string }> };
|
|
266
|
+
|
|
267
|
+
expect(firstPage.ok).toBe(true);
|
|
268
|
+
expect(firstPage.nodes).toHaveLength(1);
|
|
269
|
+
expect(firstPage.nodes[0].node_id).toBe("node-new");
|
|
270
|
+
|
|
271
|
+
expect(secondPage.ok).toBe(true);
|
|
272
|
+
expect(secondPage.nodes).toHaveLength(1);
|
|
273
|
+
expect(secondPage.nodes[0].node_id).toBe("node-old");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { SqlJsEngine } from "../src/storage/sqljsEngine.js";
|
|
6
|
+
import {
|
|
7
|
+
insertNode,
|
|
8
|
+
insertTrace,
|
|
9
|
+
type NodeRow,
|
|
10
|
+
type TraceRow,
|
|
11
|
+
} from "../src/storage/queries.js";
|
|
12
|
+
import { assessDecisionRisk, findSimilarDecisions } from "../src/similarity/similarity.js";
|
|
13
|
+
|
|
14
|
+
function makeTempDbPath(): string {
|
|
15
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ctx-similarity-"));
|
|
16
|
+
return path.join(dir, "trace.db");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeEngine(): SqlJsEngine {
|
|
20
|
+
return new SqlJsEngine({ dbPath: makeTempDbPath(), onLog: () => undefined });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("similarity and risk", () => {
|
|
24
|
+
it("detects similar decisions and failed matches", async () => {
|
|
25
|
+
const engine = makeEngine();
|
|
26
|
+
await engine.init();
|
|
27
|
+
|
|
28
|
+
const traceA: TraceRow = {
|
|
29
|
+
trace_id: "trace-a",
|
|
30
|
+
workflow_id: null,
|
|
31
|
+
actor: "tester",
|
|
32
|
+
intent: "demo",
|
|
33
|
+
tags_json: JSON.stringify(["alpha"]),
|
|
34
|
+
metadata_json: "{}",
|
|
35
|
+
started_at: Date.now(),
|
|
36
|
+
finished_at: null,
|
|
37
|
+
status: null,
|
|
38
|
+
outcome_json: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const traceB: TraceRow = {
|
|
42
|
+
trace_id: "trace-b",
|
|
43
|
+
workflow_id: null,
|
|
44
|
+
actor: "tester",
|
|
45
|
+
intent: "demo",
|
|
46
|
+
tags_json: JSON.stringify(["alpha"]),
|
|
47
|
+
metadata_json: "{}",
|
|
48
|
+
started_at: Date.now(),
|
|
49
|
+
finished_at: Date.now(),
|
|
50
|
+
status: "fail",
|
|
51
|
+
outcome_json: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const decisionA: NodeRow = {
|
|
55
|
+
node_id: "decision-a",
|
|
56
|
+
trace_id: traceA.trace_id,
|
|
57
|
+
type: "Decision",
|
|
58
|
+
summary: "Roll back release",
|
|
59
|
+
data_json: "{}",
|
|
60
|
+
confidence: null,
|
|
61
|
+
metadata_json: "{}",
|
|
62
|
+
created_at: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const decisionB: NodeRow = {
|
|
66
|
+
node_id: "decision-b",
|
|
67
|
+
trace_id: traceB.trace_id,
|
|
68
|
+
type: "Decision",
|
|
69
|
+
summary: "Roll back release",
|
|
70
|
+
data_json: "{}",
|
|
71
|
+
confidence: null,
|
|
72
|
+
metadata_json: "{}",
|
|
73
|
+
created_at: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
engine.transaction(() => {
|
|
77
|
+
insertTrace(engine, traceA);
|
|
78
|
+
insertTrace(engine, traceB);
|
|
79
|
+
insertNode(engine, decisionA);
|
|
80
|
+
insertNode(engine, decisionB);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const similar = findSimilarDecisions(engine, decisionA.node_id, {
|
|
84
|
+
scope: "all",
|
|
85
|
+
limit: 5,
|
|
86
|
+
depth: 2,
|
|
87
|
+
maxTraces: 50,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(similar.length).toBeGreaterThan(0);
|
|
91
|
+
|
|
92
|
+
const assessment = assessDecisionRisk(engine, decisionA.node_id, {
|
|
93
|
+
scope: "all",
|
|
94
|
+
limit: 5,
|
|
95
|
+
depth: 2,
|
|
96
|
+
threshold: 0.2,
|
|
97
|
+
maxTraces: 50,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(assessment.similar_failures.length).toBeGreaterThan(0);
|
|
101
|
+
expect(assessment.signature_failed_match).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { SqlJsEngine } from "../src/storage/sqljsEngine.js";
|
|
7
|
+
import {
|
|
8
|
+
getTrace,
|
|
9
|
+
insertArtifact,
|
|
10
|
+
insertEdge,
|
|
11
|
+
insertNode,
|
|
12
|
+
insertTrace,
|
|
13
|
+
type ArtifactRow,
|
|
14
|
+
type EdgeRow,
|
|
15
|
+
type NodeRow,
|
|
16
|
+
type TraceRow,
|
|
17
|
+
} from "../src/storage/queries.js";
|
|
18
|
+
|
|
19
|
+
function makeTempDbPath(): string {
|
|
20
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ctx-test-"));
|
|
21
|
+
return path.join(dir, "trace.db");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("SqlJsEngine", () => {
|
|
25
|
+
it("persists data across flushes", async () => {
|
|
26
|
+
const dbPath = makeTempDbPath();
|
|
27
|
+
const engine = new SqlJsEngine({ dbPath, onLog: () => undefined });
|
|
28
|
+
await engine.init();
|
|
29
|
+
|
|
30
|
+
const trace: TraceRow = {
|
|
31
|
+
trace_id: "trace-1",
|
|
32
|
+
workflow_id: null,
|
|
33
|
+
actor: "tester",
|
|
34
|
+
intent: "test",
|
|
35
|
+
tags_json: "[]",
|
|
36
|
+
metadata_json: "{}",
|
|
37
|
+
started_at: Date.now(),
|
|
38
|
+
finished_at: null,
|
|
39
|
+
status: null,
|
|
40
|
+
outcome_json: null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const node: NodeRow = {
|
|
44
|
+
node_id: "node-1",
|
|
45
|
+
trace_id: trace.trace_id,
|
|
46
|
+
type: "Decision",
|
|
47
|
+
summary: "Decide",
|
|
48
|
+
data_json: "{}",
|
|
49
|
+
confidence: null,
|
|
50
|
+
metadata_json: "{}",
|
|
51
|
+
created_at: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const edge: EdgeRow = {
|
|
55
|
+
edge_id: "edge-1",
|
|
56
|
+
trace_id: trace.trace_id,
|
|
57
|
+
from_node_id: "node-1",
|
|
58
|
+
to_node_id: "node-1",
|
|
59
|
+
relation_type: "derived_from",
|
|
60
|
+
data_json: "{}",
|
|
61
|
+
created_at: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const artifact: ArtifactRow = {
|
|
65
|
+
artifact_id: "artifact-1",
|
|
66
|
+
trace_id: trace.trace_id,
|
|
67
|
+
node_id: "node-1",
|
|
68
|
+
artifact_type: "note",
|
|
69
|
+
content: "payload",
|
|
70
|
+
redaction_level: "internal",
|
|
71
|
+
metadata_json: "{}",
|
|
72
|
+
sha256: "hash",
|
|
73
|
+
created_at: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
engine.transaction(() => {
|
|
77
|
+
insertTrace(engine, trace);
|
|
78
|
+
insertNode(engine, node);
|
|
79
|
+
insertEdge(engine, edge);
|
|
80
|
+
insertArtifact(engine, artifact);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await engine.flushNow();
|
|
84
|
+
|
|
85
|
+
const reloaded = new SqlJsEngine({ dbPath, onLog: () => undefined });
|
|
86
|
+
await reloaded.init();
|
|
87
|
+
|
|
88
|
+
const stored = getTrace(reloaded, trace.trace_id);
|
|
89
|
+
expect(stored?.trace_id).toBe(trace.trace_id);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src", "tests"],
|
|
15
|
+
"exclude": ["dist", "node_modules"]
|
|
16
|
+
}
|