@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.
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +35 -0
- package/API.md +1078 -0
- package/LICENSE +190 -0
- package/README.md +574 -0
- package/package.json +30 -0
- package/src/bulk.ts +128 -0
- package/src/envelope.ts +52 -0
- package/src/errors.ts +27 -0
- package/src/graph.ts +15 -0
- package/src/helpers.ts +51 -0
- package/src/index.ts +142 -0
- package/src/integrity.ts +378 -0
- package/src/intent.ts +311 -0
- package/src/query.ts +357 -0
- package/src/reducer.ts +177 -0
- package/src/replay.ts +32 -0
- package/src/retrieval.ts +306 -0
- package/src/serialization.ts +34 -0
- package/src/stats.ts +62 -0
- package/src/task.ts +373 -0
- package/src/transplant.ts +488 -0
- package/src/types.ts +248 -0
- package/tests/bugfix-and-coverage.test.ts +958 -0
- package/tests/bugfix-holes.test.ts +856 -0
- package/tests/bulk.test.ts +256 -0
- package/tests/edge-cases-v2.test.ts +355 -0
- package/tests/edge-cases.test.ts +661 -0
- package/tests/envelope.test.ts +92 -0
- package/tests/graph.test.ts +41 -0
- package/tests/helpers.test.ts +120 -0
- package/tests/integrity.test.ts +371 -0
- package/tests/intent.test.ts +276 -0
- package/tests/query-advanced.test.ts +252 -0
- package/tests/query.test.ts +623 -0
- package/tests/reducer.test.ts +342 -0
- package/tests/replay.test.ts +145 -0
- package/tests/retrieval.test.ts +691 -0
- package/tests/serialization.test.ts +118 -0
- package/tests/setup.test.ts +7 -0
- package/tests/stats.test.ts +163 -0
- package/tests/task.test.ts +322 -0
- package/tests/transplant.test.ts +385 -0
- package/tests/types.test.ts +231 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
package/src/retrieval.ts
ADDED
|
@@ -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
|
+
}
|