@astrocyteai/local 0.1.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/.pnpm-config.json +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +276 -0
- package/dist/context-tree.d.ts +35 -0
- package/dist/context-tree.js +228 -0
- package/dist/curated-retain.d.ts +41 -0
- package/dist/curated-retain.js +118 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/mcp-server.d.ts +18 -0
- package/dist/mcp-server.js +212 -0
- package/dist/search.d.ts +45 -0
- package/dist/search.js +174 -0
- package/dist/tiered-retrieval.d.ts +53 -0
- package/dist/tiered-retrieval.js +187 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +5 -0
- package/package.json +55 -0
- package/src/cli.ts +304 -0
- package/src/context-tree.ts +243 -0
- package/src/curated-retain.ts +160 -0
- package/src/index.ts +21 -0
- package/src/mcp-server.ts +282 -0
- package/src/search.ts +249 -0
- package/src/tiered-retrieval.ts +229 -0
- package/src/types.ts +68 -0
- package/tests/context-tree.test.ts +209 -0
- package/tests/curated-retain.test.ts +142 -0
- package/tests/mcp-server.test.ts +163 -0
- package/tests/search.test.ts +188 -0
- package/tests/tiered-retrieval.test.ts +270 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { ContextTree } from "../src/context-tree.js";
|
|
6
|
+
import { SearchEngine } from "../src/search.js";
|
|
7
|
+
import {
|
|
8
|
+
LocalRecallCache,
|
|
9
|
+
LocalTieredRetriever,
|
|
10
|
+
} from "../src/tiered-retrieval.js";
|
|
11
|
+
import type { SearchHit } from "../src/types.js";
|
|
12
|
+
|
|
13
|
+
// ── LocalRecallCache ���─
|
|
14
|
+
|
|
15
|
+
describe("LocalRecallCache", () => {
|
|
16
|
+
it("returns null on cache miss", () => {
|
|
17
|
+
const cache = new LocalRecallCache();
|
|
18
|
+
expect(cache.get("test query", "bank-1")).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns hits on cache hit", () => {
|
|
22
|
+
const cache = new LocalRecallCache();
|
|
23
|
+
const hits: SearchHit[] = [
|
|
24
|
+
{
|
|
25
|
+
id: "m1",
|
|
26
|
+
text: "cached",
|
|
27
|
+
score: 0.9,
|
|
28
|
+
bank_id: "b1",
|
|
29
|
+
domain: "test",
|
|
30
|
+
file_path: "test.md",
|
|
31
|
+
tags: [],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
cache.put("test query", "bank-1", hits);
|
|
35
|
+
|
|
36
|
+
const result = cache.get("test query", "bank-1");
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
expect(result![0].text).toBe("cached");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("is case insensitive", () => {
|
|
42
|
+
const cache = new LocalRecallCache();
|
|
43
|
+
const hits: SearchHit[] = [
|
|
44
|
+
{
|
|
45
|
+
id: "m1",
|
|
46
|
+
text: "cached",
|
|
47
|
+
score: 0.9,
|
|
48
|
+
bank_id: "b1",
|
|
49
|
+
domain: "test",
|
|
50
|
+
file_path: "test.md",
|
|
51
|
+
tags: [],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
cache.put("Dark Mode", "bank-1", hits);
|
|
55
|
+
|
|
56
|
+
expect(cache.get("dark mode", "bank-1")).not.toBeNull();
|
|
57
|
+
expect(cache.get("DARK MODE", "bank-1")).not.toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("isolates by bank", () => {
|
|
61
|
+
const cache = new LocalRecallCache();
|
|
62
|
+
const hits: SearchHit[] = [
|
|
63
|
+
{
|
|
64
|
+
id: "m1",
|
|
65
|
+
text: "cached",
|
|
66
|
+
score: 0.9,
|
|
67
|
+
bank_id: "b1",
|
|
68
|
+
domain: "test",
|
|
69
|
+
file_path: "test.md",
|
|
70
|
+
tags: [],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
cache.put("query", "bank-1", hits);
|
|
74
|
+
|
|
75
|
+
expect(cache.get("query", "bank-2")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("invalidates by bank", () => {
|
|
79
|
+
const cache = new LocalRecallCache();
|
|
80
|
+
const hits: SearchHit[] = [
|
|
81
|
+
{
|
|
82
|
+
id: "m1",
|
|
83
|
+
text: "cached",
|
|
84
|
+
score: 0.9,
|
|
85
|
+
bank_id: "b1",
|
|
86
|
+
domain: "test",
|
|
87
|
+
file_path: "test.md",
|
|
88
|
+
tags: [],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
cache.put("query", "bank-1", hits);
|
|
92
|
+
expect(cache.size()).toBe(1);
|
|
93
|
+
|
|
94
|
+
cache.invalidateBank("bank-1");
|
|
95
|
+
expect(cache.size()).toBe(0);
|
|
96
|
+
expect(cache.get("query", "bank-1")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("expires entries by TTL", async () => {
|
|
100
|
+
const cache = new LocalRecallCache(128, 10); // 10ms TTL
|
|
101
|
+
const hits: SearchHit[] = [
|
|
102
|
+
{
|
|
103
|
+
id: "m1",
|
|
104
|
+
text: "cached",
|
|
105
|
+
score: 0.9,
|
|
106
|
+
bank_id: "b1",
|
|
107
|
+
domain: "test",
|
|
108
|
+
file_path: "test.md",
|
|
109
|
+
tags: [],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
cache.put("query", "bank-1", hits);
|
|
113
|
+
|
|
114
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
115
|
+
expect(cache.get("query", "bank-1")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("evicts oldest on LRU overflow", () => {
|
|
119
|
+
const cache = new LocalRecallCache(2, 120_000);
|
|
120
|
+
for (let i = 0; i < 3; i++) {
|
|
121
|
+
const hits: SearchHit[] = [
|
|
122
|
+
{
|
|
123
|
+
id: `m${i}`,
|
|
124
|
+
text: `hit${i}`,
|
|
125
|
+
score: 0.9,
|
|
126
|
+
bank_id: "b1",
|
|
127
|
+
domain: "test",
|
|
128
|
+
file_path: "t.md",
|
|
129
|
+
tags: [],
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
cache.put(`query-${i}`, "bank-1", hits);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(cache.size()).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── LocalTieredRetriever ──
|
|
140
|
+
|
|
141
|
+
describe("LocalTieredRetriever", () => {
|
|
142
|
+
let tmpDir: string;
|
|
143
|
+
let tree: ContextTree;
|
|
144
|
+
let search: SearchEngine;
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
tmpDir = fs.mkdtempSync(
|
|
148
|
+
path.join(os.tmpdir(), "astrocyte-tiered-test-")
|
|
149
|
+
);
|
|
150
|
+
tree = new ContextTree(tmpDir);
|
|
151
|
+
search = new SearchEngine(path.join(tmpDir, "_search.db"));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
search.close();
|
|
156
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("retrieves via tier 1 FTS5", () => {
|
|
160
|
+
tree.store({
|
|
161
|
+
content: "Dark mode is preferred by Calvin",
|
|
162
|
+
bank_id: "test",
|
|
163
|
+
});
|
|
164
|
+
search.buildIndex(tree);
|
|
165
|
+
|
|
166
|
+
const tiered = new LocalTieredRetriever(search, null, null, 2, 0.3, 1);
|
|
167
|
+
const [hits, tier] = tiered.retrieve("dark mode", "test");
|
|
168
|
+
|
|
169
|
+
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
170
|
+
expect(tier).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns tier 0 on cache hit", () => {
|
|
174
|
+
tree.store({
|
|
175
|
+
content: "Cached content here",
|
|
176
|
+
bank_id: "test",
|
|
177
|
+
});
|
|
178
|
+
search.buildIndex(tree);
|
|
179
|
+
|
|
180
|
+
const cache = new LocalRecallCache();
|
|
181
|
+
const tiered = new LocalTieredRetriever(search, cache, null, 2, 0.3, 1);
|
|
182
|
+
|
|
183
|
+
// First query — tier 1
|
|
184
|
+
const [, tier1] = tiered.retrieve("Cached content", "test");
|
|
185
|
+
expect(tier1).toBe(1);
|
|
186
|
+
|
|
187
|
+
// Second query — tier 0 (cache)
|
|
188
|
+
const [hits2, tier2] = tiered.retrieve("Cached content", "test");
|
|
189
|
+
expect(tier2).toBe(0);
|
|
190
|
+
expect(hits2.length).toBeGreaterThanOrEqual(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("falls back to tier 1 after cache invalidation", () => {
|
|
194
|
+
tree.store({
|
|
195
|
+
content: "Original content",
|
|
196
|
+
bank_id: "test",
|
|
197
|
+
});
|
|
198
|
+
search.buildIndex(tree);
|
|
199
|
+
|
|
200
|
+
const cache = new LocalRecallCache();
|
|
201
|
+
const tiered = new LocalTieredRetriever(search, cache, null, 2, 0.3, 1);
|
|
202
|
+
|
|
203
|
+
// Populate cache
|
|
204
|
+
tiered.retrieve("Original", "test");
|
|
205
|
+
|
|
206
|
+
// Invalidate
|
|
207
|
+
cache.invalidateBank("test");
|
|
208
|
+
|
|
209
|
+
// Should go to tier 1 again
|
|
210
|
+
const [, tier] = tiered.retrieve("Original", "test");
|
|
211
|
+
expect(tier).toBe(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("respects max_tier=0", () => {
|
|
215
|
+
const tiered = new LocalTieredRetriever(search, null, null, 2, 0.3, 0);
|
|
216
|
+
const [hits, tier] = tiered.retrieve("anything", "test");
|
|
217
|
+
expect(hits).toEqual([]);
|
|
218
|
+
expect(tier).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("retrieves async via tier 1", async () => {
|
|
222
|
+
tree.store({
|
|
223
|
+
content: "Async searchable content",
|
|
224
|
+
bank_id: "test",
|
|
225
|
+
});
|
|
226
|
+
search.buildIndex(tree);
|
|
227
|
+
|
|
228
|
+
const tiered = new LocalTieredRetriever(search, null, null, 2, 0.3, 1);
|
|
229
|
+
const [hits, tier] = await tiered.aretrieve("Async searchable", "test");
|
|
230
|
+
|
|
231
|
+
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
232
|
+
expect(tier).toBe(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("returns async tier 0 on cache hit", async () => {
|
|
236
|
+
tree.store({
|
|
237
|
+
content: "Async cached content",
|
|
238
|
+
bank_id: "test",
|
|
239
|
+
});
|
|
240
|
+
search.buildIndex(tree);
|
|
241
|
+
|
|
242
|
+
const cache = new LocalRecallCache();
|
|
243
|
+
const tiered = new LocalTieredRetriever(search, cache, null, 2, 0.3, 1);
|
|
244
|
+
|
|
245
|
+
await tiered.aretrieve("Async cached", "test");
|
|
246
|
+
const [, tier] = await tiered.aretrieve("Async cached", "test");
|
|
247
|
+
expect(tier).toBe(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("mergeHits", () => {
|
|
251
|
+
it("deduplicates by ID keeping highest score", () => {
|
|
252
|
+
const hitsA: SearchHit[] = [
|
|
253
|
+
{ id: "1", text: "a", score: 0.5, bank_id: "b", domain: "d", file_path: "f", tags: [] },
|
|
254
|
+
{ id: "2", text: "b", score: 0.8, bank_id: "b", domain: "d", file_path: "f", tags: [] },
|
|
255
|
+
];
|
|
256
|
+
const hitsB: SearchHit[] = [
|
|
257
|
+
{ id: "1", text: "a", score: 0.9, bank_id: "b", domain: "d", file_path: "f", tags: [] },
|
|
258
|
+
{ id: "3", text: "c", score: 0.7, bank_id: "b", domain: "d", file_path: "f", tags: [] },
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const merged = LocalTieredRetriever.mergeHits(hitsA, hitsB);
|
|
262
|
+
expect(merged.length).toBe(3);
|
|
263
|
+
// Sorted by score descending
|
|
264
|
+
expect(merged[0].id).toBe("1");
|
|
265
|
+
expect(merged[0].score).toBe(0.9); // Kept higher score
|
|
266
|
+
expect(merged[1].id).toBe("2");
|
|
267
|
+
expect(merged[2].id).toBe("3");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
17
|
+
}
|