@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,306 @@
1
+ import type {
2
+ GraphState,
3
+ MemoryItem,
4
+ MemoryFilter,
5
+ ScoreWeights,
6
+ ScoredItem,
7
+ } from "./types.js";
8
+ import { getEdges, getScoredItems } from "./query.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // 1. Support tree — provenance walk
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface SupportNode {
15
+ item: MemoryItem;
16
+ parents: SupportNode[];
17
+ }
18
+
19
+ /**
20
+ * Build the full provenance tree for an item.
21
+ * Recursively walks `parents`, deduplicating on cycles.
22
+ */
23
+ export function getSupportTree(
24
+ state: GraphState,
25
+ itemId: string,
26
+ ): SupportNode | null {
27
+ const item = state.items.get(itemId);
28
+ if (!item) return null;
29
+
30
+ const visited = new Set<string>();
31
+
32
+ function walk(id: string): SupportNode | null {
33
+ const current = state.items.get(id);
34
+ if (!current) return null;
35
+ if (visited.has(id)) return { item: current, parents: [] };
36
+ visited.add(id);
37
+
38
+ const parentNodes: SupportNode[] = [];
39
+ if (current.parents) {
40
+ for (const pid of current.parents) {
41
+ const node = walk(pid);
42
+ if (node) parentNodes.push(node);
43
+ }
44
+ }
45
+ return { item: current, parents: parentNodes };
46
+ }
47
+
48
+ return walk(itemId);
49
+ }
50
+
51
+ /**
52
+ * Flatten a support tree into the minimal set of items that justify a claim.
53
+ * Returns all unique items in the provenance chain (including the root).
54
+ */
55
+ export function getSupportSet(state: GraphState, itemId: string): MemoryItem[] {
56
+ const item = state.items.get(itemId);
57
+ if (!item) return [];
58
+
59
+ const visited = new Set<string>();
60
+ const result: MemoryItem[] = [];
61
+
62
+ function walk(id: string): void {
63
+ if (visited.has(id)) return;
64
+ visited.add(id);
65
+ const current = state.items.get(id);
66
+ if (!current) return;
67
+ result.push(current);
68
+ if (current.parents) {
69
+ for (const pid of current.parents) {
70
+ walk(pid);
71
+ }
72
+ }
73
+ }
74
+
75
+ walk(itemId);
76
+ return result;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // 2. Contradiction-aware packing
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Get the set of item ids that have been superseded (losers of resolved contradictions).
85
+ */
86
+ function getSupersededIds(state: GraphState): Set<string> {
87
+ const superseded = new Set<string>();
88
+ for (const edge of state.edges.values()) {
89
+ if (edge.kind === "SUPERSEDES" && edge.active) {
90
+ superseded.add(edge.to);
91
+ }
92
+ }
93
+ return superseded;
94
+ }
95
+
96
+ /**
97
+ * Given a set of scored items, remove items that are the losing side of
98
+ * a resolved contradiction (SUPERSEDES), and for unresolved contradictions
99
+ * (CONTRADICTS), keep only the higher-scoring item.
100
+ */
101
+ export function filterContradictions(
102
+ state: GraphState,
103
+ scored: ScoredItem[],
104
+ ): ScoredItem[] {
105
+ const superseded = getSupersededIds(state);
106
+
107
+ // remove superseded items
108
+ let filtered = scored.filter((s) => !superseded.has(s.item.id));
109
+
110
+ // for unresolved contradictions, keep the higher-scoring side
111
+ const contradictEdges = getEdges(state, {
112
+ kind: "CONTRADICTS",
113
+ active_only: true,
114
+ });
115
+ if (contradictEdges.length > 0) {
116
+ const scoreMap = new Map<string, number>();
117
+ for (const entry of filtered) {
118
+ scoreMap.set(entry.item.id, entry.score);
119
+ }
120
+
121
+ const excluded = new Set<string>();
122
+ for (const edge of contradictEdges) {
123
+ if (excluded.has(edge.from) || excluded.has(edge.to)) continue;
124
+
125
+ const scoreA = scoreMap.get(edge.from) ?? -1;
126
+ const scoreB = scoreMap.get(edge.to) ?? -1;
127
+
128
+ if (scoreA >= 0 && scoreB >= 0) {
129
+ if (scoreA !== scoreB) {
130
+ excluded.add(scoreA > scoreB ? edge.to : edge.from);
131
+ } else {
132
+ // deterministic tiebreak: exclude the lexicographically larger id
133
+ excluded.add(
134
+ edge.from < edge.to ? edge.to : edge.from,
135
+ );
136
+ }
137
+ }
138
+ }
139
+
140
+ if (excluded.size > 0) {
141
+ filtered = filtered.filter((s) => !excluded.has(s.item.id));
142
+ }
143
+ }
144
+
145
+ return filtered;
146
+ }
147
+
148
+ /**
149
+ * Surface contradictions: keep both sides, annotate each with what contradicts it.
150
+ * Superseded items are still removed. Unresolved contradictions are preserved
151
+ * and flagged via `contradicted_by`.
152
+ */
153
+ export function surfaceContradictions(
154
+ state: GraphState,
155
+ scored: ScoredItem[],
156
+ ): ScoredItem[] {
157
+ const superseded = getSupersededIds(state);
158
+ // clone each entry to avoid mutating the input array
159
+ let result = scored
160
+ .filter((s) => !superseded.has(s.item.id))
161
+ .map((s) => ({ ...s }));
162
+
163
+ const contradictEdges = getEdges(state, {
164
+ kind: "CONTRADICTS",
165
+ active_only: true,
166
+ });
167
+ if (contradictEdges.length === 0) return result;
168
+
169
+ const itemMap = new Map<string, ScoredItem>();
170
+ for (const entry of result) {
171
+ itemMap.set(entry.item.id, entry);
172
+ }
173
+
174
+ for (const edge of contradictEdges) {
175
+ const a = itemMap.get(edge.from);
176
+ const b = itemMap.get(edge.to);
177
+ if (a && b) {
178
+ a.contradicted_by = a.contradicted_by ?? [];
179
+ a.contradicted_by.push(b.item);
180
+ b.contradicted_by = b.contradicted_by ?? [];
181
+ b.contradicted_by.push(a.item);
182
+ }
183
+ }
184
+
185
+ return result;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // 3. Diversity scoring
190
+ // ---------------------------------------------------------------------------
191
+
192
+ export interface DiversityOptions {
193
+ /** Penalty per duplicate author (0..1). Default 0. */
194
+ author_penalty?: number;
195
+ /** Penalty per shared parent (0..1). Default 0. */
196
+ parent_penalty?: number;
197
+ /** Penalty per duplicate source_kind (0..1). Default 0. */
198
+ source_penalty?: number;
199
+ }
200
+
201
+ /**
202
+ * Re-rank scored items with diversity penalties.
203
+ * Items are processed in score order. Each subsequent item from the same
204
+ * author / parent / source_kind gets its score reduced by the penalty amount.
205
+ */
206
+ export function applyDiversity(
207
+ scored: ScoredItem[],
208
+ options: DiversityOptions,
209
+ ): ScoredItem[] {
210
+ const authorCounts = options.author_penalty
211
+ ? new Map<string, number>()
212
+ : null;
213
+ const parentCounts = options.parent_penalty
214
+ ? new Map<string, number>()
215
+ : null;
216
+ const sourceCounts = options.source_penalty
217
+ ? new Map<string, number>()
218
+ : null;
219
+
220
+ const diversified = scored.map((entry) => {
221
+ let penalty = 0;
222
+
223
+ if (authorCounts) {
224
+ const count = authorCounts.get(entry.item.author) ?? 0;
225
+ penalty += count * options.author_penalty!;
226
+ authorCounts.set(entry.item.author, count + 1);
227
+ }
228
+
229
+ if (parentCounts && entry.item.parents) {
230
+ for (const pid of entry.item.parents) {
231
+ const count = parentCounts.get(pid) ?? 0;
232
+ penalty += count * options.parent_penalty!;
233
+ parentCounts.set(pid, count + 1);
234
+ }
235
+ }
236
+
237
+ if (sourceCounts) {
238
+ const count = sourceCounts.get(entry.item.source_kind) ?? 0;
239
+ penalty += count * options.source_penalty!;
240
+ sourceCounts.set(entry.item.source_kind, count + 1);
241
+ }
242
+
243
+ return {
244
+ ...entry,
245
+ score: Math.max(0, entry.score - penalty),
246
+ };
247
+ });
248
+
249
+ diversified.sort((a, b) => b.score - a.score);
250
+ return diversified;
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // 4. Combined smart retrieval
255
+ // ---------------------------------------------------------------------------
256
+
257
+ export interface SmartRetrievalOptions {
258
+ budget: number;
259
+ costFn: (item: MemoryItem) => number;
260
+ weights: ScoreWeights;
261
+ filter?: MemoryFilter;
262
+ contradictions?: "filter" | "surface"; // "filter" = collapse, "surface" = keep both + flag
263
+ diversity?: DiversityOptions;
264
+ }
265
+
266
+ /**
267
+ * Smart retrieval: score → contradiction filter → diversity → budget pack.
268
+ *
269
+ * Pipeline:
270
+ * 1. Score all items matching filter
271
+ * 2. Optionally remove contradicted/superseded items
272
+ * 3. Optionally apply diversity penalties and re-rank
273
+ * 4. Greedily pack within budget
274
+ */
275
+ export function smartRetrieve(
276
+ state: GraphState,
277
+ options: SmartRetrievalOptions,
278
+ ): ScoredItem[] {
279
+ let scored = getScoredItems(state, options.weights, {
280
+ pre: options.filter,
281
+ });
282
+
283
+ if (options.contradictions === "filter") {
284
+ scored = filterContradictions(state, scored);
285
+ } else if (options.contradictions === "surface") {
286
+ scored = surfaceContradictions(state, scored);
287
+ }
288
+
289
+ if (options.diversity) {
290
+ scored = applyDiversity(scored, options.diversity);
291
+ }
292
+
293
+ const results: ScoredItem[] = [];
294
+ let remaining = options.budget;
295
+
296
+ for (const entry of scored) {
297
+ const cost = options.costFn(entry.item);
298
+ if (cost <= remaining) {
299
+ results.push(entry);
300
+ remaining -= cost;
301
+ }
302
+ if (remaining <= 0) break;
303
+ }
304
+
305
+ return results;
306
+ }
@@ -0,0 +1,34 @@
1
+ import type { GraphState, MemoryItem, Edge } from "./types.js";
2
+
3
+ export interface SerializedGraphState {
4
+ items: [string, MemoryItem][];
5
+ edges: [string, Edge][];
6
+ }
7
+
8
+ export function toJSON(state: GraphState): SerializedGraphState {
9
+ return {
10
+ items: [...state.items.entries()],
11
+ edges: [...state.edges.entries()],
12
+ };
13
+ }
14
+
15
+ export function fromJSON(data: SerializedGraphState): GraphState {
16
+ return {
17
+ items: new Map(data.items),
18
+ edges: new Map(data.edges),
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Stringify a GraphState to a JSON string.
24
+ */
25
+ export function stringify(state: GraphState, pretty = false): string {
26
+ return JSON.stringify(toJSON(state), null, pretty ? 2 : undefined);
27
+ }
28
+
29
+ /**
30
+ * Parse a JSON string back into a GraphState.
31
+ */
32
+ export function parse(json: string): GraphState {
33
+ return fromJSON(JSON.parse(json));
34
+ }
package/src/stats.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { GraphState } from "./types.js";
2
+
3
+ export interface GraphStats {
4
+ items: {
5
+ total: number;
6
+ by_kind: Record<string, number>;
7
+ by_source_kind: Record<string, number>;
8
+ by_author: Record<string, number>;
9
+ by_scope: Record<string, number>;
10
+ with_parents: number;
11
+ root: number;
12
+ };
13
+ edges: {
14
+ total: number;
15
+ active: number;
16
+ by_kind: Record<string, number>;
17
+ };
18
+ }
19
+
20
+ function countBy<T>(
21
+ values: Iterable<T>,
22
+ keyFn: (v: T) => string,
23
+ ): Record<string, number> {
24
+ const counts: Record<string, number> = {};
25
+ for (const v of values) {
26
+ const key = keyFn(v);
27
+ counts[key] = (counts[key] ?? 0) + 1;
28
+ }
29
+ return counts;
30
+ }
31
+
32
+ export function getStats(state: GraphState): GraphStats {
33
+ const items = [...state.items.values()];
34
+ const edges = [...state.edges.values()];
35
+
36
+ let withParents = 0;
37
+ let root = 0;
38
+ for (const item of items) {
39
+ if (item.parents && item.parents.length > 0) {
40
+ withParents++;
41
+ } else {
42
+ root++;
43
+ }
44
+ }
45
+
46
+ return {
47
+ items: {
48
+ total: items.length,
49
+ by_kind: countBy(items, (i) => i.kind),
50
+ by_source_kind: countBy(items, (i) => i.source_kind),
51
+ by_author: countBy(items, (i) => i.author),
52
+ by_scope: countBy(items, (i) => i.scope),
53
+ with_parents: withParents,
54
+ root,
55
+ },
56
+ edges: {
57
+ total: edges.length,
58
+ active: edges.filter((e) => e.active).length,
59
+ by_kind: countBy(edges, (e) => e.kind),
60
+ },
61
+ };
62
+ }