@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,488 @@
1
+ import { uuidv7 } from "uuidv7";
2
+ import type {
3
+ GraphState,
4
+ MemoryItem,
5
+ Edge,
6
+ MemoryLifecycleEvent,
7
+ } from "./types.js";
8
+ import type { IntentState, Intent, IntentLifecycleEvent } from "./intent.js";
9
+ import type { TaskState, Task, TaskLifecycleEvent } from "./task.js";
10
+ import { getChildren, getEdges, extractTimestamp } from "./query.js";
11
+ import { applyCommand } from "./reducer.js";
12
+ import { applyIntentCommand } from "./intent.js";
13
+ import { applyTaskCommand } from "./task.js";
14
+
15
+ /**
16
+ * Build a uuidv7-shaped id from a given ms timestamp + random suffix.
17
+ */
18
+ function uuidFromMs(ms: number): string {
19
+ const hex = ms.toString(16).padStart(12, "0");
20
+ const rand = uuidv7().replace(/-/g, "");
21
+ return [
22
+ hex.slice(0, 8),
23
+ hex.slice(8, 12),
24
+ "7" + rand.slice(13, 16),
25
+ rand.slice(16, 20),
26
+ rand.slice(20, 32),
27
+ ].join("-");
28
+ }
29
+
30
+ /**
31
+ * Generate a new id 1ms after the original, incrementing until no collision.
32
+ */
33
+ function reIdFor(originalId: string, existingIds: Set<string>): string {
34
+ let ms = extractTimestamp(originalId) + 1;
35
+ let newId = uuidFromMs(ms);
36
+ while (existingIds.has(newId)) {
37
+ ms++;
38
+ newId = uuidFromMs(ms);
39
+ }
40
+ return newId;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Export
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export interface ExportOptions {
48
+ memory_ids?: string[];
49
+ intent_ids?: string[];
50
+ task_ids?: string[];
51
+
52
+ include_parents?: boolean;
53
+ include_children?: boolean;
54
+ include_aliases?: boolean;
55
+ include_related_tasks?: boolean;
56
+ include_related_intents?: boolean;
57
+ }
58
+
59
+ export interface MemexExport {
60
+ memories: MemoryItem[];
61
+ edges: Edge[];
62
+ intents: Intent[];
63
+ tasks: Task[];
64
+ }
65
+
66
+ export function exportSlice(
67
+ memState: GraphState,
68
+ intentState: IntentState,
69
+ taskState: TaskState,
70
+ opts: ExportOptions,
71
+ ): MemexExport {
72
+ const memoryIds = new Set<string>(opts.memory_ids ?? []);
73
+ const intentIds = new Set<string>(opts.intent_ids ?? []);
74
+ const taskIds = new Set<string>(opts.task_ids ?? []);
75
+ const edgeIds = new Set<string>();
76
+
77
+ // walk parents up-graph
78
+ if (opts.include_parents) {
79
+ const queue = [...memoryIds];
80
+ while (queue.length > 0) {
81
+ const id = queue.pop()!;
82
+ const item = memState.items.get(id);
83
+ if (item?.parents) {
84
+ for (const pid of item.parents) {
85
+ if (!memoryIds.has(pid)) {
86
+ memoryIds.add(pid);
87
+ queue.push(pid);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // walk children down-graph
95
+ if (opts.include_children) {
96
+ const queue = [...memoryIds];
97
+ while (queue.length > 0) {
98
+ const id = queue.pop()!;
99
+ const children = getChildren(memState, id);
100
+ for (const child of children) {
101
+ if (!memoryIds.has(child.id)) {
102
+ memoryIds.add(child.id);
103
+ queue.push(child.id);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ // walk aliases
110
+ if (opts.include_aliases) {
111
+ const queue = [...memoryIds];
112
+ const visited = new Set<string>();
113
+ while (queue.length > 0) {
114
+ const id = queue.pop()!;
115
+ if (visited.has(id)) continue;
116
+ visited.add(id);
117
+ const aliasEdges = getEdges(memState, {
118
+ from: id,
119
+ kind: "ALIAS",
120
+ active_only: true,
121
+ });
122
+ for (const edge of aliasEdges) {
123
+ edgeIds.add(edge.edge_id);
124
+ if (!memoryIds.has(edge.to)) {
125
+ memoryIds.add(edge.to);
126
+ queue.push(edge.to);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ // collect edges between included memories
133
+ for (const edge of memState.edges.values()) {
134
+ if (memoryIds.has(edge.from) && memoryIds.has(edge.to)) {
135
+ edgeIds.add(edge.edge_id);
136
+ }
137
+ }
138
+
139
+ // walk related intents
140
+ if (opts.include_related_intents) {
141
+ for (const intent of intentState.intents.values()) {
142
+ if (intent.root_memory_ids) {
143
+ for (const mid of intent.root_memory_ids) {
144
+ if (memoryIds.has(mid)) {
145
+ intentIds.add(intent.id);
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ // also check memory meta for creation_intent_id
152
+ for (const mid of memoryIds) {
153
+ const item = memState.items.get(mid);
154
+ if (item?.meta?.creation_intent_id) {
155
+ intentIds.add(item.meta.creation_intent_id as string);
156
+ }
157
+ }
158
+ }
159
+
160
+ // walk related tasks
161
+ if (opts.include_related_tasks) {
162
+ for (const task of taskState.tasks.values()) {
163
+ if (intentIds.has(task.intent_id)) {
164
+ taskIds.add(task.id);
165
+ continue;
166
+ }
167
+ const inputMatch = task.input_memory_ids?.some((id) => memoryIds.has(id));
168
+ const outputMatch = task.output_memory_ids?.some((id) =>
169
+ memoryIds.has(id),
170
+ );
171
+ if (inputMatch || outputMatch) {
172
+ taskIds.add(task.id);
173
+ }
174
+ }
175
+ // also check memory meta for creation_task_id
176
+ for (const mid of memoryIds) {
177
+ const item = memState.items.get(mid);
178
+ if (item?.meta?.creation_task_id) {
179
+ taskIds.add(item.meta.creation_task_id as string);
180
+ }
181
+ }
182
+ }
183
+
184
+ // collect entities
185
+ const memories: MemoryItem[] = [];
186
+ for (const id of memoryIds) {
187
+ const item = memState.items.get(id);
188
+ if (item) memories.push(item);
189
+ }
190
+
191
+ const edges: Edge[] = [];
192
+ for (const id of edgeIds) {
193
+ const edge = memState.edges.get(id);
194
+ if (edge) edges.push(edge);
195
+ }
196
+
197
+ const intents: Intent[] = [];
198
+ for (const id of intentIds) {
199
+ const intent = intentState.intents.get(id);
200
+ if (intent) intents.push(intent);
201
+ }
202
+
203
+ const tasks: Task[] = [];
204
+ for (const id of taskIds) {
205
+ const task = taskState.tasks.get(id);
206
+ if (task) tasks.push(task);
207
+ }
208
+
209
+ return { memories, edges, intents, tasks };
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Import
214
+ // ---------------------------------------------------------------------------
215
+
216
+ export interface ImportOptions {
217
+ skipExistingIds?: boolean; // default true
218
+ shallowCompareExisting?: boolean; // default false
219
+ reIdOnDifference?: boolean; // default false
220
+ }
221
+
222
+ export interface ImportReport {
223
+ created: {
224
+ memories: string[];
225
+ intents: string[];
226
+ tasks: string[];
227
+ edges: string[];
228
+ };
229
+ skipped: {
230
+ memories: string[];
231
+ intents: string[];
232
+ tasks: string[];
233
+ edges: string[];
234
+ };
235
+ conflicts: {
236
+ memories: string[];
237
+ intents: string[];
238
+ tasks: string[];
239
+ edges: string[];
240
+ };
241
+ }
242
+
243
+ function shallowEqual(
244
+ a: Record<string, unknown>,
245
+ b: Record<string, unknown>,
246
+ ): boolean {
247
+ const keysA = Object.keys(a);
248
+ const keysB = Object.keys(b);
249
+ if (keysA.length !== keysB.length) return false;
250
+ for (const key of keysA) {
251
+ if (a[key] !== b[key]) return false;
252
+ }
253
+ return true;
254
+ }
255
+
256
+ function rewriteId(id: string, idMap: Map<string, string>): string {
257
+ return idMap.get(id) ?? id;
258
+ }
259
+
260
+ function rewriteIds(
261
+ ids: string[] | undefined,
262
+ idMap: Map<string, string>,
263
+ ): string[] | undefined {
264
+ if (!ids) return ids;
265
+ return ids.map((id) => rewriteId(id, idMap));
266
+ }
267
+
268
+ export function importSlice(
269
+ memState: GraphState,
270
+ intentState: IntentState,
271
+ taskState: TaskState,
272
+ slice: MemexExport,
273
+ opts?: ImportOptions,
274
+ ): {
275
+ memState: GraphState;
276
+ intentState: IntentState;
277
+ taskState: TaskState;
278
+ report: ImportReport;
279
+ } {
280
+ const skipExisting = opts?.skipExistingIds ?? true;
281
+ const shallowCompare = opts?.shallowCompareExisting ?? false;
282
+ const doReId = opts?.reIdOnDifference ?? false;
283
+
284
+ const report: ImportReport = {
285
+ created: { memories: [], intents: [], tasks: [], edges: [] },
286
+ skipped: { memories: [], intents: [], tasks: [], edges: [] },
287
+ conflicts: { memories: [], intents: [], tasks: [], edges: [] },
288
+ };
289
+
290
+ // id remapping (only used when reIdOnDifference = true)
291
+ const memIdMap = new Map<string, string>();
292
+ const intentIdMap = new Map<string, string>();
293
+ const taskIdMap = new Map<string, string>();
294
+
295
+ // track all known ids for collision-free re-id generation
296
+ const allMemIds = new Set(memState.items.keys());
297
+ const allIntentIds = new Set(intentState.intents.keys());
298
+ const allTaskIds = new Set(taskState.tasks.keys());
299
+
300
+ let currentMem = memState;
301
+ let currentIntent = intentState;
302
+ let currentTask = taskState;
303
+
304
+ // --- import memories ---
305
+ for (const item of slice.memories) {
306
+ const existing = currentMem.items.get(item.id);
307
+ if (existing) {
308
+ if (skipExisting) {
309
+ if (shallowCompare && !shallowEqual(existing as any, item as any)) {
310
+ if (doReId) {
311
+ const newId = reIdFor(item.id, allMemIds);
312
+ allMemIds.add(newId);
313
+ memIdMap.set(item.id, newId);
314
+ const remapped: MemoryItem = {
315
+ ...item,
316
+ id: newId,
317
+ parents: rewriteIds(item.parents, memIdMap),
318
+ };
319
+ const result = applyCommand(currentMem, {
320
+ type: "memory.create",
321
+ item: remapped,
322
+ });
323
+ currentMem = result.state;
324
+ report.created.memories.push(newId);
325
+ } else {
326
+ report.conflicts.memories.push(item.id);
327
+ }
328
+ } else {
329
+ report.skipped.memories.push(item.id);
330
+ }
331
+ continue;
332
+ }
333
+ }
334
+ // no collision — create
335
+ const remapped: MemoryItem = {
336
+ ...item,
337
+ parents: rewriteIds(item.parents, memIdMap),
338
+ };
339
+ const result = applyCommand(currentMem, {
340
+ type: "memory.create",
341
+ item: remapped,
342
+ });
343
+ currentMem = result.state;
344
+ report.created.memories.push(item.id);
345
+ }
346
+
347
+ // track all known edge ids for collision-free re-id generation
348
+ const allEdgeIds = new Set(currentMem.edges.keys());
349
+
350
+ // --- import edges ---
351
+ for (const edge of slice.edges) {
352
+ const existing = currentMem.edges.get(edge.edge_id);
353
+ if (existing) {
354
+ if (skipExisting) {
355
+ if (shallowCompare && !shallowEqual(existing as any, edge as any)) {
356
+ if (doReId) {
357
+ const newId = reIdFor(edge.edge_id, allEdgeIds);
358
+ allEdgeIds.add(newId);
359
+ const remapped: Edge = {
360
+ ...edge,
361
+ edge_id: newId,
362
+ from: rewriteId(edge.from, memIdMap),
363
+ to: rewriteId(edge.to, memIdMap),
364
+ };
365
+ const result = applyCommand(currentMem, {
366
+ type: "edge.create",
367
+ edge: remapped,
368
+ });
369
+ currentMem = result.state;
370
+ report.created.edges.push(newId);
371
+ } else {
372
+ report.conflicts.edges.push(edge.edge_id);
373
+ }
374
+ } else {
375
+ report.skipped.edges.push(edge.edge_id);
376
+ }
377
+ continue;
378
+ }
379
+ }
380
+ // no collision — create
381
+ const remapped: Edge = {
382
+ ...edge,
383
+ from: rewriteId(edge.from, memIdMap),
384
+ to: rewriteId(edge.to, memIdMap),
385
+ };
386
+ const result = applyCommand(currentMem, {
387
+ type: "edge.create",
388
+ edge: remapped,
389
+ });
390
+ currentMem = result.state;
391
+ report.created.edges.push(edge.edge_id);
392
+ }
393
+
394
+ // --- import intents ---
395
+ for (const intent of slice.intents) {
396
+ const existing = currentIntent.intents.get(intent.id);
397
+ if (existing) {
398
+ if (skipExisting) {
399
+ if (shallowCompare && !shallowEqual(existing as any, intent as any)) {
400
+ if (doReId) {
401
+ const newId = reIdFor(intent.id, allIntentIds);
402
+ allIntentIds.add(newId);
403
+ intentIdMap.set(intent.id, newId);
404
+ const remapped: Intent = {
405
+ ...intent,
406
+ id: newId,
407
+ root_memory_ids: rewriteIds(intent.root_memory_ids, memIdMap),
408
+ };
409
+ const result = applyIntentCommand(currentIntent, {
410
+ type: "intent.create",
411
+ intent: remapped,
412
+ });
413
+ currentIntent = result.state;
414
+ report.created.intents.push(newId);
415
+ } else {
416
+ report.conflicts.intents.push(intent.id);
417
+ }
418
+ } else {
419
+ report.skipped.intents.push(intent.id);
420
+ }
421
+ continue;
422
+ }
423
+ }
424
+ const remapped: Intent = {
425
+ ...intent,
426
+ root_memory_ids: rewriteIds(intent.root_memory_ids, memIdMap),
427
+ };
428
+ const result = applyIntentCommand(currentIntent, {
429
+ type: "intent.create",
430
+ intent: remapped,
431
+ });
432
+ currentIntent = result.state;
433
+ report.created.intents.push(intent.id);
434
+ }
435
+
436
+ // --- import tasks ---
437
+ for (const task of slice.tasks) {
438
+ const existing = currentTask.tasks.get(task.id);
439
+ if (existing) {
440
+ if (skipExisting) {
441
+ if (shallowCompare && !shallowEqual(existing as any, task as any)) {
442
+ if (doReId) {
443
+ const newId = reIdFor(task.id, allTaskIds);
444
+ allTaskIds.add(newId);
445
+ taskIdMap.set(task.id, newId);
446
+ const remapped: Task = {
447
+ ...task,
448
+ id: newId,
449
+ intent_id: rewriteId(task.intent_id, intentIdMap),
450
+ input_memory_ids: rewriteIds(task.input_memory_ids, memIdMap),
451
+ output_memory_ids: rewriteIds(task.output_memory_ids, memIdMap),
452
+ };
453
+ const result = applyTaskCommand(currentTask, {
454
+ type: "task.create",
455
+ task: remapped,
456
+ });
457
+ currentTask = result.state;
458
+ report.created.tasks.push(newId);
459
+ } else {
460
+ report.conflicts.tasks.push(task.id);
461
+ }
462
+ } else {
463
+ report.skipped.tasks.push(task.id);
464
+ }
465
+ continue;
466
+ }
467
+ }
468
+ const remapped: Task = {
469
+ ...task,
470
+ intent_id: rewriteId(task.intent_id, intentIdMap),
471
+ input_memory_ids: rewriteIds(task.input_memory_ids, memIdMap),
472
+ output_memory_ids: rewriteIds(task.output_memory_ids, memIdMap),
473
+ };
474
+ const result = applyTaskCommand(currentTask, {
475
+ type: "task.create",
476
+ task: remapped,
477
+ });
478
+ currentTask = result.state;
479
+ report.created.tasks.push(task.id);
480
+ }
481
+
482
+ return {
483
+ memState: currentMem,
484
+ intentState: currentIntent,
485
+ taskState: currentTask,
486
+ report,
487
+ };
488
+ }