@ai2070/memex 0.9.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.
Files changed (46) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/.github/workflows/release.yml +35 -0
  3. package/API.md +1078 -0
  4. package/LICENSE +190 -0
  5. package/README.md +574 -0
  6. package/package.json +30 -0
  7. package/src/bulk.ts +128 -0
  8. package/src/envelope.ts +52 -0
  9. package/src/errors.ts +27 -0
  10. package/src/graph.ts +15 -0
  11. package/src/helpers.ts +51 -0
  12. package/src/index.ts +142 -0
  13. package/src/integrity.ts +378 -0
  14. package/src/intent.ts +311 -0
  15. package/src/query.ts +357 -0
  16. package/src/reducer.ts +177 -0
  17. package/src/replay.ts +32 -0
  18. package/src/retrieval.ts +306 -0
  19. package/src/serialization.ts +34 -0
  20. package/src/stats.ts +62 -0
  21. package/src/task.ts +373 -0
  22. package/src/transplant.ts +488 -0
  23. package/src/types.ts +248 -0
  24. package/tests/bugfix-and-coverage.test.ts +958 -0
  25. package/tests/bugfix-holes.test.ts +856 -0
  26. package/tests/bulk.test.ts +256 -0
  27. package/tests/edge-cases-v2.test.ts +355 -0
  28. package/tests/edge-cases.test.ts +661 -0
  29. package/tests/envelope.test.ts +92 -0
  30. package/tests/graph.test.ts +41 -0
  31. package/tests/helpers.test.ts +120 -0
  32. package/tests/integrity.test.ts +371 -0
  33. package/tests/intent.test.ts +276 -0
  34. package/tests/query-advanced.test.ts +252 -0
  35. package/tests/query.test.ts +623 -0
  36. package/tests/reducer.test.ts +342 -0
  37. package/tests/replay.test.ts +145 -0
  38. package/tests/retrieval.test.ts +691 -0
  39. package/tests/serialization.test.ts +118 -0
  40. package/tests/setup.test.ts +7 -0
  41. package/tests/stats.test.ts +163 -0
  42. package/tests/task.test.ts +322 -0
  43. package/tests/transplant.test.ts +385 -0
  44. package/tests/types.test.ts +231 -0
  45. package/tsconfig.json +18 -0
  46. package/vitest.config.ts +7 -0
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { toJSON, fromJSON, stringify, parse } from "../src/serialization.js";
3
+ import { createGraphState } from "../src/graph.js";
4
+ import { applyCommand } from "../src/reducer.js";
5
+ import type { MemoryItem, Edge } from "../src/types.js";
6
+
7
+ const makeItem = (id: string): MemoryItem => ({
8
+ id,
9
+ scope: "test",
10
+ kind: "observation",
11
+ content: { key: "value" },
12
+ author: "user:laz",
13
+ source_kind: "observed",
14
+ authority: 0.9,
15
+ conviction: 0.8,
16
+ importance: 0.7,
17
+ meta: { agent_id: "agent:x" },
18
+ });
19
+
20
+ const makeEdge = (id: string): Edge => ({
21
+ edge_id: id,
22
+ from: "m1",
23
+ to: "m2",
24
+ kind: "SUPPORTS",
25
+ author: "system:rule",
26
+ source_kind: "derived_deterministic",
27
+ authority: 0.8,
28
+ active: true,
29
+ weight: 0.5,
30
+ });
31
+
32
+ function buildState() {
33
+ let state = createGraphState();
34
+ state = applyCommand(state, {
35
+ type: "memory.create",
36
+ item: makeItem("m1"),
37
+ }).state;
38
+ state = applyCommand(state, {
39
+ type: "memory.create",
40
+ item: makeItem("m2"),
41
+ }).state;
42
+ state = applyCommand(state, {
43
+ type: "edge.create",
44
+ edge: makeEdge("e1"),
45
+ }).state;
46
+ return state;
47
+ }
48
+
49
+ describe("toJSON / fromJSON", () => {
50
+ it("round-trips a graph state", () => {
51
+ const state = buildState();
52
+ const json = toJSON(state);
53
+ const restored = fromJSON(json);
54
+
55
+ expect(restored.items.size).toBe(2);
56
+ expect(restored.edges.size).toBe(1);
57
+ expect(restored.items.get("m1")!.content).toEqual({ key: "value" });
58
+ expect(restored.items.get("m1")!.meta?.agent_id).toBe("agent:x");
59
+ expect(restored.edges.get("e1")!.kind).toBe("SUPPORTS");
60
+ });
61
+
62
+ it("serialized format has items and edges as arrays", () => {
63
+ const state = buildState();
64
+ const json = toJSON(state);
65
+ expect(Array.isArray(json.items)).toBe(true);
66
+ expect(Array.isArray(json.edges)).toBe(true);
67
+ expect(json.items).toHaveLength(2);
68
+ expect(json.edges).toHaveLength(1);
69
+ expect(json.items[0][0]).toBe("m1"); // [key, value] tuple
70
+ });
71
+
72
+ it("empty state round-trips", () => {
73
+ const state = createGraphState();
74
+ const restored = fromJSON(toJSON(state));
75
+ expect(restored.items.size).toBe(0);
76
+ expect(restored.edges.size).toBe(0);
77
+ });
78
+ });
79
+
80
+ describe("stringify / parse", () => {
81
+ it("round-trips through JSON string", () => {
82
+ const state = buildState();
83
+ const jsonStr = stringify(state);
84
+ const restored = parse(jsonStr);
85
+
86
+ expect(restored.items.size).toBe(2);
87
+ expect(restored.edges.size).toBe(1);
88
+ expect(restored.items.get("m2")!.authority).toBe(0.9);
89
+ });
90
+
91
+ it("produces valid JSON", () => {
92
+ const state = buildState();
93
+ const jsonStr = stringify(state);
94
+ expect(() => JSON.parse(jsonStr)).not.toThrow();
95
+ });
96
+
97
+ it("pretty mode produces formatted output", () => {
98
+ const state = buildState();
99
+ const compact = stringify(state);
100
+ const pretty = stringify(state, true);
101
+ expect(pretty.length).toBeGreaterThan(compact.length);
102
+ expect(pretty).toContain("\n");
103
+ });
104
+
105
+ it("preserves all fields through stringify/parse", () => {
106
+ const state = buildState();
107
+ const restored = parse(stringify(state));
108
+ const item = restored.items.get("m1")!;
109
+ expect(item.scope).toBe("test");
110
+ expect(item.kind).toBe("observation");
111
+ expect(item.conviction).toBe(0.8);
112
+ expect(item.importance).toBe(0.7);
113
+ expect(item.meta?.agent_id).toBe("agent:x");
114
+ const edge = restored.edges.get("e1")!;
115
+ expect(edge.weight).toBe(0.5);
116
+ expect(edge.active).toBe(true);
117
+ });
118
+ });
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("scaffold", () => {
4
+ it("toolchain works", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getStats } from "../src/stats.js";
3
+ import { createGraphState } from "../src/graph.js";
4
+ import type { MemoryItem, Edge, GraphState } from "../src/types.js";
5
+
6
+ const makeItem = (
7
+ id: string,
8
+ overrides: Partial<MemoryItem> = {},
9
+ ): MemoryItem => ({
10
+ id,
11
+ scope: "test",
12
+ kind: "observation",
13
+ content: {},
14
+ author: "agent:a",
15
+ source_kind: "observed",
16
+ authority: 0.5,
17
+ ...overrides,
18
+ });
19
+
20
+ function buildState(): GraphState {
21
+ const state: GraphState = { items: new Map(), edges: new Map() };
22
+ const items: MemoryItem[] = [
23
+ makeItem("m1", {
24
+ kind: "observation",
25
+ source_kind: "observed",
26
+ author: "agent:a",
27
+ scope: "project:x",
28
+ }),
29
+ makeItem("m2", {
30
+ kind: "observation",
31
+ source_kind: "observed",
32
+ author: "agent:a",
33
+ scope: "project:x",
34
+ }),
35
+ makeItem("m3", {
36
+ kind: "assertion",
37
+ source_kind: "user_explicit",
38
+ author: "user:laz",
39
+ scope: "project:x",
40
+ }),
41
+ makeItem("m4", {
42
+ kind: "hypothesis",
43
+ source_kind: "agent_inferred",
44
+ author: "agent:b",
45
+ scope: "project:y",
46
+ parents: ["m1"],
47
+ }),
48
+ makeItem("m5", {
49
+ kind: "derivation",
50
+ source_kind: "derived_deterministic",
51
+ author: "system:rule",
52
+ scope: "project:y",
53
+ parents: ["m2", "m3"],
54
+ }),
55
+ ];
56
+ const edges: Edge[] = [
57
+ {
58
+ edge_id: "e1",
59
+ from: "m1",
60
+ to: "m2",
61
+ kind: "SUPPORTS",
62
+ author: "system:rule",
63
+ source_kind: "derived_deterministic",
64
+ authority: 0.8,
65
+ active: true,
66
+ },
67
+ {
68
+ edge_id: "e2",
69
+ from: "m3",
70
+ to: "m4",
71
+ kind: "CONTRADICTS",
72
+ author: "system:detector",
73
+ source_kind: "derived_deterministic",
74
+ authority: 1,
75
+ active: true,
76
+ },
77
+ {
78
+ edge_id: "e3",
79
+ from: "m1",
80
+ to: "m3",
81
+ kind: "ABOUT",
82
+ author: "agent:a",
83
+ source_kind: "agent_inferred",
84
+ authority: 0.5,
85
+ active: false,
86
+ },
87
+ ];
88
+ for (const i of items) state.items.set(i.id, i);
89
+ for (const e of edges) state.edges.set(e.edge_id, e);
90
+ return state;
91
+ }
92
+
93
+ describe("getStats", () => {
94
+ it("returns correct item totals", () => {
95
+ const stats = getStats(buildState());
96
+ expect(stats.items.total).toBe(5);
97
+ expect(stats.items.root).toBe(3);
98
+ expect(stats.items.with_parents).toBe(2);
99
+ });
100
+
101
+ it("counts items by kind", () => {
102
+ const stats = getStats(buildState());
103
+ expect(stats.items.by_kind).toEqual({
104
+ observation: 2,
105
+ assertion: 1,
106
+ hypothesis: 1,
107
+ derivation: 1,
108
+ });
109
+ });
110
+
111
+ it("counts items by source_kind", () => {
112
+ const stats = getStats(buildState());
113
+ expect(stats.items.by_source_kind).toEqual({
114
+ observed: 2,
115
+ user_explicit: 1,
116
+ agent_inferred: 1,
117
+ derived_deterministic: 1,
118
+ });
119
+ });
120
+
121
+ it("counts items by author", () => {
122
+ const stats = getStats(buildState());
123
+ expect(stats.items.by_author).toEqual({
124
+ "agent:a": 2,
125
+ "user:laz": 1,
126
+ "agent:b": 1,
127
+ "system:rule": 1,
128
+ });
129
+ });
130
+
131
+ it("counts items by scope", () => {
132
+ const stats = getStats(buildState());
133
+ expect(stats.items.by_scope).toEqual({
134
+ "project:x": 3,
135
+ "project:y": 2,
136
+ });
137
+ });
138
+
139
+ it("returns correct edge totals", () => {
140
+ const stats = getStats(buildState());
141
+ expect(stats.edges.total).toBe(3);
142
+ expect(stats.edges.active).toBe(2);
143
+ });
144
+
145
+ it("counts edges by kind", () => {
146
+ const stats = getStats(buildState());
147
+ expect(stats.edges.by_kind).toEqual({
148
+ SUPPORTS: 1,
149
+ CONTRADICTS: 1,
150
+ ABOUT: 1,
151
+ });
152
+ });
153
+
154
+ it("handles empty state", () => {
155
+ const stats = getStats(createGraphState());
156
+ expect(stats.items.total).toBe(0);
157
+ expect(stats.items.root).toBe(0);
158
+ expect(stats.items.with_parents).toBe(0);
159
+ expect(stats.items.by_kind).toEqual({});
160
+ expect(stats.edges.total).toBe(0);
161
+ expect(stats.edges.active).toBe(0);
162
+ });
163
+ });
@@ -0,0 +1,322 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ createTaskState,
4
+ createTask,
5
+ applyTaskCommand,
6
+ getTasks,
7
+ getTaskById,
8
+ getTasksByIntent,
9
+ TaskNotFoundError,
10
+ DuplicateTaskError,
11
+ InvalidTaskTransitionError,
12
+ } from "../src/task.js";
13
+ import type { Task, TaskState } from "../src/task.js";
14
+
15
+ const makeTask = (overrides: Partial<Task> = {}): Task => ({
16
+ id: "t1",
17
+ intent_id: "i1",
18
+ action: "search_linkedin",
19
+ status: "pending",
20
+ priority: 0.7,
21
+ attempt: 0,
22
+ ...overrides,
23
+ });
24
+
25
+ describe("task.create", () => {
26
+ it("creates a task", () => {
27
+ const task = makeTask();
28
+ const { state, events } = applyTaskCommand(createTaskState(), {
29
+ type: "task.create",
30
+ task,
31
+ });
32
+ expect(state.tasks.get("t1")).toEqual(task);
33
+ expect(events).toHaveLength(1);
34
+ expect(events[0].type).toBe("task.created");
35
+ expect(events[0].namespace).toBe("task");
36
+ });
37
+
38
+ it("throws DuplicateTaskError", () => {
39
+ let state = createTaskState();
40
+ state = applyTaskCommand(state, {
41
+ type: "task.create",
42
+ task: makeTask(),
43
+ }).state;
44
+ expect(() =>
45
+ applyTaskCommand(state, { type: "task.create", task: makeTask() }),
46
+ ).toThrow(DuplicateTaskError);
47
+ });
48
+
49
+ it("does not mutate original state", () => {
50
+ const state = createTaskState();
51
+ applyTaskCommand(state, { type: "task.create", task: makeTask() });
52
+ expect(state.tasks.size).toBe(0);
53
+ });
54
+ });
55
+
56
+ describe("task.update", () => {
57
+ it("updates priority", () => {
58
+ let state = createTaskState();
59
+ state = applyTaskCommand(state, {
60
+ type: "task.create",
61
+ task: makeTask(),
62
+ }).state;
63
+ const { state: next } = applyTaskCommand(state, {
64
+ type: "task.update",
65
+ task_id: "t1",
66
+ partial: { priority: 0.2 },
67
+ author: "test",
68
+ });
69
+ expect(next.tasks.get("t1")!.priority).toBe(0.2);
70
+ });
71
+
72
+ it("throws TaskNotFoundError", () => {
73
+ expect(() =>
74
+ applyTaskCommand(createTaskState(), {
75
+ type: "task.update",
76
+ task_id: "nope",
77
+ partial: {},
78
+ author: "test",
79
+ }),
80
+ ).toThrow(TaskNotFoundError);
81
+ });
82
+ });
83
+
84
+ describe("task lifecycle", () => {
85
+ let state: TaskState;
86
+ beforeEach(() => {
87
+ state = applyTaskCommand(createTaskState(), {
88
+ type: "task.create",
89
+ task: makeTask(),
90
+ }).state;
91
+ });
92
+
93
+ it("pending -> running (start)", () => {
94
+ const { state: next, events } = applyTaskCommand(state, {
95
+ type: "task.start",
96
+ task_id: "t1",
97
+ agent_id: "agent:worker",
98
+ });
99
+ expect(next.tasks.get("t1")!.status).toBe("running");
100
+ expect(next.tasks.get("t1")!.agent_id).toBe("agent:worker");
101
+ expect(next.tasks.get("t1")!.attempt).toBe(1);
102
+ expect(events[0].type).toBe("task.started");
103
+ });
104
+
105
+ it("running -> completed", () => {
106
+ state = applyTaskCommand(state, {
107
+ type: "task.start",
108
+ task_id: "t1",
109
+ }).state;
110
+ const { state: next, events } = applyTaskCommand(state, {
111
+ type: "task.complete",
112
+ task_id: "t1",
113
+ result: { found: true },
114
+ output_memory_ids: ["m5"],
115
+ });
116
+ expect(next.tasks.get("t1")!.status).toBe("completed");
117
+ expect(next.tasks.get("t1")!.result).toEqual({ found: true });
118
+ expect(next.tasks.get("t1")!.output_memory_ids).toEqual(["m5"]);
119
+ expect(events[0].type).toBe("task.completed");
120
+ });
121
+
122
+ it("running -> failed", () => {
123
+ state = applyTaskCommand(state, {
124
+ type: "task.start",
125
+ task_id: "t1",
126
+ }).state;
127
+ const { state: next, events } = applyTaskCommand(state, {
128
+ type: "task.fail",
129
+ task_id: "t1",
130
+ error: "timeout",
131
+ });
132
+ expect(next.tasks.get("t1")!.status).toBe("failed");
133
+ expect(next.tasks.get("t1")!.error).toBe("timeout");
134
+ expect(events[0].type).toBe("task.failed");
135
+ });
136
+
137
+ it("failed -> running (retry)", () => {
138
+ state = applyTaskCommand(state, {
139
+ type: "task.start",
140
+ task_id: "t1",
141
+ }).state;
142
+ state = applyTaskCommand(state, {
143
+ type: "task.fail",
144
+ task_id: "t1",
145
+ error: "timeout",
146
+ }).state;
147
+ const { state: next } = applyTaskCommand(state, {
148
+ type: "task.start",
149
+ task_id: "t1",
150
+ });
151
+ expect(next.tasks.get("t1")!.status).toBe("running");
152
+ expect(next.tasks.get("t1")!.attempt).toBe(2);
153
+ });
154
+
155
+ it("pending -> cancelled", () => {
156
+ const { state: next, events } = applyTaskCommand(state, {
157
+ type: "task.cancel",
158
+ task_id: "t1",
159
+ reason: "no longer needed",
160
+ });
161
+ expect(next.tasks.get("t1")!.status).toBe("cancelled");
162
+ expect(events[0].type).toBe("task.cancelled");
163
+ });
164
+
165
+ it("running -> cancelled", () => {
166
+ state = applyTaskCommand(state, {
167
+ type: "task.start",
168
+ task_id: "t1",
169
+ }).state;
170
+ const { state: next } = applyTaskCommand(state, {
171
+ type: "task.cancel",
172
+ task_id: "t1",
173
+ });
174
+ expect(next.tasks.get("t1")!.status).toBe("cancelled");
175
+ });
176
+
177
+ it("completed -> start throws InvalidTaskTransitionError", () => {
178
+ state = applyTaskCommand(state, {
179
+ type: "task.start",
180
+ task_id: "t1",
181
+ }).state;
182
+ state = applyTaskCommand(state, {
183
+ type: "task.complete",
184
+ task_id: "t1",
185
+ }).state;
186
+ expect(() =>
187
+ applyTaskCommand(state, { type: "task.start", task_id: "t1" }),
188
+ ).toThrow(InvalidTaskTransitionError);
189
+ });
190
+
191
+ it("completed -> cancel throws InvalidTaskTransitionError", () => {
192
+ state = applyTaskCommand(state, {
193
+ type: "task.start",
194
+ task_id: "t1",
195
+ }).state;
196
+ state = applyTaskCommand(state, {
197
+ type: "task.complete",
198
+ task_id: "t1",
199
+ }).state;
200
+ expect(() =>
201
+ applyTaskCommand(state, { type: "task.cancel", task_id: "t1" }),
202
+ ).toThrow(InvalidTaskTransitionError);
203
+ });
204
+
205
+ it("pending -> complete throws InvalidTaskTransitionError", () => {
206
+ expect(() =>
207
+ applyTaskCommand(state, { type: "task.complete", task_id: "t1" }),
208
+ ).toThrow(InvalidTaskTransitionError);
209
+ });
210
+ });
211
+
212
+ describe("createTask factory", () => {
213
+ it("generates id and defaults status/attempt", () => {
214
+ const task = createTask({
215
+ intent_id: "i1",
216
+ action: "search",
217
+ priority: 0.5,
218
+ });
219
+ expect(task.id).toBeDefined();
220
+ expect(task.status).toBe("pending");
221
+ expect(task.attempt).toBe(0);
222
+ });
223
+ });
224
+
225
+ describe("task queries", () => {
226
+ let state: TaskState;
227
+ beforeEach(() => {
228
+ state = createTaskState();
229
+ state = applyTaskCommand(state, {
230
+ type: "task.create",
231
+ task: makeTask({
232
+ id: "t1",
233
+ intent_id: "i1",
234
+ action: "search",
235
+ status: "pending",
236
+ priority: 0.9,
237
+ agent_id: "agent:a",
238
+ input_memory_ids: ["m1"],
239
+ }),
240
+ }).state;
241
+ state = applyTaskCommand(state, {
242
+ type: "task.create",
243
+ task: makeTask({
244
+ id: "t2",
245
+ intent_id: "i1",
246
+ action: "summarize",
247
+ status: "running",
248
+ priority: 0.5,
249
+ agent_id: "agent:b",
250
+ }),
251
+ }).state;
252
+ state = applyTaskCommand(state, {
253
+ type: "task.create",
254
+ task: makeTask({
255
+ id: "t3",
256
+ intent_id: "i2",
257
+ action: "search",
258
+ status: "completed",
259
+ priority: 0.3,
260
+ output_memory_ids: ["m5"],
261
+ }),
262
+ }).state;
263
+ });
264
+
265
+ it("returns all tasks with no filter", () => {
266
+ expect(getTasks(state)).toHaveLength(3);
267
+ });
268
+
269
+ it("filters by intent_id", () => {
270
+ const result = getTasks(state, { intent_id: "i1" });
271
+ expect(result).toHaveLength(2);
272
+ });
273
+
274
+ it("filters by action", () => {
275
+ const result = getTasks(state, { action: "search" });
276
+ expect(result).toHaveLength(2);
277
+ });
278
+
279
+ it("filters by status", () => {
280
+ const result = getTasks(state, { status: "running" });
281
+ expect(result).toHaveLength(1);
282
+ expect(result[0].id).toBe("t2");
283
+ });
284
+
285
+ it("filters by statuses array", () => {
286
+ const result = getTasks(state, { statuses: ["pending", "running"] });
287
+ expect(result).toHaveLength(2);
288
+ });
289
+
290
+ it("filters by agent_id", () => {
291
+ const result = getTasks(state, { agent_id: "agent:a" });
292
+ expect(result).toHaveLength(1);
293
+ expect(result[0].id).toBe("t1");
294
+ });
295
+
296
+ it("filters by min_priority", () => {
297
+ const result = getTasks(state, { min_priority: 0.5 });
298
+ expect(result).toHaveLength(2);
299
+ });
300
+
301
+ it("filters by has_input_memory_id", () => {
302
+ const result = getTasks(state, { has_input_memory_id: "m1" });
303
+ expect(result).toHaveLength(1);
304
+ expect(result[0].id).toBe("t1");
305
+ });
306
+
307
+ it("filters by has_output_memory_id", () => {
308
+ const result = getTasks(state, { has_output_memory_id: "m5" });
309
+ expect(result).toHaveLength(1);
310
+ expect(result[0].id).toBe("t3");
311
+ });
312
+
313
+ it("getTaskById works", () => {
314
+ expect(getTaskById(state, "t2")?.action).toBe("summarize");
315
+ expect(getTaskById(state, "nope")).toBeUndefined();
316
+ });
317
+
318
+ it("getTasksByIntent works", () => {
319
+ const result = getTasksByIntent(state, "i1");
320
+ expect(result).toHaveLength(2);
321
+ });
322
+ });