hippocamp 0.1.0 → 0.2.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.
@@ -0,0 +1,223 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs/promises");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const test = require("node:test");
6
+
7
+ const dream = require("../scripts/hippocamp-dream.cjs");
8
+ const memory = require("../scripts/hippocamp-memory.cjs");
9
+
10
+ function modelResponse(output) {
11
+ return {
12
+ ok: true,
13
+ status: 200,
14
+ async text() {
15
+ return JSON.stringify({ output_text: JSON.stringify(output) });
16
+ },
17
+ };
18
+ }
19
+
20
+ test("Dream compacts an oversized result again without cropping", async (t) => {
21
+ const originalFetch = global.fetch;
22
+ const outputs = [
23
+ {
24
+ current_state_md: `# Current State\n\n- ${"a".repeat(140)}`,
25
+ open_threads_md: "# Open Threads\n\n- Keep the active task.",
26
+ },
27
+ {
28
+ current_state_md: "# Current State\n\n- Active.",
29
+ open_threads_md: "# Open Threads\n\n- Continue.",
30
+ },
31
+ ];
32
+ const requests = [];
33
+
34
+ global.fetch = async (_url, options) => {
35
+ requests.push(JSON.parse(options.body));
36
+ return modelResponse(outputs.shift());
37
+ };
38
+ t.after(() => {
39
+ global.fetch = originalFetch;
40
+ });
41
+
42
+ const result = await dream.compactDreamOutput({
43
+ apiKey: "test-key",
44
+ baseUrl: "https://manifest.test",
45
+ beforeBytes: 500,
46
+ model: "test-model",
47
+ report: {
48
+ slug: "test-project",
49
+ wakeUpChars: 500,
50
+ files: {
51
+ "current_state.md": { content: `# Current State\n\n- ${"z".repeat(300)}` },
52
+ "open_threads.md": { content: "# Open Threads\n\n- Keep the active task." },
53
+ },
54
+ },
55
+ targetChars: 100,
56
+ threadEvidence: {
57
+ evidenceBudgetChars: 100,
58
+ evidenceChars: 0,
59
+ openThreadCount: 1,
60
+ items: [],
61
+ },
62
+ });
63
+
64
+ assert.equal(result.passes, 2);
65
+ assert.equal(requests.length, 2);
66
+ assert.ok(result.afterBytes <= 102);
67
+ assert.match(requests[1].input, /previous compaction pass was still too large/i);
68
+ assert.equal(result.output.currentState, "# Current State\n\n- Active.");
69
+ assert.equal(result.output.openThreads, "# Open Threads\n\n- Continue.");
70
+ });
71
+
72
+ test("search uses indexed events and returns coherent evidence", async (t) => {
73
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
74
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-search-"));
75
+ const lagoonRoot = path.join(tempRoot, "lagoon");
76
+ const projectRoot = path.join(tempRoot, "search-project");
77
+
78
+ process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
79
+ await fs.mkdir(projectRoot, { recursive: true });
80
+ t.after(async () => {
81
+ if (originalRoot === undefined) {
82
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
83
+ } else {
84
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
85
+ }
86
+ await fs.rm(tempRoot, { recursive: true, force: true });
87
+ });
88
+
89
+ await memory.appendEvent({
90
+ content: "Decision:\nUse indexed search only.",
91
+ cues: ["strict-index"],
92
+ date: "2026-09-21",
93
+ projectRoot,
94
+ scope: "project",
95
+ sync: false,
96
+ timestamp: "2026-09-21T10:00:00.000Z",
97
+ title: "Strict indexed recall",
98
+ });
99
+
100
+ const indexed = await memory.searchMemory({
101
+ projectRoot,
102
+ query: "strict index",
103
+ scope: "project",
104
+ });
105
+
106
+ assert.equal(indexed.results.length, 1);
107
+ assert.match(indexed.results[0].snippet, /Use indexed search only\./);
108
+ assert.doesNotMatch(indexed.results[0].snippet, /\.\.\./);
109
+
110
+ await memory.appendEvent({
111
+ content: `Decision:\n${"x".repeat(1300)}`,
112
+ cues: ["oversized-metadata"],
113
+ date: "2026-09-21",
114
+ projectRoot,
115
+ scope: "project",
116
+ sync: false,
117
+ timestamp: "2026-09-21T11:00:00.000Z",
118
+ title: "Large metadata-only match",
119
+ });
120
+
121
+ const oversized = await memory.searchMemory({
122
+ projectRoot,
123
+ query: "oversized metadata",
124
+ scope: "project",
125
+ });
126
+
127
+ assert.deepEqual(oversized.results, []);
128
+
129
+ const projectMemoryRoot = path.join(lagoonRoot, "projects", "search-project");
130
+ await fs.writeFile(
131
+ path.join(projectMemoryRoot, "current_state.md"),
132
+ "# Current State\n\n- Durable zebra setting is active.\n",
133
+ "utf8",
134
+ );
135
+
136
+ const curated = await memory.searchMemory({
137
+ projectRoot,
138
+ query: "durable zebra",
139
+ scope: "project",
140
+ });
141
+
142
+ assert.equal(curated.results[0].path, "current_state.md");
143
+ assert.match(curated.results[0].snippet, /Durable zebra setting is active\./);
144
+
145
+ const eventsRoot = path.join(projectMemoryRoot, "events");
146
+ await fs.writeFile(
147
+ path.join(eventsRoot, "2026-09-20.md"),
148
+ "# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\nCues:\n- quasar-velvet\n\nShould not be scanned.\n",
149
+ "utf8",
150
+ );
151
+
152
+ const unindexed = await memory.searchMemory({
153
+ projectRoot,
154
+ query: "quasar velvet",
155
+ scope: "project",
156
+ });
157
+
158
+ assert.deepEqual(unindexed.results, []);
159
+ });
160
+
161
+ test("search ranks indexed event bodies with typo tolerance", async (t) => {
162
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
163
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-body-search-"));
164
+ const projectRoot = path.join(tempRoot, "body-project");
165
+
166
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
167
+ await fs.mkdir(projectRoot, { recursive: true });
168
+ t.after(async () => {
169
+ if (originalRoot === undefined) {
170
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
171
+ } else {
172
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
173
+ }
174
+ await fs.rm(tempRoot, { recursive: true, force: true });
175
+ });
176
+
177
+ for (const [title, cues, content] of [
178
+ ["Deploy note", ["deploy"], "The zanzibar migration needs a rollback plan."],
179
+ ["Unrelated note", ["billing"], "Invoices are generated monthly."],
180
+ ]) {
181
+ await memory.appendEvent({ content, cues, projectRoot, scope: "project", sync: false, title });
182
+ }
183
+
184
+ const search = await memory.searchMemory({ projectRoot, query: "zanzibr rollback", scope: "project" });
185
+
186
+ assert.equal(search.results.length, 1);
187
+ assert.equal(search.results[0].heading, "Deploy note");
188
+ assert.equal(search.results[0].match, "body");
189
+ });
190
+
191
+ test("search returns whole sentences from an oversized matching paragraph", async (t) => {
192
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
193
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-long-paragraph-"));
194
+ const projectRoot = path.join(tempRoot, "long-project");
195
+
196
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
197
+ await fs.mkdir(projectRoot, { recursive: true });
198
+ t.after(async () => {
199
+ if (originalRoot === undefined) {
200
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
201
+ } else {
202
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
203
+ }
204
+ await fs.rm(tempRoot, { recursive: true, force: true });
205
+ });
206
+
207
+ const filler = "The weekly sync covered routine updates. ".repeat(40);
208
+ await memory.appendEvent({
209
+ content: `${filler}The kestrel database moved to eu-west-3. ${filler}`,
210
+ cues: ["infra"],
211
+ projectRoot,
212
+ scope: "project",
213
+ sync: false,
214
+ title: "Weekly sync",
215
+ });
216
+
217
+ const search = await memory.searchMemory({ projectRoot, query: "kestrel database", scope: "project" });
218
+
219
+ assert.equal(search.results.length, 1);
220
+ assert.match(search.results[0].snippet, /The kestrel database moved to eu-west-3\./);
221
+ assert.ok(search.results[0].snippet.length <= 1200);
222
+ assert.match(search.results[0].snippet, /^The weekly sync|^The kestrel/);
223
+ });