@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
package/src/query.ts ADDED
@@ -0,0 +1,357 @@
1
+ import type {
2
+ GraphState,
3
+ MemoryItem,
4
+ Edge,
5
+ MemoryFilter,
6
+ EdgeFilter,
7
+ QueryOptions,
8
+ SortField,
9
+ SortOption,
10
+ ScoreWeights,
11
+ ScoredItem,
12
+ } from "./types.js";
13
+
14
+ function resolvePath(obj: unknown, path: string): unknown {
15
+ let current = obj;
16
+ for (const segment of path.split(".")) {
17
+ if (
18
+ current === null ||
19
+ current === undefined ||
20
+ typeof current !== "object"
21
+ ) {
22
+ return undefined;
23
+ }
24
+ current = (current as Record<string, unknown>)[segment];
25
+ }
26
+ return current;
27
+ }
28
+
29
+ function matchesRange(
30
+ value: number | undefined,
31
+ range: { min?: number; max?: number } | undefined,
32
+ ): boolean {
33
+ if (!range) return true;
34
+ if (range.min !== undefined && (value === undefined || value < range.min))
35
+ return false;
36
+ if (range.max !== undefined && (value === undefined || value > range.max))
37
+ return false;
38
+ return true;
39
+ }
40
+
41
+ function matchesFilter(item: MemoryItem, filter: MemoryFilter): boolean {
42
+ // ids
43
+ if (filter.ids !== undefined && !filter.ids.includes(item.id)) return false;
44
+
45
+ // scope
46
+ if (filter.scope !== undefined && item.scope !== filter.scope) return false;
47
+ if (
48
+ filter.scope_prefix !== undefined &&
49
+ !item.scope.startsWith(filter.scope_prefix)
50
+ )
51
+ return false;
52
+
53
+ // kind / source
54
+ if (filter.author !== undefined && item.author !== filter.author)
55
+ return false;
56
+ if (filter.kind !== undefined && item.kind !== filter.kind) return false;
57
+ if (
58
+ filter.source_kind !== undefined &&
59
+ item.source_kind !== filter.source_kind
60
+ )
61
+ return false;
62
+
63
+ // score ranges
64
+ if (filter.range) {
65
+ if (!matchesRange(item.authority, filter.range.authority)) return false;
66
+ if (!matchesRange(item.conviction, filter.range.conviction)) return false;
67
+ if (!matchesRange(item.importance, filter.range.importance)) return false;
68
+ }
69
+
70
+ // parent sugar
71
+ if (filter.has_parent !== undefined) {
72
+ if (!item.parents || !item.parents.includes(filter.has_parent))
73
+ return false;
74
+ }
75
+ if (filter.is_root !== undefined) {
76
+ const hasParents = item.parents !== undefined && item.parents.length > 0;
77
+ if (filter.is_root && hasParents) return false;
78
+ if (!filter.is_root && !hasParents) return false;
79
+ }
80
+
81
+ // parents (advanced)
82
+ if (filter.parents) {
83
+ const p = item.parents ?? [];
84
+ if (filter.parents.includes !== undefined) {
85
+ if (!p.includes(filter.parents.includes)) return false;
86
+ }
87
+ if (filter.parents.includes_any !== undefined) {
88
+ if (!filter.parents.includes_any.some((id) => p.includes(id)))
89
+ return false;
90
+ }
91
+ if (filter.parents.includes_all !== undefined) {
92
+ if (!filter.parents.includes_all.every((id) => p.includes(id)))
93
+ return false;
94
+ }
95
+ if (filter.parents.count !== undefined) {
96
+ if (!matchesRange(p.length, filter.parents.count)) return false;
97
+ }
98
+ }
99
+
100
+ // decay
101
+ if (filter.decay) {
102
+ const multiplier = computeDecayMultiplier(item.id, filter.decay.config);
103
+ if (multiplier < filter.decay.min) return false;
104
+ }
105
+
106
+ // created
107
+ if (filter.created) {
108
+ const ts = extractTimestamp(item.id);
109
+ if (filter.created.before !== undefined && ts >= filter.created.before)
110
+ return false;
111
+ if (filter.created.after !== undefined && ts < filter.created.after)
112
+ return false;
113
+ }
114
+
115
+ // combinators
116
+ if (filter.not && matchesFilter(item, filter.not)) return false;
117
+ if (filter.meta !== undefined) {
118
+ for (const [path, value] of Object.entries(filter.meta)) {
119
+ if (resolvePath(item.meta, path) !== value) return false;
120
+ }
121
+ }
122
+ if (filter.meta_has !== undefined) {
123
+ for (const path of filter.meta_has) {
124
+ if (resolvePath(item.meta, path) === undefined) return false;
125
+ }
126
+ }
127
+ if (filter.or !== undefined && filter.or.length > 0) {
128
+ if (!filter.or.some((sub) => matchesFilter(item, sub))) return false;
129
+ }
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Extract millisecond timestamp from a uuidv7 id.
135
+ * uuidv7 encodes unix ms in the first 48 bits.
136
+ */
137
+ export function extractTimestamp(uuidv7Id: string): number {
138
+ const hex = uuidv7Id.replace(/-/g, "").slice(0, 12);
139
+ return parseInt(hex, 16);
140
+ }
141
+
142
+ function getSortValue(item: MemoryItem, field: SortField): number {
143
+ switch (field) {
144
+ case "authority":
145
+ return item.authority;
146
+ case "conviction":
147
+ return item.conviction ?? 0;
148
+ case "importance":
149
+ return item.importance ?? 0;
150
+ case "recency":
151
+ return extractTimestamp(item.id);
152
+ }
153
+ }
154
+
155
+ export function getItems(
156
+ state: GraphState,
157
+ filter?: MemoryFilter,
158
+ options?: QueryOptions,
159
+ ): MemoryItem[] {
160
+ let results: MemoryItem[];
161
+
162
+ if (!filter) {
163
+ results = [...state.items.values()];
164
+ } else {
165
+ results = [];
166
+ for (const item of state.items.values()) {
167
+ if (matchesFilter(item, filter)) results.push(item);
168
+ }
169
+ }
170
+
171
+ if (options?.sort) {
172
+ const sorts: SortOption[] = Array.isArray(options.sort)
173
+ ? options.sort
174
+ : [options.sort];
175
+ results.sort((a, b) => {
176
+ for (const { field, order } of sorts) {
177
+ const va = getSortValue(a, field);
178
+ const vb = getSortValue(b, field);
179
+ if (va < vb) return order === "asc" ? -1 : 1;
180
+ if (va > vb) return order === "asc" ? 1 : -1;
181
+ }
182
+ return 0;
183
+ });
184
+ }
185
+
186
+ if (options?.offset !== undefined || options?.limit !== undefined) {
187
+ const start = options.offset ?? 0;
188
+ const end = options.limit !== undefined ? start + options.limit : undefined;
189
+ results = results.slice(start, end);
190
+ }
191
+
192
+ return results;
193
+ }
194
+
195
+ const INTERVAL_MS: Record<string, number> = {
196
+ hour: 3_600_000,
197
+ day: 86_400_000,
198
+ week: 604_800_000,
199
+ };
200
+
201
+ function computeDecayMultiplier(
202
+ itemId: string,
203
+ decay: import("./types.js").DecayConfig,
204
+ ): number {
205
+ const ageMs = Date.now() - extractTimestamp(itemId);
206
+ if (ageMs <= 0) return 1; // future item (clock skew) — no decay
207
+ const intervalMs = INTERVAL_MS[decay.interval];
208
+ if (intervalMs === undefined) {
209
+ throw new RangeError(
210
+ `Unknown decay interval: "${decay.interval}". Expected "hour", "day", or "week".`,
211
+ );
212
+ }
213
+ const intervals = ageMs / intervalMs;
214
+
215
+ switch (decay.type) {
216
+ case "exponential":
217
+ return Math.pow(1 - decay.rate, intervals);
218
+ case "linear":
219
+ return Math.max(0, 1 - decay.rate * intervals);
220
+ case "step":
221
+ return Math.pow(1 - decay.rate, Math.floor(intervals));
222
+ }
223
+ }
224
+
225
+ function computeScore(item: MemoryItem, weights: ScoreWeights): number {
226
+ const base =
227
+ (weights.authority ?? 0) * item.authority +
228
+ (weights.conviction ?? 0) * (item.conviction ?? 0) +
229
+ (weights.importance ?? 0) * (item.importance ?? 0);
230
+
231
+ if (!weights.decay) return base;
232
+
233
+ return base * computeDecayMultiplier(item.id, weights.decay);
234
+ }
235
+
236
+ export interface ScoredQueryOptions {
237
+ pre?: MemoryFilter; // filter before scoring
238
+ post?: MemoryFilter; // filter after scoring (applied to scored items)
239
+ min_score?: number; // drop items below this score
240
+ limit?: number;
241
+ offset?: number;
242
+ }
243
+
244
+ export function getScoredItems(
245
+ state: GraphState,
246
+ weights: ScoreWeights,
247
+ options?: ScoredQueryOptions,
248
+ ): ScoredItem[] {
249
+ const items = getItems(state, options?.pre);
250
+
251
+ let scored = items.map((item) => ({
252
+ item,
253
+ score: computeScore(item, weights),
254
+ }));
255
+
256
+ scored.sort((a, b) => b.score - a.score);
257
+
258
+ if (options?.min_score !== undefined) {
259
+ scored = scored.filter((s) => s.score >= options.min_score!);
260
+ }
261
+
262
+ if (options?.post) {
263
+ const postFilter = options.post;
264
+ scored = scored.filter((s) => matchesFilter(s.item, postFilter));
265
+ }
266
+
267
+ if (options?.offset !== undefined || options?.limit !== undefined) {
268
+ const start = options.offset ?? 0;
269
+ const end = options.limit !== undefined ? start + options.limit : undefined;
270
+ scored = scored.slice(start, end);
271
+ }
272
+
273
+ return scored;
274
+ }
275
+
276
+ export function getEdges(state: GraphState, filter?: EdgeFilter): Edge[] {
277
+ const activeOnly = filter?.active_only ?? true;
278
+ const results: Edge[] = [];
279
+ for (const edge of state.edges.values()) {
280
+ if (activeOnly && !edge.active) continue;
281
+ if (filter?.from !== undefined && edge.from !== filter.from) continue;
282
+ if (filter?.to !== undefined && edge.to !== filter.to) continue;
283
+ if (filter?.kind !== undefined && edge.kind !== filter.kind) continue;
284
+ if (
285
+ filter?.min_weight !== undefined &&
286
+ (edge.weight === undefined || edge.weight < filter.min_weight)
287
+ )
288
+ continue;
289
+ results.push(edge);
290
+ }
291
+ return results;
292
+ }
293
+
294
+ export function getItemById(
295
+ state: GraphState,
296
+ id: string,
297
+ ): MemoryItem | undefined {
298
+ return state.items.get(id);
299
+ }
300
+
301
+ export function getEdgeById(
302
+ state: GraphState,
303
+ edgeId: string,
304
+ ): Edge | undefined {
305
+ return state.edges.get(edgeId);
306
+ }
307
+
308
+ export function getRelatedItems(
309
+ state: GraphState,
310
+ itemId: string,
311
+ direction: "from" | "to" | "both" = "both",
312
+ ): MemoryItem[] {
313
+ const relatedIds = new Set<string>();
314
+
315
+ for (const edge of state.edges.values()) {
316
+ if (!edge.active) continue;
317
+ if (
318
+ (direction === "from" || direction === "both") &&
319
+ edge.from === itemId
320
+ ) {
321
+ relatedIds.add(edge.to);
322
+ }
323
+ if ((direction === "to" || direction === "both") && edge.to === itemId) {
324
+ relatedIds.add(edge.from);
325
+ }
326
+ }
327
+
328
+ relatedIds.delete(itemId);
329
+
330
+ const results: MemoryItem[] = [];
331
+ for (const id of relatedIds) {
332
+ const item = state.items.get(id);
333
+ if (item) results.push(item);
334
+ }
335
+ return results;
336
+ }
337
+
338
+ export function getParents(state: GraphState, itemId: string): MemoryItem[] {
339
+ const item = state.items.get(itemId);
340
+ if (!item?.parents) return [];
341
+ const results: MemoryItem[] = [];
342
+ for (const pid of item.parents) {
343
+ const parent = state.items.get(pid);
344
+ if (parent) results.push(parent);
345
+ }
346
+ return results;
347
+ }
348
+
349
+ export function getChildren(state: GraphState, itemId: string): MemoryItem[] {
350
+ const results: MemoryItem[] = [];
351
+ for (const item of state.items.values()) {
352
+ if (item.parents && item.parents.includes(itemId)) {
353
+ results.push(item);
354
+ }
355
+ }
356
+ return results;
357
+ }
package/src/reducer.ts ADDED
@@ -0,0 +1,177 @@
1
+ import type {
2
+ GraphState,
3
+ MemoryCommand,
4
+ MemoryLifecycleEvent,
5
+ MemoryItem,
6
+ Edge,
7
+ } from "./types.js";
8
+ import {
9
+ MemoryNotFoundError,
10
+ EdgeNotFoundError,
11
+ DuplicateMemoryError,
12
+ DuplicateEdgeError,
13
+ } from "./errors.js";
14
+
15
+ function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
16
+ const result: Record<string, unknown> = {};
17
+ for (const [key, value] of Object.entries(obj)) {
18
+ if (value !== undefined) result[key] = value;
19
+ }
20
+ return result as Partial<T>;
21
+ }
22
+
23
+ export function mergeItem(
24
+ existing: MemoryItem,
25
+ partial: Partial<MemoryItem>,
26
+ ): MemoryItem {
27
+ const {
28
+ content: partialContent,
29
+ meta: partialMeta,
30
+ id: _id,
31
+ ...rest
32
+ } = partial;
33
+ return {
34
+ ...existing,
35
+ ...stripUndefined(rest),
36
+ content:
37
+ partialContent !== undefined
38
+ ? { ...existing.content, ...stripUndefined(partialContent) }
39
+ : existing.content,
40
+ meta:
41
+ partialMeta !== undefined
42
+ ? { ...existing.meta, ...stripUndefined(partialMeta) }
43
+ : existing.meta,
44
+ };
45
+ }
46
+
47
+ function mergeEdge(existing: Edge, partial: Partial<Edge>): Edge {
48
+ const { edge_id: _eid, from: _from, to: _to, ...rest } = partial;
49
+ return { ...existing, ...stripUndefined(rest) };
50
+ }
51
+
52
+ export function applyCommand(
53
+ state: GraphState,
54
+ cmd: MemoryCommand,
55
+ ): { state: GraphState; events: MemoryLifecycleEvent[] } {
56
+ switch (cmd.type) {
57
+ case "memory.create": {
58
+ if (state.items.has(cmd.item.id)) {
59
+ throw new DuplicateMemoryError(cmd.item.id);
60
+ }
61
+ const items = new Map(state.items);
62
+ items.set(cmd.item.id, cmd.item);
63
+ return {
64
+ state: { items, edges: state.edges },
65
+ events: [
66
+ {
67
+ namespace: "memory",
68
+ type: "memory.created",
69
+ item: cmd.item,
70
+ cause_type: cmd.type,
71
+ },
72
+ ],
73
+ };
74
+ }
75
+
76
+ case "memory.update": {
77
+ const existing = state.items.get(cmd.item_id);
78
+ if (!existing) {
79
+ throw new MemoryNotFoundError(cmd.item_id);
80
+ }
81
+ const merged = mergeItem(existing, cmd.partial);
82
+ const items = new Map(state.items);
83
+ items.set(cmd.item_id, merged);
84
+ return {
85
+ state: { items, edges: state.edges },
86
+ events: [
87
+ {
88
+ namespace: "memory",
89
+ type: "memory.updated",
90
+ item: merged,
91
+ cause_type: cmd.type,
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ case "memory.retract": {
98
+ const existing = state.items.get(cmd.item_id);
99
+ if (!existing) {
100
+ throw new MemoryNotFoundError(cmd.item_id);
101
+ }
102
+ const items = new Map(state.items);
103
+ items.delete(cmd.item_id);
104
+ return {
105
+ state: { items, edges: state.edges },
106
+ events: [
107
+ {
108
+ namespace: "memory",
109
+ type: "memory.retracted",
110
+ item: existing,
111
+ cause_type: cmd.type,
112
+ },
113
+ ],
114
+ };
115
+ }
116
+
117
+ case "edge.create": {
118
+ if (state.edges.has(cmd.edge.edge_id)) {
119
+ throw new DuplicateEdgeError(cmd.edge.edge_id);
120
+ }
121
+ const edges = new Map(state.edges);
122
+ edges.set(cmd.edge.edge_id, cmd.edge);
123
+ return {
124
+ state: { items: state.items, edges },
125
+ events: [
126
+ {
127
+ namespace: "memory",
128
+ type: "edge.created",
129
+ edge: cmd.edge,
130
+ cause_type: cmd.type,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+
136
+ case "edge.update": {
137
+ const existing = state.edges.get(cmd.edge_id);
138
+ if (!existing) {
139
+ throw new EdgeNotFoundError(cmd.edge_id);
140
+ }
141
+ const merged = mergeEdge(existing, cmd.partial);
142
+ const edges = new Map(state.edges);
143
+ edges.set(cmd.edge_id, merged);
144
+ return {
145
+ state: { items: state.items, edges },
146
+ events: [
147
+ {
148
+ namespace: "memory",
149
+ type: "edge.updated",
150
+ edge: merged,
151
+ cause_type: cmd.type,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+
157
+ case "edge.retract": {
158
+ const existing = state.edges.get(cmd.edge_id);
159
+ if (!existing) {
160
+ throw new EdgeNotFoundError(cmd.edge_id);
161
+ }
162
+ const edges = new Map(state.edges);
163
+ edges.delete(cmd.edge_id);
164
+ return {
165
+ state: { items: state.items, edges },
166
+ events: [
167
+ {
168
+ namespace: "memory",
169
+ type: "edge.retracted",
170
+ edge: existing,
171
+ cause_type: cmd.type,
172
+ },
173
+ ],
174
+ };
175
+ }
176
+ }
177
+ }
package/src/replay.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type {
2
+ GraphState,
3
+ MemoryCommand,
4
+ MemoryLifecycleEvent,
5
+ EventEnvelope,
6
+ } from "./types.js";
7
+ import { createGraphState } from "./graph.js";
8
+ import { applyCommand } from "./reducer.js";
9
+
10
+ export function replayCommands(commands: MemoryCommand[]): {
11
+ state: GraphState;
12
+ events: MemoryLifecycleEvent[];
13
+ } {
14
+ let state = createGraphState();
15
+ const allEvents: MemoryLifecycleEvent[] = [];
16
+
17
+ for (const cmd of commands) {
18
+ const result = applyCommand(state, cmd);
19
+ state = result.state;
20
+ allEvents.push(...result.events);
21
+ }
22
+
23
+ return { state, events: allEvents };
24
+ }
25
+
26
+ export function replayFromEnvelopes(
27
+ envelopes: EventEnvelope<MemoryCommand>[],
28
+ ): { state: GraphState; events: MemoryLifecycleEvent[] } {
29
+ const sorted = [...envelopes].sort((a, b) => a.ts.localeCompare(b.ts));
30
+ const commands = sorted.map((env) => env.payload);
31
+ return replayCommands(commands);
32
+ }