recallnest 3.0.0 → 4.0.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 (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,363 +0,0 @@
1
- import { describe, expect, it, beforeAll, afterAll } from "bun:test";
2
- import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
-
6
- import {
7
- isObsidianVault,
8
- parseFrontmatter,
9
- extractWikiLinks,
10
- contentHash,
11
- scanVault,
12
- } from "../obsidian-connector.js";
13
-
14
- // ---------------------------------------------------------------------------
15
- // Temp vault fixture
16
- // ---------------------------------------------------------------------------
17
-
18
- const VAULT_ROOT = join(tmpdir(), `recallnest-obsidian-test-${Date.now()}`);
19
-
20
- beforeAll(() => {
21
- // Create vault structure
22
- mkdirSync(join(VAULT_ROOT, ".obsidian"), { recursive: true });
23
- mkdirSync(join(VAULT_ROOT, "projects"), { recursive: true });
24
- mkdirSync(join(VAULT_ROOT, "daily"), { recursive: true });
25
- mkdirSync(join(VAULT_ROOT, ".trash"), { recursive: true });
26
-
27
- // Root-level note with frontmatter + wikilinks
28
- writeFileSync(
29
- join(VAULT_ROOT, "My Note.md"),
30
- `---
31
- tags: [ai, memory]
32
- aliases: [note-1]
33
- status: active
34
- ---
35
-
36
- # My Note
37
-
38
- This is a note about [[RecallNest]] and [[LanceDB|vector database]].
39
-
40
- Some more content here to make it long enough.
41
- `,
42
- );
43
-
44
- // Nested note in projects/
45
- writeFileSync(
46
- join(VAULT_ROOT, "projects", "Project Alpha.md"),
47
- `---
48
- tags:
49
- - project
50
- - alpha
51
- priority: high
52
- ---
53
-
54
- # Project Alpha
55
-
56
- Working on [[Feature X]] for [[Project Alpha]].
57
-
58
- Details about the project implementation and design decisions.
59
- `,
60
- );
61
-
62
- // Daily note without frontmatter
63
- writeFileSync(
64
- join(VAULT_ROOT, "daily", "2026-04-13.md"),
65
- `# 2026-04-13
66
-
67
- - Met with team about [[Q2 Goals]]
68
- - Reviewed [[PR #123]]
69
- - Need to follow up on [[Budget Review]]
70
-
71
- Some additional notes about the day and what happened.
72
- `,
73
- );
74
-
75
- // Short note (should be skipped)
76
- writeFileSync(join(VAULT_ROOT, "short.md"), "Hi");
77
-
78
- // Trash note (should be skipped)
79
- writeFileSync(
80
- join(VAULT_ROOT, ".trash", "deleted.md"),
81
- "This is deleted content that should not be ingested into the vault.\n",
82
- );
83
- });
84
-
85
- afterAll(() => {
86
- try {
87
- rmSync(VAULT_ROOT, { recursive: true, force: true });
88
- } catch {
89
- // cleanup best-effort
90
- }
91
- });
92
-
93
- // ---------------------------------------------------------------------------
94
- // isObsidianVault
95
- // ---------------------------------------------------------------------------
96
-
97
- describe("isObsidianVault", () => {
98
- it("returns true for a directory with .obsidian/", () => {
99
- expect(isObsidianVault(VAULT_ROOT)).toBe(true);
100
- });
101
-
102
- it("returns false for a regular directory", () => {
103
- expect(isObsidianVault(join(VAULT_ROOT, "projects"))).toBe(false);
104
- });
105
-
106
- it("returns false for non-existent directory", () => {
107
- expect(isObsidianVault("/nonexistent/path")).toBe(false);
108
- });
109
- });
110
-
111
- // ---------------------------------------------------------------------------
112
- // parseFrontmatter
113
- // ---------------------------------------------------------------------------
114
-
115
- describe("parseFrontmatter", () => {
116
- it("parses inline list tags", () => {
117
- const content = `---
118
- tags: [ai, memory]
119
- status: active
120
- ---
121
-
122
- Body content here.`;
123
- const { frontmatter, body } = parseFrontmatter(content);
124
- expect(frontmatter).not.toBeNull();
125
- expect(frontmatter!.tags).toEqual(["ai", "memory"]);
126
- expect(frontmatter!.status).toBe("active");
127
- expect(body).toBe("Body content here.");
128
- });
129
-
130
- it("parses YAML list tags", () => {
131
- const content = `---
132
- tags:
133
- - project
134
- - alpha
135
- priority: high
136
- ---
137
-
138
- Body here.`;
139
- const { frontmatter, body } = parseFrontmatter(content);
140
- expect(frontmatter).not.toBeNull();
141
- expect(frontmatter!.tags).toEqual(["project", "alpha"]);
142
- expect(frontmatter!.priority).toBe("high");
143
- expect(body).toBe("Body here.");
144
- });
145
-
146
- it("returns null frontmatter when no --- block", () => {
147
- const content = "# Just a heading\n\nSome content.";
148
- const { frontmatter, body } = parseFrontmatter(content);
149
- expect(frontmatter).toBeNull();
150
- expect(body).toBe(content);
151
- });
152
-
153
- it("handles empty frontmatter", () => {
154
- const content = `---
155
- ---
156
-
157
- Body.`;
158
- const { frontmatter, body } = parseFrontmatter(content);
159
- expect(frontmatter).toBeNull();
160
- expect(body).toBe("Body.");
161
- });
162
-
163
- it("handles quoted values", () => {
164
- const content = `---
165
- title: 'My Title'
166
- author: "Alice"
167
- ---
168
-
169
- Content.`;
170
- const { frontmatter } = parseFrontmatter(content);
171
- expect(frontmatter).not.toBeNull();
172
- expect(frontmatter!.title).toBe("My Title");
173
- expect(frontmatter!.author).toBe("Alice");
174
- });
175
-
176
- it("handles empty inline list []", () => {
177
- const content = `---
178
- tags: []
179
- ---
180
-
181
- Content here is long enough.`;
182
- const { frontmatter } = parseFrontmatter(content);
183
- // Empty list should not produce tags
184
- expect(frontmatter).toBeNull();
185
- });
186
-
187
- it("handles Windows-style line endings", () => {
188
- const content = "---\r\ntags: [a, b]\r\n---\r\n\r\nBody.";
189
- const { frontmatter, body } = parseFrontmatter(content);
190
- expect(frontmatter).not.toBeNull();
191
- expect(frontmatter!.tags).toEqual(["a", "b"]);
192
- expect(body).toBe("Body.");
193
- });
194
- });
195
-
196
- // ---------------------------------------------------------------------------
197
- // extractWikiLinks
198
- // ---------------------------------------------------------------------------
199
-
200
- describe("extractWikiLinks", () => {
201
- it("extracts simple wikilinks", () => {
202
- const links = extractWikiLinks("Link to [[RecallNest]] and [[LanceDB]].");
203
- expect(links).toHaveLength(2);
204
- expect(links[0].target).toBe("RecallNest");
205
- expect(links[1].target).toBe("LanceDB");
206
- });
207
-
208
- it("extracts wikilinks with display text", () => {
209
- const links = extractWikiLinks("See [[LanceDB|vector database]] for details.");
210
- expect(links).toHaveLength(1);
211
- expect(links[0].target).toBe("LanceDB");
212
- expect(links[0].displayText).toBe("vector database");
213
- });
214
-
215
- it("skips embed syntax ![[embed]]", () => {
216
- const links = extractWikiLinks("Normal [[link]] and embed ![[image.png]].");
217
- expect(links).toHaveLength(1);
218
- expect(links[0].target).toBe("link");
219
- });
220
-
221
- it("handles multiple links on same line", () => {
222
- const links = extractWikiLinks("[[A]] connects to [[B]] and [[C]]");
223
- expect(links).toHaveLength(3);
224
- });
225
-
226
- it("returns empty array for no links", () => {
227
- const links = extractWikiLinks("Just plain text.");
228
- expect(links).toHaveLength(0);
229
- });
230
-
231
- it("trims whitespace from targets", () => {
232
- const links = extractWikiLinks("[[ Spaced Target ]]");
233
- expect(links[0].target).toBe("Spaced Target");
234
- });
235
- });
236
-
237
- // ---------------------------------------------------------------------------
238
- // contentHash
239
- // ---------------------------------------------------------------------------
240
-
241
- describe("contentHash", () => {
242
- it("returns a 16-char hex string", () => {
243
- const hash = contentHash("hello world");
244
- expect(hash).toHaveLength(16);
245
- expect(/^[0-9a-f]{16}$/.test(hash)).toBe(true);
246
- });
247
-
248
- it("is deterministic", () => {
249
- expect(contentHash("same input")).toBe(contentHash("same input"));
250
- });
251
-
252
- it("differs for different inputs", () => {
253
- expect(contentHash("input A")).not.toBe(contentHash("input B"));
254
- });
255
- });
256
-
257
- // ---------------------------------------------------------------------------
258
- // scanVault
259
- // ---------------------------------------------------------------------------
260
-
261
- describe("scanVault", () => {
262
- it("produces valid connector-v1 output", () => {
263
- const output = scanVault(VAULT_ROOT);
264
- expect(output.version).toBe("connector-v1");
265
- expect(output.source).toBe("obsidian");
266
- expect(output.scope).toContain("vault:");
267
- expect(Array.isArray(output.records)).toBe(true);
268
- });
269
-
270
- it("finds all non-short, non-excluded .md files", () => {
271
- const output = scanVault(VAULT_ROOT);
272
- // Should find: My Note.md, projects/Project Alpha.md, daily/2026-04-13.md
273
- // Should NOT find: short.md (too short), .trash/deleted.md (excluded)
274
- expect(output.records).toHaveLength(3);
275
- });
276
-
277
- it("extracts frontmatter tags", () => {
278
- const output = scanVault(VAULT_ROOT);
279
- const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
280
- expect(myNote).toBeDefined();
281
- expect(myNote!.tags).toContain("ai");
282
- expect(myNote!.tags).toContain("memory");
283
- });
284
-
285
- it("extracts wikilinks as link: tags", () => {
286
- const output = scanVault(VAULT_ROOT);
287
- const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
288
- expect(myNote).toBeDefined();
289
- expect(myNote!.tags).toContain("link:RecallNest");
290
- expect(myNote!.tags).toContain("link:LanceDB");
291
- });
292
-
293
- it("maps folder structure to folder: tags", () => {
294
- const output = scanVault(VAULT_ROOT);
295
- const projectNote = output.records.find((r) => r.id?.includes("projects/"));
296
- expect(projectNote).toBeDefined();
297
- expect(projectNote!.tags).toContain("folder:projects");
298
- });
299
-
300
- it("does not add folder: tag for root-level files", () => {
301
- const output = scanVault(VAULT_ROOT);
302
- const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
303
- expect(myNote).toBeDefined();
304
- const folderTags = myNote!.tags!.filter((t) => t.startsWith("folder:"));
305
- expect(folderTags).toHaveLength(0);
306
- });
307
-
308
- it("skips .trash and .obsidian directories", () => {
309
- const output = scanVault(VAULT_ROOT);
310
- const trashNote = output.records.find((r) => r.id?.includes(".trash"));
311
- expect(trashNote).toBeUndefined();
312
- });
313
-
314
- it("computes contentHash for each record", () => {
315
- const output = scanVault(VAULT_ROOT);
316
- for (const record of output.records) {
317
- expect(record.contentHash).toBeDefined();
318
- expect(record.contentHash!).toHaveLength(16);
319
- }
320
- });
321
-
322
- it("includes sourceMetadata with vault info", () => {
323
- const output = scanVault(VAULT_ROOT);
324
- const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
325
- expect(myNote!.sourceMetadata).toBeDefined();
326
- expect(myNote!.sourceMetadata!.vaultPath).toBe(VAULT_ROOT);
327
- expect(myNote!.sourceMetadata!.relativePath).toBe("My Note.md");
328
- expect(myNote!.sourceMetadata!.frontmatter).toBeDefined();
329
- expect(myNote!.sourceMetadata!.wikiLinks).toBeDefined();
330
- });
331
-
332
- it("respects custom scopePrefix", () => {
333
- const output = scanVault(VAULT_ROOT, { scopePrefix: "custom:my-vault" });
334
- expect(output.scope).toBe("custom:my-vault");
335
- });
336
-
337
- it("respects custom excludeDirs", () => {
338
- // Exclude 'daily' directory
339
- const output = scanVault(VAULT_ROOT, {
340
- excludeDirs: [".obsidian", ".trash", ".git", "node_modules", "daily"],
341
- });
342
- const dailyNote = output.records.find((r) => r.id?.includes("daily/"));
343
- expect(dailyNote).toBeUndefined();
344
- // Should still find the other 2
345
- expect(output.records).toHaveLength(2);
346
- });
347
-
348
- it("strips frontmatter from body text", () => {
349
- const output = scanVault(VAULT_ROOT);
350
- const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
351
- expect(myNote!.text).not.toContain("---");
352
- expect(myNote!.text).toContain("# My Note");
353
- });
354
-
355
- it("handles note without frontmatter", () => {
356
- const output = scanVault(VAULT_ROOT);
357
- const dailyNote = output.records.find((r) => r.id?.includes("2026-04-13"));
358
- expect(dailyNote).toBeDefined();
359
- // Should still extract wikilinks
360
- expect(dailyNote!.tags).toContain("link:Q2 Goals");
361
- expect(dailyNote!.tags).toContain("link:PR #123");
362
- });
363
- });
@@ -1,98 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { createRetriever, type RetrievalResultSet } from "../retriever.js";
4
-
5
- // P-omitted: 验证召回因 limit 上限截断时,对调用方暴露 omitted(提高 limit 可见)。
6
- // 借鉴 RepoPrompt CE「剪枝必回报」——召回从黑箱变可协商。
7
-
8
- function buildCandidate(id: string, score: number) {
9
- return {
10
- entry: {
11
- id,
12
- text: `distinct memory content number ${id}`,
13
- vector: [1, 0, 0],
14
- category: "preferences",
15
- scope: "memory:agent",
16
- importance: 0.8,
17
- timestamp: Date.parse("2026-03-16T00:00:00.000Z"),
18
- metadata: JSON.stringify({}),
19
- },
20
- score,
21
- };
22
- }
23
-
24
- // 最小过滤 config:mode=vector 跳过 BM25/rerank;minScore/hardMinScore=0 + 高分候选避免 hard-min 截断;
25
- // filterNoise=false;RIF/sourceDiversity 字段缺省即不启用;MMR 只重排不删。
26
- // 于是过滤链净效果 = sort → slice(limit),omitted = candidates - limit 可精确断言。
27
- function makeRetriever(
28
- candidates: ReturnType<typeof buildCandidate>[],
29
- configOverride: Record<string, unknown> = {},
30
- ) {
31
- return createRetriever({
32
- hasFtsSupport: false,
33
- // 模拟真实 MemoryStore.vectorSearch:尊重 k 参数(按 k 截断返回)。
34
- // 这样测试能真实反映 over-fetch 是否生效——若 vector-only 不 over-fetch、只取 limit 条,
35
- // 候选池 ≤ limit、omitted 必为 0,下面断言会失败(Codex review 暴露的假绿点)。
36
- async vectorSearch(_vec: number[], k: number) { return candidates.slice(0, k); },
37
- } as any, {
38
- async embedQuery() { return [1, 0, 0]; },
39
- async embedPassage() { return [1, 0, 0]; },
40
- } as any, {
41
- mode: "vector",
42
- rerank: "none",
43
- filterNoise: false,
44
- hardMinScore: 0,
45
- minScore: 0,
46
- recencyWeight: 0,
47
- timeDecayHalfLifeDays: 0,
48
- ...configOverride,
49
- });
50
- }
51
-
52
- describe("retrieval omitted reporting (P-omitted)", () => {
53
- it("reports omitted count + reason when candidates exceed limit", async () => {
54
- const candidates = Array.from({ length: 8 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
55
- const retriever = makeRetriever(candidates);
56
-
57
- const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 3, category: "preferences" }) as RetrievalResultSet;
58
-
59
- expect(results).toHaveLength(3);
60
- expect(results.omitted).toBeDefined();
61
- expect(results.omitted?.count).toBe(5); // 8 candidates - limit 3
62
- expect(results.omitted?.reason).toBe("limit");
63
- });
64
-
65
- it("leaves omitted undefined when candidates fit within limit", async () => {
66
- const candidates = Array.from({ length: 2 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
67
- const retriever = makeRetriever(candidates);
68
-
69
- const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 5, category: "preferences" }) as RetrievalResultSet;
70
-
71
- expect(results).toHaveLength(2);
72
- expect(results.omitted).toBeUndefined();
73
- });
74
-
75
- it("persists omitted through the validity filter (regression: array is rebuilt downstream)", async () => {
76
- // retrieve() runs filterByValidity which returns a NEW array, dropping any
77
- // property hung on the result array. omitted must survive via the resultSet snapshot.
78
- const candidates = Array.from({ length: 6 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
79
- const retriever = makeRetriever(candidates);
80
-
81
- const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 2, category: "preferences" }) as RetrievalResultSet;
82
-
83
- expect(results).toHaveLength(2);
84
- expect(results.omitted?.count).toBe(4); // 6 - 2
85
- });
86
-
87
- it("reports omitted even when sourceDiversity caps the diversified pool (P3-1)", async () => {
88
- // 启用 sourceDiversity 时 applySourceDiversity 先把 diversified 压到 limit;
89
- // omitted 必须从 source-diversity 之前的候选池(clusterDeduped)算,否则恒为 0(Codex 复审 P3-1)。
90
- const candidates = Array.from({ length: 8 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
91
- const retriever = makeRetriever(candidates, { sourceDiversity: 0.5 });
92
-
93
- const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 3, category: "preferences" }) as RetrievalResultSet;
94
-
95
- expect(results).toHaveLength(3);
96
- expect(results.omitted?.count).toBe(5); // 8 - 3,即使 source diversity 压缩了 diversified
97
- });
98
- });