recallnest 1.4.1 → 2.2.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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -1,256 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- buildGraph,
4
- pprTraverse,
5
- pprFromTriples,
6
- edgeWeight,
7
- DEFAULT_PPR_CONFIG,
8
- } from "../ppr-traversal.js";
9
- import type { KGTriple, NeighborhoodResult } from "../kg-store.js";
10
-
11
- // ============================================================================
12
- // Helpers
13
- // ============================================================================
14
-
15
- function makeTriple(
16
- subject: string,
17
- predicate: string,
18
- object: string,
19
- confidence = 0.9,
20
- id?: string,
21
- ): KGTriple {
22
- return {
23
- id: id ?? `${subject}-${predicate}-${object}`,
24
- scope: "test",
25
- subject,
26
- predicate,
27
- object,
28
- confidence,
29
- source_memory_id: `mem-${subject}-${object}`,
30
- source_text: `${subject} ${predicate} ${object}`,
31
- timestamp: Date.now(),
32
- };
33
- }
34
-
35
- function makeNeighborhood(triples: KGTriple[]): NeighborhoodResult[] {
36
- const entities = new Set<string>();
37
- for (const t of triples) {
38
- entities.add(t.subject);
39
- entities.add(t.object);
40
- }
41
- return [...entities].map(entity => ({
42
- entity,
43
- triples: triples.filter(t => t.subject === entity || t.object === entity),
44
- hops: 0,
45
- }));
46
- }
47
-
48
- // ============================================================================
49
- // edgeWeight
50
- // ============================================================================
51
-
52
- describe("edgeWeight", () => {
53
- it("returns known weights for standard predicates", () => {
54
- expect(edgeWeight("created_by")).toBe(0.95);
55
- expect(edgeWeight("uses")).toBe(0.90);
56
- expect(edgeWeight("related_to")).toBe(0.50);
57
- });
58
-
59
- it("returns default weight for unknown predicates", () => {
60
- expect(edgeWeight("unknown_predicate")).toBe(0.60);
61
- });
62
- });
63
-
64
- // ============================================================================
65
- // buildGraph
66
- // ============================================================================
67
-
68
- describe("buildGraph", () => {
69
- it("builds undirected graph from neighborhood", () => {
70
- const triples = [
71
- makeTriple("Alice", "uses", "Python"),
72
- makeTriple("Alice", "works_with", "Bob"),
73
- ];
74
- const neighborhood = makeNeighborhood(triples);
75
- const graph = buildGraph(neighborhood);
76
-
77
- expect(graph.nodes.size).toBe(3); // Alice, Python, Bob
78
- expect(graph.adj.get("Alice")?.length).toBe(2); // -> Python, -> Bob
79
- expect(graph.adj.get("Python")?.length).toBe(1); // -> Alice
80
- expect(graph.adj.get("Bob")?.length).toBe(1); // -> Alice
81
- });
82
-
83
- it("deduplicates triples by id", () => {
84
- const t = makeTriple("Alice", "uses", "Python", 0.9, "same-id");
85
- const neighborhood: NeighborhoodResult[] = [
86
- { entity: "Alice", triples: [t], hops: 0 },
87
- { entity: "Python", triples: [t], hops: 1 },
88
- ];
89
- const graph = buildGraph(neighborhood);
90
-
91
- // Should only have 1 edge per direction despite appearing in 2 neighborhood entries
92
- expect(graph.adj.get("Alice")?.length).toBe(1);
93
- expect(graph.adj.get("Python")?.length).toBe(1);
94
- });
95
-
96
- it("handles empty neighborhood", () => {
97
- const graph = buildGraph([]);
98
- expect(graph.nodes.size).toBe(0);
99
- expect(graph.adj.size).toBe(0);
100
- });
101
- });
102
-
103
- // ============================================================================
104
- // pprTraverse — convergence
105
- // ============================================================================
106
-
107
- describe("pprTraverse", () => {
108
- it("converges on a simple chain A -> B -> C", () => {
109
- const triples = [
110
- makeTriple("A", "uses", "B"),
111
- makeTriple("B", "uses", "C"),
112
- ];
113
- const graph = buildGraph(makeNeighborhood(triples));
114
- const results = pprTraverse(graph, ["A"]);
115
-
116
- // A should have highest score (it's the seed)
117
- expect(results.length).toBeGreaterThan(0);
118
- expect(results[0].entity).toBe("A");
119
-
120
- // B should score higher than C (closer to seed)
121
- const bResult = results.find(r => r.entity === "B");
122
- const cResult = results.find(r => r.entity === "C");
123
- expect(bResult).toBeDefined();
124
- expect(cResult).toBeDefined();
125
- expect(bResult!.score).toBeGreaterThan(cResult!.score);
126
- });
127
-
128
- it("respects hop limits", () => {
129
- const triples = [
130
- makeTriple("A", "uses", "B"),
131
- makeTriple("B", "uses", "C"),
132
- makeTriple("C", "uses", "D"),
133
- ];
134
- const graph = buildGraph(makeNeighborhood(triples));
135
- const results = pprTraverse(graph, ["A"], { hopLimit: 1 });
136
-
137
- // With hopLimit=1, only A and B should be reachable
138
- const entities = results.map(r => r.entity);
139
- expect(entities).toContain("A");
140
- expect(entities).toContain("B");
141
- expect(entities).not.toContain("C");
142
- expect(entities).not.toContain("D");
143
- });
144
-
145
- it("handles multiple seed entities", () => {
146
- const triples = [
147
- makeTriple("A", "uses", "C"),
148
- makeTriple("B", "uses", "C"),
149
- ];
150
- const graph = buildGraph(makeNeighborhood(triples));
151
- const results = pprTraverse(graph, ["A", "B"]);
152
-
153
- // C should be reachable from both seeds
154
- const cResult = results.find(r => r.entity === "C");
155
- expect(cResult).toBeDefined();
156
- expect(cResult!.hops).toBe(1);
157
- });
158
-
159
- it("returns empty for empty graph", () => {
160
- const graph = buildGraph([]);
161
- const results = pprTraverse(graph, ["A"]);
162
- expect(results).toEqual([]);
163
- });
164
-
165
- it("returns empty when seeds not in graph", () => {
166
- const triples = [makeTriple("X", "uses", "Y")];
167
- const graph = buildGraph(makeNeighborhood(triples));
168
- const results = pprTraverse(graph, ["NotInGraph"]);
169
- expect(results).toEqual([]);
170
- });
171
-
172
- it("respects topK limit", () => {
173
- const triples = [
174
- makeTriple("A", "uses", "B"),
175
- makeTriple("A", "uses", "C"),
176
- makeTriple("A", "uses", "D"),
177
- makeTriple("A", "uses", "E"),
178
- ];
179
- const graph = buildGraph(makeNeighborhood(triples));
180
- const results = pprTraverse(graph, ["A"], { topK: 2 });
181
- expect(results.length).toBe(2);
182
- });
183
-
184
- it("computes shortest paths correctly", () => {
185
- const triples = [
186
- makeTriple("A", "uses", "B"),
187
- makeTriple("B", "uses", "C"),
188
- ];
189
- const graph = buildGraph(makeNeighborhood(triples));
190
- const results = pprTraverse(graph, ["A"]);
191
-
192
- const aResult = results.find(r => r.entity === "A");
193
- const bResult = results.find(r => r.entity === "B");
194
- const cResult = results.find(r => r.entity === "C");
195
-
196
- expect(aResult?.hops).toBe(0);
197
- expect(aResult?.path).toEqual(["A"]);
198
- expect(bResult?.hops).toBe(1);
199
- expect(bResult?.path).toEqual(["A", "B"]);
200
- expect(cResult?.hops).toBe(2);
201
- expect(cResult?.path).toEqual(["A", "B", "C"]);
202
- });
203
-
204
- it("edge weights affect scores — stronger predicates yield higher scores", () => {
205
- // Two separate subgraphs from same seed
206
- const triples = [
207
- makeTriple("A", "created_by", "B", 1.0), // weight 0.95
208
- makeTriple("A", "related_to", "C", 1.0), // weight 0.50
209
- ];
210
- const graph = buildGraph(makeNeighborhood(triples));
211
- const results = pprTraverse(graph, ["A"]);
212
-
213
- const bResult = results.find(r => r.entity === "B");
214
- const cResult = results.find(r => r.entity === "C");
215
- expect(bResult).toBeDefined();
216
- expect(cResult).toBeDefined();
217
- // B should score higher due to stronger edge weight
218
- expect(bResult!.score).toBeGreaterThan(cResult!.score);
219
- });
220
-
221
- it("all scores sum to approximately 1.0 (probability distribution)", () => {
222
- const triples = [
223
- makeTriple("A", "uses", "B"),
224
- makeTriple("B", "uses", "C"),
225
- makeTriple("C", "uses", "A"), // cycle
226
- ];
227
- const graph = buildGraph(makeNeighborhood(triples));
228
- const results = pprTraverse(graph, ["A"], { maxIterations: 50 });
229
-
230
- const totalScore = results.reduce((sum, r) => sum + r.score, 0);
231
- // PPR scores approximate a probability distribution
232
- expect(totalScore).toBeGreaterThan(0.8);
233
- expect(totalScore).toBeLessThan(1.2);
234
- });
235
- });
236
-
237
- // ============================================================================
238
- // pprFromTriples (convenience)
239
- // ============================================================================
240
-
241
- describe("pprFromTriples", () => {
242
- it("works with raw triples", () => {
243
- const triples = [
244
- makeTriple("Alice", "uses", "Python"),
245
- makeTriple("Alice", "works_at", "Google"),
246
- ];
247
- const results = pprFromTriples(triples, ["Alice"]);
248
-
249
- expect(results.length).toBeGreaterThan(0);
250
- expect(results[0].entity).toBe("Alice");
251
-
252
- const entities = results.map(r => r.entity);
253
- expect(entities).toContain("Python");
254
- expect(entities).toContain("Google");
255
- });
256
- });
@@ -1,211 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { promoteMemory } from "../capture-engine.js";
4
- import { formatExplainResults, formatSearchResults } from "../memory-output.js";
5
- import type { RetrievalResult } from "../retriever.js";
6
-
7
- const TEST_SCOPE = "project:test";
8
-
9
- function createDeps() {
10
- const storedEntries: any[] = [];
11
- const conflicts: any[] = [];
12
- let seq = 1;
13
-
14
- return {
15
- storedEntries,
16
- conflicts,
17
- deps: {
18
- embedder: {
19
- async embedPassage(text: string) {
20
- return [text.length, 1, 0];
21
- },
22
- },
23
- store: {
24
- async store(entry: any) {
25
- const stored = {
26
- ...entry,
27
- id: `00000000-0000-0000-0000-${String(seq).padStart(12, "0")}`,
28
- timestamp: 1_700_000_000_000 + seq,
29
- };
30
- seq += 1;
31
- storedEntries.push(stored);
32
- return stored;
33
- },
34
- async list(_scopeFilter?: string[], category?: string, limit = 20, offset = 0) {
35
- return storedEntries
36
- .filter((entry) => !category || entry.category === category)
37
- .slice(offset, offset + limit);
38
- },
39
- async update(id: string, updates: any) {
40
- const index = storedEntries.findIndex((entry) => entry.id === id);
41
- if (index < 0) return null;
42
- storedEntries[index] = {
43
- ...storedEntries[index],
44
- ...updates,
45
- timestamp: updates.timestamp ?? storedEntries[index].timestamp,
46
- };
47
- return storedEntries[index];
48
- },
49
- async getById(id: string) {
50
- return storedEntries.find((entry) => entry.id === id) || null;
51
- },
52
- async get(id: string) {
53
- const exact = storedEntries.find((entry) => entry.id === id);
54
- if (exact) return exact;
55
- const matches = storedEntries.filter((entry) => entry.id.startsWith(id));
56
- if (matches.length > 1) {
57
- throw new Error(`Ambiguous prefix "${id}" matches ${matches.length} memories. Use a longer prefix or full ID.`);
58
- }
59
- return matches[0] || null;
60
- },
61
- },
62
- conflictStore: {
63
- async save(record: any) {
64
- conflicts.push(record);
65
- return record;
66
- },
67
- async replace(record: any) {
68
- const index = conflicts.findIndex((item) => item.conflictId === record.conflictId);
69
- if (index >= 0) {
70
- conflicts[index] = record;
71
- } else {
72
- conflicts.push(record);
73
- }
74
- return record;
75
- },
76
- async getOpenByFingerprint(fingerprint: string) {
77
- return conflicts.find((item) => item.status === "open" && item.fingerprint === fingerprint) || null;
78
- },
79
- async getLatestByFingerprint(fingerprint: string) {
80
- return conflicts.find((item) => item.fingerprint === fingerprint) || null;
81
- },
82
- },
83
- },
84
- };
85
- }
86
-
87
- function buildEvidenceEntry(
88
- deps: ReturnType<typeof createDeps>["deps"],
89
- text: string,
90
- scope: string,
91
- ) {
92
- return deps.store.store({
93
- text,
94
- vector: [1, 2, 3],
95
- category: "events",
96
- scope,
97
- importance: 0.55,
98
- metadata: JSON.stringify({
99
- source: "cc",
100
- boundary: {
101
- layer: "evidence",
102
- authority: "transcript-ingest",
103
- conflictPolicy: "append-only",
104
- originalCategory: "preferences",
105
- },
106
- }),
107
- });
108
- }
109
-
110
- function toResult(entry: any, score = 0.91): RetrievalResult {
111
- return {
112
- entry,
113
- score,
114
- sources: {
115
- vector: { score: 0.9, rank: 1 },
116
- bm25: { score: 0.8, rank: 2 },
117
- fused: { score },
118
- },
119
- };
120
- }
121
-
122
- describe("preference guard smoke", () => {
123
- it("renders slot provenance end-to-end for a promoted reply-style preference", async () => {
124
- const { deps, storedEntries, conflicts } = createDeps();
125
- const source = await buildEvidenceEntry(
126
- deps,
127
- "[用户] 用户偏好短句直说。",
128
- "cc:session-reply-style-smoke",
129
- );
130
-
131
- const promoted = await promoteMemory(deps as any, {
132
- memoryId: source.id,
133
- text: "User prefers concise, direct replies.",
134
- category: "preferences",
135
- scope: TEST_SCOPE,
136
- tags: ["writing"],
137
- });
138
-
139
- const entry = storedEntries.find((item) => item.id === promoted.id);
140
- expect(entry).toBeDefined();
141
-
142
- const searchOutput = formatSearchResults([toResult(entry)], {
143
- query: "reply style",
144
- profile: "default",
145
- });
146
- const explainOutput = formatExplainResults([toResult(entry)], {
147
- query: "reply style",
148
- profile: "default",
149
- });
150
-
151
- expect(searchOutput).toContain("key:preferences:reply-style:concise:direct");
152
- expect(searchOutput).toContain("slot:reply-style:concise:direct");
153
- expect(searchOutput).toContain(`promoted:${source.id.slice(0, 8)}<-evidence/transcript-ingest`);
154
- expect(explainOutput).toContain("slot:reply-style:concise:direct");
155
- expect(explainOutput).toContain(`promoted:${source.id.slice(0, 8)}<-evidence/transcript-ingest`);
156
- expect(conflicts).toHaveLength(0);
157
- });
158
-
159
- it("keeps descriptive draft and migration notes on plain preferences keys end-to-end", async () => {
160
- const { deps, storedEntries, conflicts } = createDeps();
161
- const draftSource = await buildEvidenceEntry(
162
- deps,
163
- "[用户] 这段文案简洁直接,先别改。",
164
- "cc:session-draft-note-smoke",
165
- );
166
- const migrationSource = await buildEvidenceEntry(
167
- deps,
168
- "[用户] 文档里写了 uses Bun over Node 的迁移说明。",
169
- "cc:session-migration-note-smoke",
170
- );
171
-
172
- const promotedDraft = await promoteMemory(deps as any, {
173
- memoryId: draftSource.id,
174
- text: "这段文案简洁直接,先别改。",
175
- category: "preferences",
176
- scope: TEST_SCOPE,
177
- tags: ["writing"],
178
- });
179
- const promotedMigration = await promoteMemory(deps as any, {
180
- memoryId: migrationSource.id,
181
- text: "文档里写了 uses Bun over Node 的迁移说明。",
182
- category: "preferences",
183
- scope: TEST_SCOPE,
184
- tags: ["tooling"],
185
- });
186
-
187
- const results = [
188
- storedEntries.find((item) => item.id === promotedDraft.id),
189
- storedEntries.find((item) => item.id === promotedMigration.id),
190
- ].filter(Boolean).map((entry) => toResult(entry));
191
-
192
- const searchOutput = formatSearchResults(results, {
193
- query: "preferences",
194
- profile: "default",
195
- });
196
- const explainOutput = formatExplainResults(results, {
197
- query: "preferences",
198
- profile: "default",
199
- });
200
-
201
- expect(searchOutput).toContain("key:preferences:这段文案简洁直接-先别改");
202
- expect(searchOutput).toContain("key:preferences:文档里写了-uses-bun-over-node-的迁移说明");
203
- expect(searchOutput).not.toContain("slot:reply-style:");
204
- expect(searchOutput).not.toContain("slot:tool-choice:");
205
- expect(explainOutput).toContain("key:preferences:这段文案简洁直接-先别改");
206
- expect(explainOutput).toContain("key:preferences:文档里写了-uses-bun-over-node-的迁移说明");
207
- expect(explainOutput).not.toContain("slot:reply-style:");
208
- expect(explainOutput).not.toContain("slot:tool-choice:");
209
- expect(conflicts).toHaveLength(0);
210
- });
211
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * Tests for Tier 3.6: Preference Pattern Quick Matching
3
- *
4
- * Validates:
5
- * 1. matchPreference returns "create" when no similar preferences exist
6
- * 2. matchPreference returns "skip" for near-duplicate preferences (no LLM)
7
- * 3. matchPreference uses LLM when available for decision
8
- * 4. matchPreference returns "merge" with merged text from LLM
9
- * 5. matchPreference handles LLM failure gracefully
10
- * 6. applyPreferenceMatch handles merge and skip correctly
11
- * 7. Non-preference categories are not filtered
12
- */
13
- import { describe, expect, it } from "bun:test";
14
- import {
15
- matchPreference,
16
- applyPreferenceMatch,
17
- DEFAULT_PREFERENCE_MATCHER_CONFIG,
18
- } from "../preference-matcher.js";
19
- import type { MemoryEntry, MemorySearchResult } from "../store.js";
20
- import type { LLMClient, DedupDecision } from "../llm-client.js";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
-
26
- function makeEntry(id: string, text: string, category = "preferences"): MemoryEntry {
27
- return {
28
- id,
29
- text,
30
- vector: [1, 0, 0],
31
- category,
32
- scope: "project:test",
33
- importance: 0.7,
34
- timestamp: Date.now(),
35
- metadata: "{}",
36
- };
37
- }
38
-
39
- function createMockStore(searchResults: MemorySearchResult[] = []) {
40
- const updates: Array<{ id: string; text?: string }> = [];
41
- const data = new Map<string, MemoryEntry>();
42
- for (const r of searchResults) {
43
- data.set(r.entry.id, { ...r.entry });
44
- }
45
-
46
- return {
47
- updates,
48
- data,
49
- async vectorSearch(
50
- _vector: number[],
51
- _limit: number,
52
- _minScore: number,
53
- _scopeFilter?: string[],
54
- ) {
55
- return searchResults;
56
- },
57
- async update(id: string, upd: { text?: string; metadata?: string }, _scope?: string[]) {
58
- const entry = data.get(id);
59
- if (!entry) return null;
60
- if (upd.text) {
61
- entry.text = upd.text;
62
- updates.push({ id, text: upd.text });
63
- }
64
- return entry;
65
- },
66
- };
67
- }
68
-
69
- function createMockLLM(dedupResult: DedupDecision, synthResult: string | null = "merged preference text"): LLMClient {
70
- return {
71
- async dedupDecision(_newText: string, _existingText: string) {
72
- return dedupResult;
73
- },
74
- async synthesizeFragments(_fragments: string[], _query: string) {
75
- return synthResult;
76
- },
77
- } as any;
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // matchPreference tests
82
- // ---------------------------------------------------------------------------
83
-
84
- describe("matchPreference", () => {
85
- const vector = [1, 0, 0];
86
- const scope = "project:test";
87
-
88
- it("returns create when no similar preferences found", async () => {
89
- const store = createMockStore([]);
90
- const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
91
-
92
- expect(result.action).toBe("create");
93
- expect(result.reason).toBe("no-similar-preferences");
94
- });
95
-
96
- it("returns create when matches are non-preference category", async () => {
97
- const store = createMockStore([
98
- { entry: makeEntry("e1", "some entity", "entities"), score: 0.85 },
99
- ]);
100
- const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
101
-
102
- expect(result.action).toBe("create");
103
- expect(result.reason).toBe("no-similar-preferences");
104
- });
105
-
106
- it("returns skip for high similarity without LLM", async () => {
107
- const store = createMockStore([
108
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.95 },
109
- ]);
110
- const result = await matchPreference("I like dark mode", vector, scope, store as any, null);
111
-
112
- expect(result.action).toBe("skip");
113
- expect(result.reason).toContain("no-llm");
114
- });
115
-
116
- it("returns create for moderate similarity without LLM", async () => {
117
- const store = createMockStore([
118
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.82 },
119
- ]);
120
- const result = await matchPreference("I like vim keybindings", vector, scope, store as any, null);
121
-
122
- expect(result.action).toBe("create");
123
- expect(result.reason).toContain("no-llm");
124
- });
125
-
126
- it("uses LLM to decide SKIP", async () => {
127
- const store = createMockStore([
128
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
129
- ]);
130
- const llm = createMockLLM({ action: "SKIP", reason: "same preference" });
131
-
132
- const result = await matchPreference("I like dark mode", vector, scope, store as any, llm);
133
-
134
- expect(result.action).toBe("skip");
135
- expect(result.reason).toContain("llm-skip");
136
- });
137
-
138
- it("uses LLM to decide MERGE and generates merged text", async () => {
139
- const store = createMockStore([
140
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
141
- ]);
142
- const llm = createMockLLM(
143
- { action: "MERGE", reason: "compatible preferences" },
144
- "User prefers dark mode and compact layout",
145
- );
146
-
147
- const result = await matchPreference("I also like compact layout", vector, scope, store as any, llm);
148
-
149
- expect(result.action).toBe("merge");
150
- expect(result.mergeTargetId).toBe("p1");
151
- expect(result.mergedText).toBe("User prefers dark mode and compact layout");
152
- });
153
-
154
- it("uses LLM to decide CREATE", async () => {
155
- const store = createMockStore([
156
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.80 },
157
- ]);
158
- const llm = createMockLLM({ action: "CREATE", reason: "different preference" });
159
-
160
- const result = await matchPreference("I prefer vim keybindings", vector, scope, store as any, llm);
161
-
162
- expect(result.action).toBe("create");
163
- expect(result.reason).toContain("llm-create");
164
- });
165
-
166
- it("falls back to create on LLM failure", async () => {
167
- const store = createMockStore([
168
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
169
- ]);
170
- const llm = {
171
- async dedupDecision() { throw new Error("LLM down"); },
172
- async synthesizeFragments() { return null; },
173
- } as any;
174
-
175
- const result = await matchPreference("test pref", vector, scope, store as any, llm);
176
-
177
- expect(result.action).toBe("create");
178
- expect(result.reason).toBe("llm-error");
179
- });
180
- });
181
-
182
- // ---------------------------------------------------------------------------
183
- // applyPreferenceMatch tests
184
- // ---------------------------------------------------------------------------
185
-
186
- describe("applyPreferenceMatch", () => {
187
- it("handles skip action", async () => {
188
- const store = createMockStore([]);
189
- const result = await applyPreferenceMatch(
190
- { action: "skip", reason: "duplicate" },
191
- store as any,
192
- "project:test",
193
- );
194
-
195
- expect(result.handled).toBe(true);
196
- expect(result.entry).toBeUndefined();
197
- });
198
-
199
- it("handles merge action — updates existing entry text", async () => {
200
- const existing = makeEntry("p1", "old preference text");
201
- const store = createMockStore([{ entry: existing, score: 0.9 }]);
202
-
203
- const result = await applyPreferenceMatch(
204
- {
205
- action: "merge",
206
- mergeTargetId: "p1",
207
- mergedText: "merged preference text",
208
- reason: "llm-merge",
209
- },
210
- store as any,
211
- "project:test",
212
- );
213
-
214
- expect(result.handled).toBe(true);
215
- expect(result.entry).toBeDefined();
216
- expect(store.updates.length).toBe(1);
217
- expect(store.updates[0].text).toBe("merged preference text");
218
- });
219
-
220
- it("returns not handled for create action", async () => {
221
- const store = createMockStore([]);
222
- const result = await applyPreferenceMatch(
223
- { action: "create", reason: "new" },
224
- store as any,
225
- "project:test",
226
- );
227
-
228
- expect(result.handled).toBe(false);
229
- });
230
- });