recallnest 3.0.0 → 3.0.1

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 (184) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +222 -254
  3. package/README_CN.md +206 -243
  4. package/package.json +3 -2
  5. package/src/capture-engine.ts +26 -4
  6. package/src/context-composer-stable.ts +5 -1
  7. package/src/embedder.ts +24 -0
  8. package/src/env-config.ts +18 -0
  9. package/src/ingest.ts +225 -17
  10. package/src/kg-extractor.ts +17 -0
  11. package/src/memory-output.ts +35 -0
  12. package/src/pivot-apply.ts +52 -1
  13. package/src/pivot-evidence.ts +128 -0
  14. package/src/ppr-traversal.ts +25 -1
  15. package/src/runtime-config.ts +9 -0
  16. package/src/session-engine.ts +11 -4
  17. package/src/session-output.ts +9 -1
  18. package/src/session-store.ts +2 -3
  19. package/src/skill-promotion.ts +67 -1
  20. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  21. package/src/__tests__/activity-counter.test.ts +0 -125
  22. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  23. package/src/__tests__/admission-control.test.ts +0 -206
  24. package/src/__tests__/age-label.test.ts +0 -33
  25. package/src/__tests__/anchor-generator.test.ts +0 -67
  26. package/src/__tests__/api-server-security.test.ts +0 -102
  27. package/src/__tests__/auto-consolidation.test.ts +0 -168
  28. package/src/__tests__/auto-recall.test.ts +0 -131
  29. package/src/__tests__/babel-integration.test.ts +0 -144
  30. package/src/__tests__/batch-dedup.test.ts +0 -65
  31. package/src/__tests__/belief-history.test.ts +0 -220
  32. package/src/__tests__/canary-eval.test.ts +0 -114
  33. package/src/__tests__/capture-engine.test.ts +0 -1328
  34. package/src/__tests__/capture-heuristic.test.ts +0 -171
  35. package/src/__tests__/cascade-forget.test.ts +0 -210
  36. package/src/__tests__/category-min-scores.test.ts +0 -116
  37. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  38. package/src/__tests__/circuit-breaker.test.ts +0 -99
  39. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  40. package/src/__tests__/confidence-metadata.test.ts +0 -125
  41. package/src/__tests__/confidence-tracker.test.ts +0 -261
  42. package/src/__tests__/conflict-advisor.test.ts +0 -206
  43. package/src/__tests__/conflict-escalation.test.ts +0 -149
  44. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  45. package/src/__tests__/conflict-output.test.ts +0 -85
  46. package/src/__tests__/conflict-store.test.ts +0 -418
  47. package/src/__tests__/connector-v1.test.ts +0 -181
  48. package/src/__tests__/consolidation-engine.test.ts +0 -520
  49. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  50. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  51. package/src/__tests__/context-composer-pins.test.ts +0 -64
  52. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  53. package/src/__tests__/context-reconstructor.test.ts +0 -65
  54. package/src/__tests__/context-renderer.test.ts +0 -91
  55. package/src/__tests__/continuity-eval.test.ts +0 -275
  56. package/src/__tests__/continuity-schema.test.ts +0 -97
  57. package/src/__tests__/contracts.test.ts +0 -262
  58. package/src/__tests__/conversation-importer.test.ts +0 -692
  59. package/src/__tests__/core-recall-eval.test.ts +0 -235
  60. package/src/__tests__/core-summary.test.ts +0 -109
  61. package/src/__tests__/data-checkup.test.ts +0 -304
  62. package/src/__tests__/decay-exemption.test.ts +0 -129
  63. package/src/__tests__/dedup-actions.test.ts +0 -58
  64. package/src/__tests__/dedup-guard.test.ts +0 -39
  65. package/src/__tests__/deterministic-id.test.ts +0 -39
  66. package/src/__tests__/distill-lock.test.ts +0 -305
  67. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  68. package/src/__tests__/doctor.test.ts +0 -319
  69. package/src/__tests__/dream-pipeline.test.ts +0 -820
  70. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  71. package/src/__tests__/dream-write-batching.test.ts +0 -211
  72. package/src/__tests__/drill-down.test.ts +0 -108
  73. package/src/__tests__/embedder.test.ts +0 -122
  74. package/src/__tests__/emotion-decay.test.ts +0 -46
  75. package/src/__tests__/emotion-detector.test.ts +0 -56
  76. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  77. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  78. package/src/__tests__/entity-resolver.test.ts +0 -58
  79. package/src/__tests__/env-config-invariant.test.ts +0 -80
  80. package/src/__tests__/env-config.test.ts +0 -211
  81. package/src/__tests__/error-signature-boost.test.ts +0 -87
  82. package/src/__tests__/error-signature.test.ts +0 -95
  83. package/src/__tests__/error-taxonomy.test.ts +0 -129
  84. package/src/__tests__/ethics-layer.test.ts +0 -476
  85. package/src/__tests__/event-segmenter.test.ts +0 -153
  86. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  87. package/src/__tests__/frequency-tracker.test.ts +0 -113
  88. package/src/__tests__/freshness.test.ts +0 -186
  89. package/src/__tests__/graph-export.test.ts +0 -489
  90. package/src/__tests__/hotness-blend.test.ts +0 -283
  91. package/src/__tests__/ingest-chunking.test.ts +0 -44
  92. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  93. package/src/__tests__/ingest.test.ts +0 -360
  94. package/src/__tests__/interference-detector.test.ts +0 -203
  95. package/src/__tests__/kg-extractor.test.ts +0 -192
  96. package/src/__tests__/kg-store.test.ts +0 -300
  97. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  98. package/src/__tests__/llm-consolidation.test.ts +0 -226
  99. package/src/__tests__/maintenance-locks.test.ts +0 -197
  100. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  101. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  102. package/src/__tests__/memory-boundaries.test.ts +0 -282
  103. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  104. package/src/__tests__/memory-evolution.test.ts +0 -271
  105. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  106. package/src/__tests__/memory-lint.test.ts +0 -421
  107. package/src/__tests__/memory-output.test.ts +0 -224
  108. package/src/__tests__/memory-promotion.test.ts +0 -315
  109. package/src/__tests__/memory-utility.test.ts +0 -256
  110. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  111. package/src/__tests__/multi-vector.test.ts +0 -242
  112. package/src/__tests__/narrative-architecture.test.ts +0 -456
  113. package/src/__tests__/noise-filter.test.ts +0 -205
  114. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  115. package/src/__tests__/obsidian-connector.test.ts +0 -363
  116. package/src/__tests__/omitted-reporting.test.ts +0 -98
  117. package/src/__tests__/openai-contract.test.ts +0 -513
  118. package/src/__tests__/pending-review.test.ts +0 -77
  119. package/src/__tests__/pii-redact.test.ts +0 -333
  120. package/src/__tests__/pivot-apply.test.ts +0 -373
  121. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  122. package/src/__tests__/pivot-distill.test.ts +0 -1178
  123. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  124. package/src/__tests__/ppr-traversal.test.ts +0 -256
  125. package/src/__tests__/prediction-engine.test.ts +0 -349
  126. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  127. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  128. package/src/__tests__/preference-matcher.test.ts +0 -282
  129. package/src/__tests__/preference-slots.test.ts +0 -205
  130. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  131. package/src/__tests__/prospective-memory.test.ts +0 -283
  132. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  133. package/src/__tests__/query-entity-detector.test.ts +0 -171
  134. package/src/__tests__/query-expander-alias.test.ts +0 -146
  135. package/src/__tests__/query-expander.test.ts +0 -34
  136. package/src/__tests__/recall-gate.test.ts +0 -266
  137. package/src/__tests__/recall-governor.test.ts +0 -227
  138. package/src/__tests__/recall-mode.test.ts +0 -64
  139. package/src/__tests__/resolve-db-path.test.ts +0 -130
  140. package/src/__tests__/resolve-tier.test.ts +0 -64
  141. package/src/__tests__/result-synthesizer.test.ts +0 -198
  142. package/src/__tests__/resume-context.test.ts +0 -4898
  143. package/src/__tests__/retrieval-eval.test.ts +0 -163
  144. package/src/__tests__/retrieval-trace.test.ts +0 -213
  145. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  146. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  147. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  148. package/src/__tests__/retroactive-boost.test.ts +0 -223
  149. package/src/__tests__/salience-scoring.test.ts +0 -106
  150. package/src/__tests__/scope-inventory.test.ts +0 -200
  151. package/src/__tests__/scope-policy.test.ts +0 -69
  152. package/src/__tests__/scope-suggester.test.ts +0 -80
  153. package/src/__tests__/session-checkpoint.test.ts +0 -291
  154. package/src/__tests__/session-distiller.test.ts +0 -640
  155. package/src/__tests__/session-gc.test.ts +0 -215
  156. package/src/__tests__/skill-engine.test.ts +0 -552
  157. package/src/__tests__/skill-promotion.test.ts +0 -654
  158. package/src/__tests__/skill-verifier.test.ts +0 -122
  159. package/src/__tests__/source-heartbeat.test.ts +0 -254
  160. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  161. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  162. package/src/__tests__/store-read-consistency.test.ts +0 -122
  163. package/src/__tests__/store-schema-migration.test.ts +0 -87
  164. package/src/__tests__/store-scope-audit.test.ts +0 -81
  165. package/src/__tests__/store-upsert.test.ts +0 -201
  166. package/src/__tests__/supersede-chain.test.ts +0 -154
  167. package/src/__tests__/synthesis-contract.test.ts +0 -252
  168. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  169. package/src/__tests__/temporal-parser.test.ts +0 -241
  170. package/src/__tests__/temporal-validity.test.ts +0 -160
  171. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  172. package/src/__tests__/term-registry.test.ts +0 -121
  173. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  174. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  175. package/src/__tests__/topic-tag.test.ts +0 -113
  176. package/src/__tests__/trusted-memory.test.ts +0 -867
  177. package/src/__tests__/ui-server-security.test.ts +0 -93
  178. package/src/__tests__/usage-tracker.test.ts +0 -287
  179. package/src/__tests__/utility-weight.test.ts +0 -57
  180. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  181. package/src/__tests__/version-manager.test.ts +0 -248
  182. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  183. package/src/__tests__/workflow-observation.test.ts +0 -433
  184. 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
- });