recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,230 @@
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
+ });
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ inferPreferenceSlot,
5
+ inferAtomicBrandItemPreferenceSlot,
6
+ parseBrandItemPreference,
7
+ inferReplyStylePreferenceSlot,
8
+ inferToolChoicePreferenceSlot,
9
+ inferImplicitUsageSlot,
10
+ samePreferenceSlot,
11
+ } from "../preference-slots.js";
12
+
13
+ describe("preference slots", () => {
14
+ it("parses Chinese and English brand-item preferences", () => {
15
+ expect(parseBrandItemPreference("我喜欢喝星巴克的抹茶拿铁")).toEqual({
16
+ brand: "星巴克",
17
+ items: ["抹茶拿铁"],
18
+ aggregate: false,
19
+ });
20
+
21
+ expect(parseBrandItemPreference("我喜欢吃麦当劳的麦旋风、板烧鸡腿堡和藤椒鸡派")).toEqual({
22
+ brand: "麦当劳",
23
+ items: ["麦旋风", "板烧鸡腿堡", "藤椒鸡派"],
24
+ aggregate: true,
25
+ });
26
+
27
+ expect(parseBrandItemPreference("I like the Big Mac from McDonald's")).toEqual({
28
+ brand: "mcdonald's",
29
+ items: ["bigmac"],
30
+ aggregate: false,
31
+ });
32
+
33
+ expect(parseBrandItemPreference("我们刚讨论过星巴克的抹茶拿铁做法")).toBeNull();
34
+ });
35
+
36
+ it("infers atomic brand-item slots and skips aggregate preferences", () => {
37
+ expect(inferAtomicBrandItemPreferenceSlot("我喜欢喝星巴克的抹茶拿铁")).toEqual({
38
+ type: "brand-item",
39
+ brand: "星巴克",
40
+ item: "抹茶拿铁",
41
+ });
42
+
43
+ expect(inferAtomicBrandItemPreferenceSlot("I like Big Mac from McDonald's")).toEqual({
44
+ type: "brand-item",
45
+ brand: "mcdonald's",
46
+ item: "bigmac",
47
+ });
48
+
49
+ expect(samePreferenceSlot(
50
+ inferAtomicBrandItemPreferenceSlot("I like the Big Mac from McDonald's"),
51
+ inferAtomicBrandItemPreferenceSlot("I like Big Mac from McDonald's"),
52
+ )).toBe(true);
53
+
54
+ expect(inferAtomicBrandItemPreferenceSlot("我喜欢吃麦当劳的麦旋风、板烧鸡腿堡")).toBeNull();
55
+ });
56
+
57
+ it("infers reply-style traits from explicit reply-style preferences", () => {
58
+ expect(inferReplyStylePreferenceSlot("User prefers concise, direct replies.")).toEqual({
59
+ type: "reply-style",
60
+ traits: ["concise", "direct"],
61
+ });
62
+
63
+ expect(inferReplyStylePreferenceSlot("用户不接受浮夸/营销腔,语气要口语化、不端着。")).toEqual({
64
+ type: "reply-style",
65
+ traits: ["colloquial", "grounded"],
66
+ });
67
+ });
68
+
69
+ it("can infer reply-style slots from compact trait phrases", () => {
70
+ expect(inferPreferenceSlot("用户偏好短句直说。")).toEqual({
71
+ type: "reply-style",
72
+ traits: ["concise", "direct"],
73
+ });
74
+ });
75
+
76
+ it("ignores descriptive non-preference text for reply-style and tool-choice parsing", () => {
77
+ expect(inferReplyStylePreferenceSlot("这段文案简洁直接,先别改。")).toBeNull();
78
+ expect(inferReplyStylePreferenceSlot("这段文案挺口语化,先别改。")).toBeNull();
79
+ expect(inferToolChoicePreferenceSlot("文档里写了 uses Bun over Node 的迁移说明。")).toBeNull();
80
+ });
81
+
82
+ it("compares reply-style slots by normalized trait sets", () => {
83
+ expect(samePreferenceSlot(
84
+ inferReplyStylePreferenceSlot("User prefers concise, direct replies."),
85
+ inferReplyStylePreferenceSlot("User prefers direct concise responses."),
86
+ )).toBe(true);
87
+
88
+ expect(samePreferenceSlot(
89
+ inferReplyStylePreferenceSlot("User prefers concise, direct replies."),
90
+ inferReplyStylePreferenceSlot("User prefers colloquial grounded replies."),
91
+ )).toBe(false);
92
+ });
93
+
94
+ it("infers tool-choice slots from explicit comparative preferences", () => {
95
+ expect(inferToolChoicePreferenceSlot("Uses Bun over Node.")).toEqual({
96
+ type: "tool-choice",
97
+ preferredTool: "bun",
98
+ avoidedTool: "node",
99
+ });
100
+
101
+ expect(inferToolChoicePreferenceSlot("更喜欢用 rg 而不是 grep")).toEqual({
102
+ type: "tool-choice",
103
+ preferredTool: "rg",
104
+ avoidedTool: "grep",
105
+ });
106
+ });
107
+
108
+ it("detects implicit usage preferences from 'I use X' patterns", () => {
109
+ expect(inferImplicitUsageSlot("I use Adobe Premiere Pro for video editing.")).toEqual({
110
+ type: "implicit-usage",
111
+ subject: "adobepremierepro",
112
+ });
113
+
114
+ expect(inferImplicitUsageSlot("User uses Python for data analysis.")).toEqual({
115
+ type: "implicit-usage",
116
+ subject: "python",
117
+ });
118
+
119
+ expect(inferImplicitUsageSlot("I have been using VS Code.")).toEqual({
120
+ type: "implicit-usage",
121
+ subject: "vscode",
122
+ });
123
+ });
124
+
125
+ it("detects implicit ownership preferences from 'I have X' patterns", () => {
126
+ expect(inferImplicitUsageSlot("I have a Sony A7III camera")).toEqual({
127
+ type: "implicit-usage",
128
+ subject: "sonya7iii",
129
+ });
130
+
131
+ expect(inferImplicitUsageSlot("User bought a Canon EOS R5 camera")).toEqual({
132
+ type: "implicit-usage",
133
+ subject: "canoneosr5",
134
+ });
135
+ });
136
+
137
+ it("detects implicit activity preferences from 'I enjoy X' patterns", () => {
138
+ expect(inferImplicitUsageSlot("I enjoy hiking.")).toEqual({
139
+ type: "implicit-usage",
140
+ subject: "hiking",
141
+ });
142
+
143
+ expect(inferImplicitUsageSlot("User loves ocean views.")).toEqual({
144
+ type: "implicit-usage",
145
+ subject: "oceanviews",
146
+ });
147
+
148
+ expect(inferImplicitUsageSlot("I really like photography.")).toEqual({
149
+ type: "implicit-usage",
150
+ subject: "photography",
151
+ });
152
+ });
153
+
154
+ it("detects implicit interest preferences from 'I'm learning X' patterns", () => {
155
+ expect(inferImplicitUsageSlot("I'm learning Spanish.")).toEqual({
156
+ type: "implicit-usage",
157
+ subject: "spanish",
158
+ });
159
+
160
+ expect(inferImplicitUsageSlot("User is interested in machine learning.")).toEqual({
161
+ type: "implicit-usage",
162
+ subject: "machinelearning",
163
+ });
164
+ });
165
+
166
+ it("detects Chinese implicit usage preferences", () => {
167
+ expect(inferImplicitUsageSlot("我在用Figma做设计。")).toEqual({
168
+ type: "implicit-usage",
169
+ subject: "figma",
170
+ });
171
+
172
+ expect(inferImplicitUsageSlot("我在学西班牙语。")).toEqual({
173
+ type: "implicit-usage",
174
+ subject: "西班牙语",
175
+ });
176
+ });
177
+
178
+ it("rejects short/empty implicit usage subjects", () => {
179
+ // "I use it" → subject "it" normalizes to 2 chars — at threshold
180
+ expect(inferImplicitUsageSlot("I use a.")).toBeNull();
181
+ });
182
+
183
+ it("explicit patterns take priority over implicit in inferPreferenceSlot", () => {
184
+ // "I like Big Mac from McDonald's" → brand-item, not implicit-usage
185
+ expect(inferPreferenceSlot("I like Big Mac from McDonald's")?.type).toBe("brand-item");
186
+
187
+ // "Uses Bun over Node." → tool-choice, not implicit-usage
188
+ expect(inferPreferenceSlot("Uses Bun over Node.")?.type).toBe("tool-choice");
189
+
190
+ // "I use Adobe Premiere Pro for video editing." → implicit-usage (no explicit pattern matches)
191
+ expect(inferPreferenceSlot("I use Adobe Premiere Pro for video editing.")?.type).toBe("implicit-usage");
192
+ });
193
+
194
+ it("compares implicit-usage slots by normalized subject", () => {
195
+ expect(samePreferenceSlot(
196
+ inferImplicitUsageSlot("I use Adobe Premiere Pro."),
197
+ inferImplicitUsageSlot("User uses Adobe Premiere Pro for editing."),
198
+ )).toBe(true);
199
+
200
+ expect(samePreferenceSlot(
201
+ inferImplicitUsageSlot("I use Premiere Pro."),
202
+ inferImplicitUsageSlot("I use DaVinci Resolve."),
203
+ )).toBe(false);
204
+ });
205
+ });