@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,958 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { applyCommand, mergeItem } from "../src/reducer.js";
3
+ import { createGraphState } from "../src/graph.js";
4
+ import {
5
+ getItems,
6
+ getEdges,
7
+ getScoredItems,
8
+ extractTimestamp,
9
+ } from "../src/query.js";
10
+ import {
11
+ filterContradictions,
12
+ surfaceContradictions,
13
+ applyDiversity,
14
+ smartRetrieve,
15
+ } from "../src/retrieval.js";
16
+ import {
17
+ getContradictions,
18
+ markContradiction,
19
+ resolveContradiction,
20
+ getStaleItems,
21
+ getDependents,
22
+ cascadeRetract,
23
+ getAliases,
24
+ getAliasGroup,
25
+ } from "../src/integrity.js";
26
+ import {
27
+ createIntentState,
28
+ createIntent,
29
+ applyIntentCommand,
30
+ InvalidIntentTransitionError,
31
+ } from "../src/intent.js";
32
+ import {
33
+ createTaskState,
34
+ createTask,
35
+ applyTaskCommand,
36
+ InvalidTaskTransitionError,
37
+ } from "../src/task.js";
38
+ import { exportSlice, importSlice } from "../src/transplant.js";
39
+ import { toJSON, fromJSON, stringify, parse } from "../src/serialization.js";
40
+ import { cloneGraphState } from "../src/graph.js";
41
+ import {
42
+ DuplicateMemoryError,
43
+ EdgeNotFoundError,
44
+ } from "../src/errors.js";
45
+ import type {
46
+ MemoryItem,
47
+ Edge,
48
+ GraphState,
49
+ ScoredItem,
50
+ } from "../src/types.js";
51
+ import type { IntentState, Intent } from "../src/intent.js";
52
+ import type { TaskState, Task } from "../src/task.js";
53
+
54
+ // -- helpers --
55
+
56
+ const makeItem = (
57
+ id: string,
58
+ overrides: Partial<MemoryItem> = {},
59
+ ): MemoryItem => ({
60
+ id,
61
+ scope: "test",
62
+ kind: "observation",
63
+ content: {},
64
+ author: "agent:a",
65
+ source_kind: "observed",
66
+ authority: 0.5,
67
+ ...overrides,
68
+ });
69
+
70
+ const makeEdge = (
71
+ id: string,
72
+ from: string,
73
+ to: string,
74
+ kind: string = "SUPPORTS",
75
+ overrides: Partial<Edge> = {},
76
+ ): Edge => ({
77
+ edge_id: id,
78
+ from,
79
+ to,
80
+ kind,
81
+ author: "system:rule",
82
+ source_kind: "derived_deterministic",
83
+ authority: 0.8,
84
+ active: true,
85
+ ...overrides,
86
+ });
87
+
88
+ function stateWith(items: MemoryItem[], edges: Edge[] = []): GraphState {
89
+ const s = createGraphState();
90
+ for (const i of items) s.items.set(i.id, i);
91
+ for (const e of edges) s.edges.set(e.edge_id, e);
92
+ return s;
93
+ }
94
+
95
+ function toScored(items: MemoryItem[], scores: number[]): ScoredItem[] {
96
+ return items.map((item, i) => ({ item, score: scores[i] }));
97
+ }
98
+
99
+ const makeIntent = (overrides: Partial<Intent> = {}): Intent => ({
100
+ id: "i1",
101
+ label: "find_kati",
102
+ priority: 0.8,
103
+ owner: "user:laz",
104
+ status: "active",
105
+ ...overrides,
106
+ });
107
+
108
+ const makeTask = (overrides: Partial<Task> = {}): Task => ({
109
+ id: "t1",
110
+ intent_id: "i1",
111
+ action: "search_linkedin",
112
+ status: "pending",
113
+ priority: 0.7,
114
+ attempt: 0,
115
+ ...overrides,
116
+ });
117
+
118
+ // ============================================================
119
+ // BUG FIX: mergeEdge strips undefined and protects identity fields
120
+ // ============================================================
121
+
122
+ describe("edge.update — mergeEdge fixes", () => {
123
+ it("does not overwrite edge fields with undefined", () => {
124
+ const state = stateWith(
125
+ [],
126
+ [makeEdge("e1", "m1", "m2", "SUPPORTS", { weight: 0.8 })],
127
+ );
128
+ const { state: next } = applyCommand(state, {
129
+ type: "edge.update",
130
+ edge_id: "e1",
131
+ partial: { weight: undefined, kind: "ABOUT" } as Partial<Edge>,
132
+ author: "test",
133
+ });
134
+ const edge = next.edges.get("e1")!;
135
+ expect(edge.weight).toBe(0.8); // preserved, not overwritten with undefined
136
+ expect(edge.kind).toBe("ABOUT"); // actual update applied
137
+ });
138
+
139
+ it("ignores edge_id in partial (cannot change identity)", () => {
140
+ const state = stateWith([], [makeEdge("e1", "m1", "m2")]);
141
+ const { state: next } = applyCommand(state, {
142
+ type: "edge.update",
143
+ edge_id: "e1",
144
+ partial: { edge_id: "sneaky" } as Partial<Edge>,
145
+ author: "test",
146
+ });
147
+ const edge = next.edges.get("e1")!;
148
+ expect(edge.edge_id).toBe("e1"); // identity preserved
149
+ expect(next.edges.has("sneaky")).toBe(false);
150
+ });
151
+
152
+ it("ignores from/to in partial (cannot change endpoints)", () => {
153
+ const state = stateWith([], [makeEdge("e1", "m1", "m2")]);
154
+ const { state: next } = applyCommand(state, {
155
+ type: "edge.update",
156
+ edge_id: "e1",
157
+ partial: { from: "x", to: "y" } as Partial<Edge>,
158
+ author: "test",
159
+ });
160
+ const edge = next.edges.get("e1")!;
161
+ expect(edge.from).toBe("m1");
162
+ expect(edge.to).toBe("m2");
163
+ });
164
+ });
165
+
166
+ // ============================================================
167
+ // BUG FIX: intent.update cannot bypass state machine
168
+ // ============================================================
169
+
170
+ describe("intent.update — status protection", () => {
171
+ it("ignores status in partial, does not bypass state machine", () => {
172
+ const intent = makeIntent({ status: "active" });
173
+ let state = createIntentState();
174
+ state = applyIntentCommand(state, {
175
+ type: "intent.create",
176
+ intent,
177
+ }).state;
178
+
179
+ // attempt to set status to "completed" via update
180
+ const { state: next } = applyIntentCommand(state, {
181
+ type: "intent.update",
182
+ intent_id: "i1",
183
+ partial: { status: "completed" } as Partial<Intent>,
184
+ author: "test",
185
+ });
186
+ // status should remain "active"
187
+ expect(next.intents.get("i1")!.status).toBe("active");
188
+ });
189
+
190
+ it("cannot set status to cancelled via update on cancelled intent", () => {
191
+ const intent = makeIntent({ status: "active" });
192
+ let state = createIntentState();
193
+ state = applyIntentCommand(state, {
194
+ type: "intent.create",
195
+ intent,
196
+ }).state;
197
+ state = applyIntentCommand(state, {
198
+ type: "intent.cancel",
199
+ intent_id: "i1",
200
+ author: "test",
201
+ }).state;
202
+
203
+ // try to "revive" a cancelled intent via update
204
+ const { state: next } = applyIntentCommand(state, {
205
+ type: "intent.update",
206
+ intent_id: "i1",
207
+ partial: { status: "active" } as Partial<Intent>,
208
+ author: "test",
209
+ });
210
+ expect(next.intents.get("i1")!.status).toBe("cancelled");
211
+ });
212
+
213
+ it("still allows updating other fields", () => {
214
+ const intent = makeIntent();
215
+ let state = createIntentState();
216
+ state = applyIntentCommand(state, {
217
+ type: "intent.create",
218
+ intent,
219
+ }).state;
220
+ const { state: next } = applyIntentCommand(state, {
221
+ type: "intent.update",
222
+ intent_id: "i1",
223
+ partial: { label: "new_label", priority: 0.3 },
224
+ author: "test",
225
+ });
226
+ expect(next.intents.get("i1")!.label).toBe("new_label");
227
+ expect(next.intents.get("i1")!.priority).toBe(0.3);
228
+ expect(next.intents.get("i1")!.status).toBe("active"); // unchanged
229
+ });
230
+ });
231
+
232
+ // ============================================================
233
+ // BUG FIX: task.update cannot bypass state machine
234
+ // ============================================================
235
+
236
+ describe("task.update — status protection", () => {
237
+ it("ignores status in partial, does not bypass state machine", () => {
238
+ const task = makeTask({ status: "pending" });
239
+ let state = createTaskState();
240
+ state = applyTaskCommand(state, { type: "task.create", task }).state;
241
+
242
+ const { state: next } = applyTaskCommand(state, {
243
+ type: "task.update",
244
+ task_id: "t1",
245
+ partial: { status: "completed" } as Partial<Task>,
246
+ author: "test",
247
+ });
248
+ expect(next.tasks.get("t1")!.status).toBe("pending");
249
+ });
250
+
251
+ it("cannot revive a failed task via update", () => {
252
+ const task = makeTask({ status: "pending" });
253
+ let state = createTaskState();
254
+ state = applyTaskCommand(state, { type: "task.create", task }).state;
255
+ state = applyTaskCommand(state, {
256
+ type: "task.start",
257
+ task_id: "t1",
258
+ }).state;
259
+ state = applyTaskCommand(state, {
260
+ type: "task.fail",
261
+ task_id: "t1",
262
+ error: "oops",
263
+ }).state;
264
+
265
+ const { state: next } = applyTaskCommand(state, {
266
+ type: "task.update",
267
+ task_id: "t1",
268
+ partial: { status: "running" } as Partial<Task>,
269
+ author: "test",
270
+ });
271
+ expect(next.tasks.get("t1")!.status).toBe("failed");
272
+ });
273
+ });
274
+
275
+ // ============================================================
276
+ // BUG FIX: applyDiversity preserves contradicted_by
277
+ // ============================================================
278
+
279
+ describe("applyDiversity — contradicted_by preservation", () => {
280
+ it("preserves contradicted_by annotations through diversity", () => {
281
+ const m1 = makeItem("m1", { author: "a" });
282
+ const m2 = makeItem("m2", { author: "a" });
283
+ const m3 = makeItem("m3", { author: "b" });
284
+
285
+ const scored: ScoredItem[] = [
286
+ { item: m1, score: 0.9, contradicted_by: [m3] },
287
+ { item: m2, score: 0.8 },
288
+ { item: m3, score: 0.7, contradicted_by: [m1] },
289
+ ];
290
+
291
+ const result = applyDiversity(scored, { author_penalty: 0.1 });
292
+ const r1 = result.find((s) => s.item.id === "m1")!;
293
+ const r3 = result.find((s) => s.item.id === "m3")!;
294
+ expect(r1.contradicted_by).toEqual([m3]);
295
+ expect(r3.contradicted_by).toEqual([m1]);
296
+ });
297
+ });
298
+
299
+ // ============================================================
300
+ // BUG FIX: smartRetrieve surface+diversity preserves contradicted_by
301
+ // ============================================================
302
+
303
+ describe("smartRetrieve — surface + diversity pipeline", () => {
304
+ it("returns contradicted_by when both surface and diversity are used", () => {
305
+ const m1 = makeItem("m1", { authority: 0.9, author: "a" });
306
+ const m2 = makeItem("m2", { authority: 0.8, author: "a" });
307
+ let state = stateWith([m1, m2]);
308
+ const { state: marked } = markContradiction(
309
+ state,
310
+ "m1",
311
+ "m2",
312
+ "system:detector",
313
+ );
314
+
315
+ const result = smartRetrieve(marked, {
316
+ budget: 1000,
317
+ costFn: () => 1,
318
+ weights: { authority: 1 },
319
+ contradictions: "surface",
320
+ diversity: { author_penalty: 0.05 },
321
+ });
322
+
323
+ const item1 = result.find((s) => s.item.id === "m1");
324
+ const item2 = result.find((s) => s.item.id === "m2");
325
+ expect(item1?.contradicted_by).toBeDefined();
326
+ expect(item1!.contradicted_by!.length).toBeGreaterThan(0);
327
+ expect(item2?.contradicted_by).toBeDefined();
328
+ });
329
+ });
330
+
331
+ // ============================================================
332
+ // BUG FIX: computeDecayMultiplier throws on unknown interval
333
+ // ============================================================
334
+
335
+ describe("decay interval validation", () => {
336
+ it("throws RangeError for unknown decay interval", () => {
337
+ const m1 = makeItem("m1", { authority: 0.5 });
338
+ const state = stateWith([m1]);
339
+
340
+ expect(() =>
341
+ getScoredItems(state, {
342
+ authority: 1,
343
+ decay: {
344
+ rate: 0.1,
345
+ interval: "month" as any,
346
+ type: "exponential",
347
+ },
348
+ }),
349
+ ).toThrow(RangeError);
350
+ });
351
+
352
+ it("throws with descriptive message for unknown interval", () => {
353
+ const m1 = makeItem("m1", { authority: 0.5 });
354
+ const state = stateWith([m1]);
355
+
356
+ expect(() =>
357
+ getScoredItems(state, {
358
+ authority: 1,
359
+ decay: {
360
+ rate: 0.1,
361
+ interval: "month" as any,
362
+ type: "exponential",
363
+ },
364
+ }),
365
+ ).toThrow(/Unknown decay interval.*month/);
366
+ });
367
+ });
368
+
369
+ // ============================================================
370
+ // COVERAGE: resolveContradiction without prior CONTRADICTS edge
371
+ // ============================================================
372
+
373
+ describe("resolveContradiction edge cases", () => {
374
+ it("throws when no CONTRADICTS edge exists between the items", () => {
375
+ const state = stateWith([
376
+ makeItem("m1", { authority: 0.9 }),
377
+ makeItem("m2", { authority: 0.7 }),
378
+ ]);
379
+ // no markContradiction first — calling resolve directly should throw
380
+ expect(() =>
381
+ resolveContradiction(state, "m1", "m2", "system:resolver"),
382
+ ).toThrow(/No active CONTRADICTS edge/);
383
+ });
384
+ });
385
+
386
+ // ============================================================
387
+ // COVERAGE: filterContradictions tie-breaking
388
+ // ============================================================
389
+
390
+ describe("filterContradictions — equal scores", () => {
391
+ it("excludes one item when scores are exactly equal", () => {
392
+ const m1 = makeItem("m1");
393
+ const m2 = makeItem("m2");
394
+ let state = stateWith([m1, m2]);
395
+ state = markContradiction(state, "m1", "m2", "system:detector").state;
396
+
397
+ const scored = toScored([m1, m2], [0.5, 0.5]);
398
+ const filtered = filterContradictions(state, scored);
399
+ // one should be excluded
400
+ expect(filtered).toHaveLength(1);
401
+ });
402
+
403
+ it("tiebreaks deterministically regardless of edge direction", () => {
404
+ const m1 = makeItem("aaa");
405
+ const m2 = makeItem("zzz");
406
+
407
+ // edge direction: m1 -> m2
408
+ let state1 = stateWith([m1, m2]);
409
+ state1 = markContradiction(state1, "aaa", "zzz", "sys").state;
410
+ const r1 = filterContradictions(state1, toScored([m1, m2], [0.5, 0.5]));
411
+
412
+ // edge direction: m2 -> m1
413
+ let state2 = stateWith([m1, m2]);
414
+ state2 = markContradiction(state2, "zzz", "aaa", "sys").state;
415
+ const r2 = filterContradictions(state2, toScored([m1, m2], [0.5, 0.5]));
416
+
417
+ // same item should survive regardless of edge direction
418
+ expect(r1).toHaveLength(1);
419
+ expect(r2).toHaveLength(1);
420
+ expect(r1[0].item.id).toBe(r2[0].item.id);
421
+ // lexicographically smaller id survives
422
+ expect(r1[0].item.id).toBe("aaa");
423
+ });
424
+ });
425
+
426
+ // ============================================================
427
+ // COVERAGE: getContradictions when one side is retracted
428
+ // ============================================================
429
+
430
+ describe("getContradictions — retracted items", () => {
431
+ it("skips contradictions where one item has been retracted", () => {
432
+ const m1 = makeItem("m1");
433
+ const m2 = makeItem("m2");
434
+ let state = stateWith([m1, m2]);
435
+ state = markContradiction(state, "m1", "m2", "system:detector").state;
436
+ // retract m2
437
+ state = applyCommand(state, {
438
+ type: "memory.retract",
439
+ item_id: "m2",
440
+ author: "test",
441
+ }).state;
442
+
443
+ const contradictions = getContradictions(state);
444
+ expect(contradictions).toHaveLength(0);
445
+ });
446
+ });
447
+
448
+ // ============================================================
449
+ // COVERAGE: getScoredItems with post filter
450
+ // ============================================================
451
+
452
+ describe("getScoredItems — post filter", () => {
453
+ it("applies post filter after scoring", () => {
454
+ const state = stateWith([
455
+ makeItem("m1", { authority: 0.9, scope: "a" }),
456
+ makeItem("m2", { authority: 0.8, scope: "b" }),
457
+ makeItem("m3", { authority: 0.7, scope: "a" }),
458
+ ]);
459
+
460
+ const result = getScoredItems(state, { authority: 1 }, {
461
+ post: { scope: "a" },
462
+ });
463
+ expect(result).toHaveLength(2);
464
+ expect(result.every((r) => r.item.scope === "a")).toBe(true);
465
+ // should be sorted by score
466
+ expect(result[0].item.id).toBe("m1");
467
+ expect(result[1].item.id).toBe("m3");
468
+ });
469
+
470
+ it("post filter can use score-based range after scoring", () => {
471
+ const state = stateWith([
472
+ makeItem("m1", { authority: 0.9, importance: 0.1 }),
473
+ makeItem("m2", { authority: 0.3, importance: 0.9 }),
474
+ makeItem("m3", { authority: 0.1, importance: 0.1 }),
475
+ ]);
476
+
477
+ // score by authority, then post-filter for high-importance only
478
+ const result = getScoredItems(
479
+ state,
480
+ { authority: 1 },
481
+ { post: { range: { importance: { min: 0.5 } } } },
482
+ );
483
+ expect(result).toHaveLength(1);
484
+ expect(result[0].item.id).toBe("m2");
485
+ });
486
+ });
487
+
488
+ // ============================================================
489
+ // COVERAGE: getEdges with to filter
490
+ // ============================================================
491
+
492
+ describe("getEdges — to filter", () => {
493
+ it("filters edges by to field", () => {
494
+ const state = stateWith(
495
+ [],
496
+ [
497
+ makeEdge("e1", "m1", "m2"),
498
+ makeEdge("e2", "m1", "m3"),
499
+ makeEdge("e3", "m2", "m3"),
500
+ ],
501
+ );
502
+ const result = getEdges(state, { to: "m3" });
503
+ expect(result).toHaveLength(2);
504
+ expect(result.every((e) => e.to === "m3")).toBe(true);
505
+ });
506
+ });
507
+
508
+ // ============================================================
509
+ // COVERAGE: cascadeRetract on nonexistent item
510
+ // ============================================================
511
+
512
+ describe("cascadeRetract — edge cases", () => {
513
+ it("returns empty retracted list for nonexistent item", () => {
514
+ const state = stateWith([makeItem("m1")]);
515
+ const { state: next, retracted } = cascadeRetract(
516
+ state,
517
+ "nonexistent",
518
+ "test",
519
+ );
520
+ expect(retracted).toHaveLength(0);
521
+ expect(next.items.has("m1")).toBe(true);
522
+ });
523
+
524
+ it("handles circular parent-child dependencies", () => {
525
+ // m1 -> m2 -> m3 -> m1 (cycle via parents)
526
+ const state = stateWith([
527
+ makeItem("m1", { parents: ["m3"] }),
528
+ makeItem("m2", { parents: ["m1"] }),
529
+ makeItem("m3", { parents: ["m2"] }),
530
+ ]);
531
+ const deps = getDependents(state, "m1", true);
532
+ // should not infinite loop; should find m2 and m3
533
+ expect(deps.length).toBeGreaterThanOrEqual(2);
534
+ const ids = deps.map((d) => d.id).sort();
535
+ expect(ids).toContain("m2");
536
+ expect(ids).toContain("m3");
537
+ });
538
+ });
539
+
540
+ // ============================================================
541
+ // COVERAGE: getAliasGroup with nonexistent start
542
+ // ============================================================
543
+
544
+ describe("getAliasGroup — nonexistent item", () => {
545
+ it("returns empty array for nonexistent item id", () => {
546
+ const state = stateWith([makeItem("m1")]);
547
+ const group = getAliasGroup(state, "nonexistent");
548
+ expect(group).toHaveLength(0);
549
+ });
550
+ });
551
+
552
+ // ============================================================
553
+ // COVERAGE: cloneGraphState shallow clone behavior
554
+ // ============================================================
555
+
556
+ describe("cloneGraphState — shallow clone", () => {
557
+ it("Map-level mutations do not affect original", () => {
558
+ const state = stateWith([makeItem("m1")]);
559
+ const clone = cloneGraphState(state);
560
+ clone.items.delete("m1");
561
+ expect(state.items.has("m1")).toBe(true);
562
+ expect(clone.items.has("m1")).toBe(false);
563
+ });
564
+
565
+ it("value-level references are shared (shallow)", () => {
566
+ const state = stateWith([makeItem("m1", { content: { x: 1 } })]);
567
+ const clone = cloneGraphState(state);
568
+ // both maps point to the same MemoryItem object
569
+ expect(clone.items.get("m1")).toBe(state.items.get("m1"));
570
+ });
571
+ });
572
+
573
+ // ============================================================
574
+ // COVERAGE: extractTimestamp with non-uuidv7 ids
575
+ // ============================================================
576
+
577
+ describe("extractTimestamp — edge cases", () => {
578
+ it("returns NaN for non-uuidv7 formatted id", () => {
579
+ const ts = extractTimestamp("not-a-uuid");
580
+ // "nota-uuid" after removing hyphens -> "notauuid", first 12 chars
581
+ // parseInt of non-hex string = NaN
582
+ expect(Number.isNaN(ts)).toBe(true);
583
+ });
584
+
585
+ it("extracts valid timestamp from real uuidv7", () => {
586
+ // uuidv7 encodes timestamp in first 48 bits
587
+ const now = Date.now();
588
+ const ts = extractTimestamp(
589
+ now.toString(16).padStart(12, "0").slice(0, 8) +
590
+ "-" +
591
+ now.toString(16).padStart(12, "0").slice(8, 12) +
592
+ "-7000-8000-000000000000",
593
+ );
594
+ expect(ts).toBe(now);
595
+ });
596
+ });
597
+
598
+ // ============================================================
599
+ // COVERAGE: serialization.parse with malformed input
600
+ // ============================================================
601
+
602
+ describe("serialization — error handling", () => {
603
+ it("throws on malformed JSON", () => {
604
+ expect(() => parse("{not valid json")).toThrow();
605
+ });
606
+
607
+ it("creates empty maps when items field is missing", () => {
608
+ // parse doesn't validate shape — this documents the behavior
609
+ const state = parse('{"edges": []}');
610
+ expect(state.items.size).toBe(0);
611
+ expect(state.edges.size).toBe(0);
612
+ });
613
+
614
+ it("round-trips correctly", () => {
615
+ const state = stateWith(
616
+ [makeItem("m1", { content: { text: "hello" } })],
617
+ [makeEdge("e1", "m1", "m2")],
618
+ );
619
+ const json = stringify(state);
620
+ const restored = parse(json);
621
+ expect(restored.items.get("m1")!.content).toEqual({ text: "hello" });
622
+ expect(restored.edges.get("e1")!.from).toBe("m1");
623
+ });
624
+ });
625
+
626
+ // ============================================================
627
+ // COVERAGE: importSlice with skipExistingIds: false + collision
628
+ // ============================================================
629
+
630
+ describe("importSlice — skipExistingIds: false", () => {
631
+ it("throws DuplicateMemoryError when importing existing id with skipExisting=false", () => {
632
+ const mem = stateWith([makeItem("m1")]);
633
+ const intents = createIntentState();
634
+ const tasks = createTaskState();
635
+
636
+ const slice = {
637
+ memories: [makeItem("m1", { content: { new: true } })],
638
+ edges: [],
639
+ intents: [],
640
+ tasks: [],
641
+ };
642
+
643
+ expect(() =>
644
+ importSlice(mem, intents, tasks, slice, { skipExistingIds: false }),
645
+ ).toThrow(DuplicateMemoryError);
646
+ });
647
+
648
+ it("creates non-colliding items with skipExisting=false", () => {
649
+ const mem = stateWith([makeItem("m1")]);
650
+ const intents = createIntentState();
651
+ const tasks = createTaskState();
652
+
653
+ const slice = {
654
+ memories: [makeItem("m2")],
655
+ edges: [],
656
+ intents: [],
657
+ tasks: [],
658
+ };
659
+
660
+ const result = importSlice(mem, intents, tasks, slice, {
661
+ skipExistingIds: false,
662
+ });
663
+ expect(result.memState.items.has("m2")).toBe(true);
664
+ expect(result.report.created.memories).toContain("m2");
665
+ });
666
+ });
667
+
668
+ // ============================================================
669
+ // COVERAGE: exportSlice with include_aliases
670
+ // ============================================================
671
+
672
+ describe("exportSlice — include_aliases", () => {
673
+ it("walks alias edges and includes aliased items", () => {
674
+ const m1 = makeItem("m1");
675
+ const m2 = makeItem("m2");
676
+ const m3 = makeItem("m3");
677
+ let state = stateWith([m1, m2, m3]);
678
+ const { state: aliased } = applyCommand(state, {
679
+ type: "edge.create",
680
+ edge: makeEdge("ae1", "m1", "m2", "ALIAS"),
681
+ });
682
+ const { state: aliased2 } = applyCommand(aliased, {
683
+ type: "edge.create",
684
+ edge: makeEdge("ae2", "m2", "m3", "ALIAS"),
685
+ });
686
+
687
+ const intents = createIntentState();
688
+ const tasks = createTaskState();
689
+
690
+ const slice = exportSlice(aliased2, intents, tasks, {
691
+ memory_ids: ["m1"],
692
+ include_aliases: true,
693
+ });
694
+
695
+ const ids = slice.memories.map((m) => m.id).sort();
696
+ expect(ids).toContain("m1");
697
+ expect(ids).toContain("m2");
698
+ expect(ids).toContain("m3");
699
+ // alias edges should be included
700
+ expect(slice.edges.length).toBeGreaterThanOrEqual(2);
701
+ });
702
+ });
703
+
704
+ // ============================================================
705
+ // COVERAGE: smartRetrieve with no contradiction handling
706
+ // ============================================================
707
+
708
+ describe("smartRetrieve — no contradiction handling", () => {
709
+ it("returns all items when contradictions option is undefined", () => {
710
+ const m1 = makeItem("m1", { authority: 0.9 });
711
+ const m2 = makeItem("m2", { authority: 0.8 });
712
+ let state = stateWith([m1, m2]);
713
+ state = markContradiction(state, "m1", "m2", "system:detector").state;
714
+
715
+ const result = smartRetrieve(state, {
716
+ budget: 1000,
717
+ costFn: () => 1,
718
+ weights: { authority: 1 },
719
+ // contradictions: undefined — no handling
720
+ });
721
+ // both items should be returned
722
+ expect(result).toHaveLength(2);
723
+ });
724
+ });
725
+
726
+ // ============================================================
727
+ // COVERAGE: surfaceContradictions idempotency
728
+ // ============================================================
729
+
730
+ describe("surfaceContradictions — repeated calls", () => {
731
+ it("does not accumulate duplicate contradicted_by entries on fresh clone", () => {
732
+ const m1 = makeItem("m1");
733
+ const m2 = makeItem("m2");
734
+ let state = stateWith([m1, m2]);
735
+ state = markContradiction(state, "m1", "m2", "system:detector").state;
736
+
737
+ const scored = toScored([m1, m2], [0.5, 0.5]);
738
+ const result1 = surfaceContradictions(state, scored);
739
+ // calling again with fresh scored (no stale contradicted_by)
740
+ const result2 = surfaceContradictions(state, toScored([m1, m2], [0.5, 0.5]));
741
+
742
+ const r1m1 = result1.find((s) => s.item.id === "m1")!;
743
+ const r2m1 = result2.find((s) => s.item.id === "m1")!;
744
+ expect(r1m1.contradicted_by).toHaveLength(1);
745
+ expect(r2m1.contradicted_by).toHaveLength(1);
746
+ });
747
+ });
748
+
749
+ // ============================================================
750
+ // COVERAGE: applyDiversity with mixed parent/no-parent items
751
+ // ============================================================
752
+
753
+ describe("applyDiversity — mixed parents", () => {
754
+ it("handles items with and without parents", () => {
755
+ const m1 = makeItem("m1", { parents: ["p1"] });
756
+ const m2 = makeItem("m2"); // no parents
757
+ const m3 = makeItem("m3", { parents: ["p1"] });
758
+
759
+ const scored = toScored([m1, m2, m3], [0.9, 0.8, 0.7]);
760
+ const result = applyDiversity(scored, { parent_penalty: 0.1 });
761
+
762
+ // m1 has parent p1 (first seen, no penalty)
763
+ // m2 has no parents (no penalty)
764
+ // m3 has parent p1 (second occurrence, penalty applied)
765
+ const m3result = result.find((s) => s.item.id === "m3")!;
766
+ expect(m3result.score).toBeLessThan(0.7); // penalty applied
767
+ const m2result = result.find((s) => s.item.id === "m2")!;
768
+ expect(m2result.score).toBe(0.8); // no penalty
769
+ });
770
+ });
771
+
772
+ // ============================================================
773
+ // COVERAGE: memory.update — mergeItem behavior
774
+ // ============================================================
775
+
776
+ describe("mergeItem — edge cases", () => {
777
+ it("stripUndefined prevents accidental key deletion in content", () => {
778
+ const existing = makeItem("m1", { content: { a: 1, b: 2 } });
779
+ const merged = mergeItem(existing, {
780
+ content: { a: undefined, c: 3 } as any,
781
+ });
782
+ // a should be preserved (undefined stripped), b preserved, c added
783
+ expect(merged.content.a).toBe(1);
784
+ expect(merged.content.b).toBe(2);
785
+ expect(merged.content.c).toBe(3);
786
+ });
787
+
788
+ it("stripUndefined prevents accidental key deletion in meta", () => {
789
+ const existing = makeItem("m1", { meta: { agent_id: "bot", x: 1 } });
790
+ const merged = mergeItem(existing, {
791
+ meta: { agent_id: undefined, y: 2 } as any,
792
+ });
793
+ expect(merged.meta!.agent_id).toBe("bot");
794
+ expect(merged.meta!.x).toBe(1);
795
+ expect(merged.meta!.y).toBe(2);
796
+ });
797
+
798
+ it("does not allow changing id via partial", () => {
799
+ const existing = makeItem("m1");
800
+ const merged = mergeItem(existing, { id: "sneaky" });
801
+ expect(merged.id).toBe("m1");
802
+ });
803
+ });
804
+
805
+ // ============================================================
806
+ // COVERAGE: importSlice re-id on intents and tasks
807
+ // ============================================================
808
+
809
+ describe("importSlice — re-id intents and tasks", () => {
810
+ it("remaps intent root_memory_ids when memories are re-id'd", () => {
811
+ // set up existing state with m1
812
+ const mem = stateWith([makeItem("m1", { content: { old: true } })]);
813
+ const intents = createIntentState();
814
+ const tasks = createTaskState();
815
+
816
+ // slice has m1 (different content) and intent referencing m1
817
+ const slice = {
818
+ memories: [makeItem("m1", { content: { new: true } })],
819
+ edges: [],
820
+ intents: [
821
+ makeIntent({
822
+ id: "i1",
823
+ root_memory_ids: ["m1"],
824
+ }),
825
+ ],
826
+ tasks: [],
827
+ };
828
+
829
+ const result = importSlice(mem, intents, tasks, slice, {
830
+ skipExistingIds: true,
831
+ shallowCompareExisting: true,
832
+ reIdOnDifference: true,
833
+ });
834
+
835
+ // memory should have been re-id'd
836
+ expect(result.report.created.memories).toHaveLength(1);
837
+ const newMemId = result.report.created.memories[0];
838
+ expect(newMemId).not.toBe("m1");
839
+
840
+ // intent should reference the new memory id
841
+ const importedIntent = result.report.created.intents[0];
842
+ const intent = result.intentState.intents.get(importedIntent)!;
843
+ expect(intent.root_memory_ids).toContain(newMemId);
844
+ expect(intent.root_memory_ids).not.toContain("m1");
845
+ });
846
+
847
+ it("remaps task memory ids when memories are re-id'd", () => {
848
+ const mem = stateWith([makeItem("m1", { content: { old: true } })]);
849
+ let intents = createIntentState();
850
+ intents = applyIntentCommand(intents, {
851
+ type: "intent.create",
852
+ intent: makeIntent({ id: "i1" }),
853
+ }).state;
854
+ const tasks = createTaskState();
855
+
856
+ const slice = {
857
+ memories: [makeItem("m1", { content: { new: true } })],
858
+ edges: [],
859
+ intents: [],
860
+ tasks: [
861
+ makeTask({
862
+ id: "t1",
863
+ intent_id: "i1",
864
+ input_memory_ids: ["m1"],
865
+ output_memory_ids: ["m1"],
866
+ }),
867
+ ],
868
+ };
869
+
870
+ const result = importSlice(mem, intents, tasks, slice, {
871
+ skipExistingIds: true,
872
+ shallowCompareExisting: true,
873
+ reIdOnDifference: true,
874
+ });
875
+
876
+ const newMemId = result.report.created.memories[0];
877
+ const importedTaskId = result.report.created.tasks[0];
878
+ const task = result.taskState.tasks.get(importedTaskId)!;
879
+ expect(task.input_memory_ids).toContain(newMemId);
880
+ expect(task.output_memory_ids).toContain(newMemId);
881
+ });
882
+ });
883
+
884
+ // ============================================================
885
+ // COVERAGE: edge collision in importSlice
886
+ // ============================================================
887
+
888
+ describe("importSlice — edge collision", () => {
889
+ it("skips edge when id already exists and skipExisting is true", () => {
890
+ const edge = makeEdge("e1", "m1", "m2");
891
+ const mem = stateWith([makeItem("m1"), makeItem("m2")], [edge]);
892
+ const intents = createIntentState();
893
+ const tasks = createTaskState();
894
+
895
+ const slice = {
896
+ memories: [],
897
+ edges: [makeEdge("e1", "m1", "m2", "ABOUT")],
898
+ intents: [],
899
+ tasks: [],
900
+ };
901
+
902
+ const result = importSlice(mem, intents, tasks, slice);
903
+ expect(result.report.skipped.edges).toContain("e1");
904
+ // original edge should be unchanged
905
+ expect(result.memState.edges.get("e1")!.kind).toBe("SUPPORTS");
906
+ });
907
+ });
908
+
909
+ // ============================================================
910
+ // COVERAGE: created filter boundary (exclusive after)
911
+ // ============================================================
912
+
913
+ describe("created filter — boundary semantics", () => {
914
+ it("before is exclusive (item at exact boundary is excluded)", () => {
915
+ // Create an item with a known id that encodes a specific timestamp
916
+ const ts = 1700000000000;
917
+ const hex = ts.toString(16).padStart(12, "0");
918
+ const id =
919
+ hex.slice(0, 8) +
920
+ "-" +
921
+ hex.slice(8, 12) +
922
+ "-7000-8000-000000000000";
923
+
924
+ const state = stateWith([makeItem(id)]);
925
+ const items = getItems(state, { created: { before: ts } });
926
+ expect(items).toHaveLength(0); // exclusive: at exactly ts, excluded
927
+ });
928
+
929
+ it("after is inclusive (item at exact boundary is included)", () => {
930
+ const ts = 1700000000000;
931
+ const hex = ts.toString(16).padStart(12, "0");
932
+ const id =
933
+ hex.slice(0, 8) +
934
+ "-" +
935
+ hex.slice(8, 12) +
936
+ "-7000-8000-000000000000";
937
+
938
+ const state = stateWith([makeItem(id)]);
939
+ const items = getItems(state, { created: { after: ts } });
940
+ expect(items).toHaveLength(1); // inclusive: at exactly ts, included
941
+ });
942
+
943
+ it("item between before and after is included", () => {
944
+ const ts = 1700000000000;
945
+ const hex = ts.toString(16).padStart(12, "0");
946
+ const id =
947
+ hex.slice(0, 8) +
948
+ "-" +
949
+ hex.slice(8, 12) +
950
+ "-7000-8000-000000000000";
951
+
952
+ const state = stateWith([makeItem(id)]);
953
+ const items = getItems(state, {
954
+ created: { after: ts - 1, before: ts + 1 },
955
+ });
956
+ expect(items).toHaveLength(1);
957
+ });
958
+ });