recallnest 2.6.1 → 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 (207) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +280 -195
  3. package/README_CN.md +266 -199
  4. package/config.json.example +8 -1
  5. package/docs/api-reference.md +1 -1
  6. package/integrations/README.md +1 -1
  7. package/integrations/agy/README.md +73 -0
  8. package/integrations/agy/setup.sh +75 -0
  9. package/integrations/setup-all.sh +1 -1
  10. package/package.json +7 -5
  11. package/src/access-tracker.ts +10 -1
  12. package/src/activity-counter.ts +69 -11
  13. package/src/age-label.ts +29 -0
  14. package/src/audit-log.ts +35 -0
  15. package/src/auto-gc.ts +97 -30
  16. package/src/capture-engine.ts +26 -4
  17. package/src/cli.ts +224 -10
  18. package/src/consolidation-engine.ts +317 -123
  19. package/src/context-composer-stable.ts +5 -1
  20. package/src/data-checkup.ts +17 -3
  21. package/src/dream-pipeline.ts +90 -12
  22. package/src/embedder.ts +44 -5
  23. package/src/env-config.ts +71 -0
  24. package/src/forget-engine.ts +8 -2
  25. package/src/gateway-server.ts +272 -0
  26. package/src/ingest.ts +248 -22
  27. package/src/kg-extractor.ts +17 -0
  28. package/src/llm-client.ts +86 -25
  29. package/src/mcp-server.ts +2 -1
  30. package/src/mcp-tools-advanced.ts +7 -2
  31. package/src/mcp-tools-core.ts +11 -1
  32. package/src/mcp-tools-governance.ts +45 -3
  33. package/src/memory-evolution.ts +16 -0
  34. package/src/memory-output.ts +47 -8
  35. package/src/memory-promotion.ts +280 -0
  36. package/src/memory-store-port.ts +32 -3
  37. package/src/memory-utility.ts +314 -0
  38. package/src/pivot-apply.ts +52 -1
  39. package/src/pivot-evidence.ts +128 -0
  40. package/src/ppr-traversal.ts +25 -1
  41. package/src/recall-ledger.ts +76 -0
  42. package/src/retriever.ts +82 -9
  43. package/src/runtime-config.ts +9 -0
  44. package/src/scope-policy.ts +22 -1
  45. package/src/session-engine.ts +11 -4
  46. package/src/session-output.ts +13 -4
  47. package/src/session-store.ts +2 -3
  48. package/src/skill-promotion.ts +218 -1
  49. package/src/store.ts +231 -71
  50. package/src/synthesis-contract.ts +286 -0
  51. package/src/version-manager.ts +83 -0
  52. package/src/workflow-observation-schema.ts +26 -0
  53. package/integrations/gemini-cli/README.md +0 -57
  54. package/integrations/gemini-cli/setup.sh +0 -64
  55. package/src/__tests__/access-tracker-novelty.test.ts +0 -304
  56. package/src/__tests__/activity-counter.test.ts +0 -125
  57. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  58. package/src/__tests__/admission-control.test.ts +0 -206
  59. package/src/__tests__/anchor-generator.test.ts +0 -67
  60. package/src/__tests__/api-server-security.test.ts +0 -102
  61. package/src/__tests__/auto-consolidation.test.ts +0 -168
  62. package/src/__tests__/auto-recall.test.ts +0 -131
  63. package/src/__tests__/babel-integration.test.ts +0 -144
  64. package/src/__tests__/batch-dedup.test.ts +0 -65
  65. package/src/__tests__/belief-history.test.ts +0 -220
  66. package/src/__tests__/canary-eval.test.ts +0 -114
  67. package/src/__tests__/capture-engine.test.ts +0 -1328
  68. package/src/__tests__/capture-heuristic.test.ts +0 -171
  69. package/src/__tests__/cascade-forget.test.ts +0 -210
  70. package/src/__tests__/category-min-scores.test.ts +0 -116
  71. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  72. package/src/__tests__/circuit-breaker.test.ts +0 -99
  73. package/src/__tests__/cluster-consolidation.test.ts +0 -945
  74. package/src/__tests__/confidence-metadata.test.ts +0 -125
  75. package/src/__tests__/confidence-tracker.test.ts +0 -261
  76. package/src/__tests__/conflict-advisor.test.ts +0 -206
  77. package/src/__tests__/conflict-escalation.test.ts +0 -149
  78. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  79. package/src/__tests__/conflict-output.test.ts +0 -85
  80. package/src/__tests__/conflict-store.test.ts +0 -418
  81. package/src/__tests__/connector-v1.test.ts +0 -181
  82. package/src/__tests__/consolidation-engine.test.ts +0 -430
  83. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  84. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  85. package/src/__tests__/context-composer-pins.test.ts +0 -64
  86. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  87. package/src/__tests__/context-reconstructor.test.ts +0 -65
  88. package/src/__tests__/context-renderer.test.ts +0 -91
  89. package/src/__tests__/continuity-eval.test.ts +0 -275
  90. package/src/__tests__/continuity-schema.test.ts +0 -97
  91. package/src/__tests__/contracts.test.ts +0 -262
  92. package/src/__tests__/conversation-importer.test.ts +0 -692
  93. package/src/__tests__/core-recall-eval.test.ts +0 -235
  94. package/src/__tests__/core-summary.test.ts +0 -109
  95. package/src/__tests__/data-checkup.test.ts +0 -276
  96. package/src/__tests__/decay-exemption.test.ts +0 -129
  97. package/src/__tests__/dedup-actions.test.ts +0 -58
  98. package/src/__tests__/dedup-guard.test.ts +0 -39
  99. package/src/__tests__/deterministic-id.test.ts +0 -39
  100. package/src/__tests__/distill-lock.test.ts +0 -305
  101. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  102. package/src/__tests__/doctor.test.ts +0 -319
  103. package/src/__tests__/dream-pipeline.test.ts +0 -780
  104. package/src/__tests__/drill-down.test.ts +0 -108
  105. package/src/__tests__/embedder.test.ts +0 -122
  106. package/src/__tests__/emotion-decay.test.ts +0 -46
  107. package/src/__tests__/emotion-detector.test.ts +0 -56
  108. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  109. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  110. package/src/__tests__/entity-resolver.test.ts +0 -58
  111. package/src/__tests__/env-config.test.ts +0 -151
  112. package/src/__tests__/error-signature-boost.test.ts +0 -87
  113. package/src/__tests__/error-signature.test.ts +0 -95
  114. package/src/__tests__/error-taxonomy.test.ts +0 -129
  115. package/src/__tests__/ethics-layer.test.ts +0 -476
  116. package/src/__tests__/event-segmenter.test.ts +0 -153
  117. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  118. package/src/__tests__/frequency-tracker.test.ts +0 -113
  119. package/src/__tests__/freshness.test.ts +0 -186
  120. package/src/__tests__/graph-export.test.ts +0 -489
  121. package/src/__tests__/hotness-blend.test.ts +0 -283
  122. package/src/__tests__/ingest-chunking.test.ts +0 -44
  123. package/src/__tests__/ingest.test.ts +0 -360
  124. package/src/__tests__/interference-detector.test.ts +0 -203
  125. package/src/__tests__/kg-extractor.test.ts +0 -192
  126. package/src/__tests__/kg-store.test.ts +0 -300
  127. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  128. package/src/__tests__/llm-consolidation.test.ts +0 -226
  129. package/src/__tests__/maintenance-locks.test.ts +0 -189
  130. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  131. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  132. package/src/__tests__/memory-boundaries.test.ts +0 -282
  133. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
  134. package/src/__tests__/memory-evolution.test.ts +0 -271
  135. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  136. package/src/__tests__/memory-lint.test.ts +0 -421
  137. package/src/__tests__/memory-output.test.ts +0 -215
  138. package/src/__tests__/memory-promotion.test.ts +0 -315
  139. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  140. package/src/__tests__/multi-vector.test.ts +0 -242
  141. package/src/__tests__/narrative-architecture.test.ts +0 -456
  142. package/src/__tests__/noise-filter.test.ts +0 -205
  143. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  144. package/src/__tests__/obsidian-connector.test.ts +0 -363
  145. package/src/__tests__/omitted-reporting.test.ts +0 -98
  146. package/src/__tests__/pending-review.test.ts +0 -77
  147. package/src/__tests__/pii-redact.test.ts +0 -333
  148. package/src/__tests__/pivot-apply.test.ts +0 -373
  149. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  150. package/src/__tests__/pivot-distill.test.ts +0 -1178
  151. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  152. package/src/__tests__/ppr-traversal.test.ts +0 -256
  153. package/src/__tests__/prediction-engine.test.ts +0 -349
  154. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  155. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  156. package/src/__tests__/preference-matcher.test.ts +0 -282
  157. package/src/__tests__/preference-slots.test.ts +0 -205
  158. package/src/__tests__/prospective-memory.test.ts +0 -283
  159. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  160. package/src/__tests__/query-entity-detector.test.ts +0 -171
  161. package/src/__tests__/query-expander-alias.test.ts +0 -146
  162. package/src/__tests__/query-expander.test.ts +0 -34
  163. package/src/__tests__/recall-gate.test.ts +0 -266
  164. package/src/__tests__/recall-governor.test.ts +0 -227
  165. package/src/__tests__/recall-mode.test.ts +0 -64
  166. package/src/__tests__/resolve-db-path.test.ts +0 -130
  167. package/src/__tests__/resolve-tier.test.ts +0 -64
  168. package/src/__tests__/result-synthesizer.test.ts +0 -198
  169. package/src/__tests__/resume-context.test.ts +0 -4898
  170. package/src/__tests__/retrieval-eval.test.ts +0 -163
  171. package/src/__tests__/retrieval-trace.test.ts +0 -213
  172. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  173. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  174. package/src/__tests__/retroactive-boost.test.ts +0 -223
  175. package/src/__tests__/salience-scoring.test.ts +0 -106
  176. package/src/__tests__/scope-inventory.test.ts +0 -200
  177. package/src/__tests__/scope-policy.test.ts +0 -69
  178. package/src/__tests__/scope-suggester.test.ts +0 -80
  179. package/src/__tests__/session-checkpoint.test.ts +0 -291
  180. package/src/__tests__/session-distiller.test.ts +0 -640
  181. package/src/__tests__/session-gc.test.ts +0 -215
  182. package/src/__tests__/skill-engine.test.ts +0 -552
  183. package/src/__tests__/skill-promotion.test.ts +0 -654
  184. package/src/__tests__/skill-verifier.test.ts +0 -122
  185. package/src/__tests__/source-heartbeat.test.ts +0 -250
  186. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  187. package/src/__tests__/store-patch-metadata.test.ts +0 -205
  188. package/src/__tests__/store-read-consistency.test.ts +0 -122
  189. package/src/__tests__/store-schema-migration.test.ts +0 -87
  190. package/src/__tests__/store-scope-audit.test.ts +0 -81
  191. package/src/__tests__/store-upsert.test.ts +0 -201
  192. package/src/__tests__/supersede-chain.test.ts +0 -154
  193. package/src/__tests__/temporal-parser.test.ts +0 -241
  194. package/src/__tests__/temporal-validity.test.ts +0 -160
  195. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  196. package/src/__tests__/term-registry.test.ts +0 -121
  197. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  198. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  199. package/src/__tests__/topic-tag.test.ts +0 -113
  200. package/src/__tests__/trusted-memory.test.ts +0 -839
  201. package/src/__tests__/ui-server-security.test.ts +0 -93
  202. package/src/__tests__/usage-tracker.test.ts +0 -287
  203. package/src/__tests__/version-manager.test.ts +0 -213
  204. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  205. package/src/__tests__/workflow-observation.test.ts +0 -433
  206. package/src/__tests__/write-verifier.test.ts +0 -122
  207. /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
@@ -1,280 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { persistMemory } from "../capture-engine.js";
7
- import {
8
- NoisePrototypeBank,
9
- isProtectedContent,
10
- resetSharedNoisePrototypeBank,
11
- resolveNoisePrototypeBank,
12
- resolveNoisePrototypeMode,
13
- } from "../noise-prototype-bank.js";
14
-
15
- const cleanupPaths: string[] = [];
16
-
17
- afterEach(() => {
18
- resetSharedNoisePrototypeBank();
19
- while (cleanupPaths.length > 0) {
20
- const target = cleanupPaths.pop();
21
- if (target) rmSync(target, { recursive: true, force: true });
22
- }
23
- });
24
-
25
- function tempDir(): string {
26
- const dir = mkdtempSync(join(tmpdir(), "recallnest-npb-"));
27
- cleanupPaths.push(dir);
28
- return dir;
29
- }
30
-
31
- function makeBank(overrides: Partial<ConstructorParameters<typeof NoisePrototypeBank>[0]> = {}) {
32
- const dir = tempDir();
33
- const bank = new NoisePrototypeBank({
34
- filePath: join(dir, "bank.json"),
35
- shadowLogPath: join(dir, "shadow.jsonl"),
36
- ...overrides,
37
- });
38
- return { bank, dir };
39
- }
40
-
41
- // Deterministic fake embedder: unit vectors along an axis picked by seed.
42
- function axisVector(axis: number, dim = 8): number[] {
43
- const v = new Array(dim).fill(0);
44
- v[axis] = 1;
45
- return v;
46
- }
47
-
48
- const embedAxis0 = async () => axisVector(0);
49
-
50
- describe("NoisePrototypeBank — learning discipline", () => {
51
- it("first rejection is only a pending candidate; the repeat sighting learns", async () => {
52
- const { bank } = makeBank();
53
- expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0)).toBe("candidate_seen");
54
- expect(bank.list().length).toBe(0);
55
- expect(bank.pendingCount()).toBe(1);
56
-
57
- expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0)).toBe("learned");
58
- expect(bank.list().length).toBe(1);
59
- expect(bank.pendingCount()).toBe(0);
60
- expect(bank.list()[0]?.seenCount).toBe(2);
61
- });
62
-
63
- it("normalization dedupes near-identical rejections into one candidate", async () => {
64
- const { bank } = makeBank();
65
- await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0);
66
- // Same content modulo whitespace/punctuation/case → same candidate.
67
- expect(await bank.learnFromRejection("嗯嗯嗯嗯,好的 好的!!", embedAxis0)).toBe("learned");
68
- });
69
-
70
- it("already-learned content reports skipped_duplicate without re-embedding", async () => {
71
- const { bank } = makeBank();
72
- let embeds = 0;
73
- const countingEmbed = async () => {
74
- embeds += 1;
75
- return axisVector(0);
76
- };
77
- await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed);
78
- await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed);
79
- expect(embeds).toBe(1);
80
- expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed)).toBe("skipped_duplicate");
81
- expect(embeds).toBe(1);
82
- });
83
-
84
- it("protected content is never learned, even when the noise filter rejected it", async () => {
85
- const { bank } = makeBank();
86
- expect(await bank.learnFromRejection("记住我喜欢的部署方式", embedAxis0)).toBe("skipped_protected");
87
- expect(await bank.learnFromRejection("记住我喜欢的部署方式", embedAxis0)).toBe("skipped_protected");
88
- expect(bank.list().length).toBe(0);
89
- expect(bank.pendingCount()).toBe(0);
90
- });
91
-
92
- it("the cap stops learning at maxPrototypes", async () => {
93
- const { bank } = makeBank({ maxPrototypes: 1, minSeenCount: 1 });
94
- expect(await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0)).toBe("learned");
95
- expect(await bank.learnFromRejection("噪声内容乙乙乙乙", embedAxis0)).toBe("skipped_full");
96
- expect(bank.list().length).toBe(1);
97
- });
98
- });
99
-
100
- describe("NoisePrototypeBank — shadow matching", () => {
101
- async function seededBank() {
102
- const made = makeBank({ minSeenCount: 1 });
103
- await made.bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0);
104
- return made;
105
- }
106
-
107
- it("similarity at/above threshold suggests rejection and logs, but only logs", async () => {
108
- const { bank, dir } = await seededBank();
109
- const match = bank.matchShadow("嗯嗯好的收到啦", axisVector(0), "mem-123");
110
- expect(match.matched).toBe(true);
111
- expect(match.similarity).toBeCloseTo(1, 5);
112
- expect(bank.list()[0]?.hitCount).toBe(1);
113
-
114
- const log = readFileSync(join(dir, "shadow.jsonl"), "utf8").trim().split("\n");
115
- const suggest = log.map((l) => JSON.parse(l)).find((e) => e.event === "suggest_reject");
116
- expect(suggest?.memoryId).toBe("mem-123");
117
- expect(suggest?.prototypeId).toBe(bank.list()[0]?.id);
118
- expect(JSON.stringify(suggest)).not.toContain("嗯嗯好的收到啦");
119
- });
120
-
121
- it("below-threshold similarity does not match", async () => {
122
- const { bank } = await seededBank();
123
- const match = bank.matchShadow("完全不同的正交内容", axisVector(1));
124
- expect(match.matched).toBe(false);
125
- expect(bank.list()[0]?.hitCount).toBe(0);
126
- });
127
-
128
- it("protected content is never flagged regardless of similarity", async () => {
129
- const { bank } = await seededBank();
130
- const match = bank.matchShadow("记住我喜欢的部署方式", axisVector(0));
131
- expect(match.matched).toBe(false);
132
- expect(match.protected).toBe(true);
133
- });
134
-
135
- it("dimension mismatch degrades to no match instead of throwing", async () => {
136
- const { bank } = await seededBank();
137
- const match = bank.matchShadow("维度不齐的向量", [1, 0]);
138
- expect(match.matched).toBe(false);
139
- });
140
- });
141
-
142
- describe("NoisePrototypeBank — correction & lifecycle", () => {
143
- it("removePrototype surgically deletes one false positive", async () => {
144
- const { bank } = makeBank({ minSeenCount: 1 });
145
- await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
146
- const id = bank.list()[0]?.id ?? "";
147
- expect(bank.removePrototype(id)).toBe(true);
148
- expect(bank.list().length).toBe(0);
149
- expect(bank.removePrototype(id)).toBe(false);
150
- });
151
-
152
- it("clear resets everything", async () => {
153
- const { bank } = makeBank({ minSeenCount: 1 });
154
- await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
155
- bank.clear();
156
- expect(bank.list().length).toBe(0);
157
- });
158
-
159
- it("state survives reconstruction from disk; expired prototypes are swept", async () => {
160
- const { bank, dir } = makeBank({ minSeenCount: 1 });
161
- await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
162
-
163
- const reloaded = new NoisePrototypeBank({
164
- filePath: join(dir, "bank.json"),
165
- shadowLogPath: join(dir, "shadow.jsonl"),
166
- });
167
- expect(reloaded.list().length).toBe(1);
168
-
169
- // Age the stored prototype past the TTL, then reload.
170
- const raw = JSON.parse(readFileSync(join(dir, "bank.json"), "utf8"));
171
- raw.prototypes[0].learnedAt = "2020-01-01T00:00:00.000Z";
172
- writeFileSync(join(dir, "bank.json"), JSON.stringify(raw), "utf8");
173
- const expiring = new NoisePrototypeBank({
174
- filePath: join(dir, "bank.json"),
175
- shadowLogPath: join(dir, "shadow.jsonl"),
176
- });
177
- expect(expiring.list().length).toBe(0);
178
- });
179
- });
180
-
181
- describe("isProtectedContent", () => {
182
- it("covers memory verbs, corrections, decisions, identity, preferences", () => {
183
- for (const text of ["记住这个", "帮我记一下", "纠正一下,不对", "我决定用方案B", "我是安闲静雅", "我喜欢深色主题", "i prefer dark mode", "remember this setup"]) {
184
- expect(`${text}:${isProtectedContent(text)}`).toBe(`${text}:true`);
185
- }
186
- for (const text of ["嗯嗯好的", "git push", "今天天气不错"]) {
187
- expect(`${text}:${isProtectedContent(text)}`).toBe(`${text}:false`);
188
- }
189
- });
190
- });
191
-
192
- describe("resolveNoisePrototypeMode / shared bank resolution", () => {
193
- it("defaults to shadow in production, off under NODE_ENV=test unless explicit", () => {
194
- expect(resolveNoisePrototypeMode({} as NodeJS.ProcessEnv)).toBe("shadow");
195
- expect(resolveNoisePrototypeMode({ NODE_ENV: "test" } as NodeJS.ProcessEnv)).toBe("off");
196
- expect(resolveNoisePrototypeMode({ NODE_ENV: "test", RECALLNEST_NOISE_PROTOTYPE: "shadow" } as NodeJS.ProcessEnv)).toBe("shadow");
197
- expect(resolveNoisePrototypeMode({ RECALLNEST_NOISE_PROTOTYPE: "off" } as NodeJS.ProcessEnv)).toBe("off");
198
- expect(resolveNoisePrototypeMode({ RECALLNEST_NOISE_PROTOTYPE: "banana" } as NodeJS.ProcessEnv)).toBe("shadow");
199
- });
200
-
201
- it("explicit deps win over the env-governed shared instance", () => {
202
- const { bank } = makeBank();
203
- expect(resolveNoisePrototypeBank(bank)).toBe(bank);
204
- expect(resolveNoisePrototypeBank(null)).toBeNull();
205
- // Under bun test (NODE_ENV=test, no explicit env) the shared bank is off.
206
- expect(resolveNoisePrototypeBank(undefined)).toBeNull();
207
- });
208
- });
209
-
210
- // --- integration through persistMemory ---
211
-
212
- function makePersistDeps(bank: NoisePrototypeBank | null) {
213
- const storeCalls: unknown[] = [];
214
- return {
215
- storeCalls,
216
- deps: {
217
- embedder: {
218
- async embedPassage() {
219
- return axisVector(0);
220
- },
221
- },
222
- store: {
223
- async store(entry: Record<string, unknown>) {
224
- storeCalls.push(entry);
225
- return { ...entry, id: `mem-${storeCalls.length}`, timestamp: Date.now() };
226
- },
227
- },
228
- noisePrototypeBank: bank,
229
- },
230
- };
231
- }
232
-
233
- describe("persistMemory × noise-prototype shadow", () => {
234
- it("noise_detected rejection feeds the bank; the write is still rejected", async () => {
235
- const { bank } = makeBank({ minSeenCount: 1 });
236
- const { deps } = makePersistDeps(bank);
237
-
238
- const result = await persistMemory(deps as never, {
239
- text: "hello world, how are you today?",
240
- category: "events",
241
- importance: 0.7,
242
- scope: "project:npb-test",
243
- source: "manual",
244
- });
245
-
246
- expect(result.disposition).toBe("rejected");
247
- expect(result.rejectionReason).toBe("noise_detected");
248
- expect(bank.list().length).toBe(1); // minSeenCount 1 → learned immediately
249
- });
250
-
251
- it("accepted writes run the shadow match against the write-path vector", async () => {
252
- const { bank, dir } = makeBank({ minSeenCount: 1 });
253
- await bank.learnFromRejection("噪声原型种子内容", embedAxis0);
254
- const { deps } = makePersistDeps(bank);
255
-
256
- const result = await persistMemory(deps as never, {
257
- text: "RecallNest 的 readConsistencyInterval 已经默认为 0",
258
- category: "events",
259
- importance: 0.7,
260
- scope: "project:npb-test",
261
- source: "manual",
262
- });
263
-
264
- expect(result.disposition).not.toBe("rejected");
265
- const log = readFileSync(join(dir, "shadow.jsonl"), "utf8");
266
- expect(log).toContain("suggest_reject"); // axis-0 fake vectors always collide
267
- });
268
-
269
- it("bank=null disables both sides completely", async () => {
270
- const { deps } = makePersistDeps(null);
271
- const result = await persistMemory(deps as never, {
272
- text: "hello world, how are you today?",
273
- category: "events",
274
- importance: 0.7,
275
- scope: "project:npb-test",
276
- source: "manual",
277
- });
278
- expect(result.disposition).toBe("rejected"); // admission unchanged
279
- });
280
- });
@@ -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
- });