recallnest 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,282 +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
- import { parseEvolution } from "../memory-evolution.js";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Helpers
25
- // ---------------------------------------------------------------------------
26
-
27
- function makeEntry(id: string, text: string, category = "preferences"): MemoryEntry {
28
- return {
29
- id,
30
- text,
31
- vector: [1, 0, 0],
32
- category,
33
- scope: "project:test",
34
- importance: 0.7,
35
- timestamp: Date.now(),
36
- metadata: "{}",
37
- };
38
- }
39
-
40
- function createMockStore(searchResults: MemorySearchResult[] = []) {
41
- const updates: Array<{ id: string; text?: string; metadata?: string }> = [];
42
- const archived: MemoryEntry[] = [];
43
- const data = new Map<string, MemoryEntry>();
44
- for (const r of searchResults) {
45
- data.set(r.entry.id, { ...r.entry });
46
- }
47
-
48
- return {
49
- updates,
50
- archived,
51
- data,
52
- async vectorSearch(
53
- _vector: number[],
54
- _limit: number,
55
- _minScore: number,
56
- _scopeFilter?: string[],
57
- ) {
58
- return searchResults;
59
- },
60
- async getById(id: string) {
61
- return data.get(id) ?? null;
62
- },
63
- // Belief-history rows land here rather than in `data` — merging must archive the
64
- // version it replaces, so tests need to see what was written.
65
- async upsert(entry: MemoryEntry) {
66
- archived.push(entry);
67
- return entry;
68
- },
69
- async store(entry: MemoryEntry) {
70
- archived.push(entry);
71
- return entry;
72
- },
73
- async update(id: string, upd: { text?: string; metadata?: string }, _scope?: string[]) {
74
- const entry = data.get(id);
75
- if (!entry) return null;
76
- if (upd.text) {
77
- entry.text = upd.text;
78
- }
79
- if (upd.metadata) {
80
- entry.metadata = upd.metadata;
81
- }
82
- updates.push({ id, text: upd.text, metadata: upd.metadata });
83
- return entry;
84
- },
85
- };
86
- }
87
-
88
- const mockDeps = {
89
- embedder: {
90
- async embedPassage(text: string) {
91
- return [text.length, 1, 0];
92
- },
93
- },
94
- };
95
-
96
- function createMockLLM(dedupResult: DedupDecision, synthResult: string | null = "merged preference text"): LLMClient {
97
- return {
98
- async dedupDecision(_newText: string, _existingText: string) {
99
- return dedupResult;
100
- },
101
- async synthesizeFragments(_fragments: string[], _query: string) {
102
- return synthResult;
103
- },
104
- } as any;
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // matchPreference tests
109
- // ---------------------------------------------------------------------------
110
-
111
- describe("matchPreference", () => {
112
- const vector = [1, 0, 0];
113
- const scope = "project:test";
114
-
115
- it("returns create when no similar preferences found", async () => {
116
- const store = createMockStore([]);
117
- const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
118
-
119
- expect(result.action).toBe("create");
120
- expect(result.reason).toBe("no-similar-preferences");
121
- });
122
-
123
- it("ignores superseded belief-history rows as match candidates", async () => {
124
- // store.vectorSearch() filters by scope only, so archived versions of a preference
125
- // surface here alongside live ones. Matching against one would drop the incoming
126
- // preference as a duplicate of a belief that is no longer held.
127
- const archivedRow = makeEntry("p-old", "I prefer dark mode");
128
- archivedRow.metadata = JSON.stringify({
129
- evolution: { status: "superseded", validUntil: Date.now(), supersededBy: "p-live" },
130
- });
131
- const store = createMockStore([{ entry: archivedRow, score: 0.99 }]);
132
-
133
- const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
134
-
135
- expect(result.action).toBe("create");
136
- expect(result.reason).toBe("no-similar-preferences");
137
- });
138
-
139
- it("returns create when matches are non-preference category", async () => {
140
- const store = createMockStore([
141
- { entry: makeEntry("e1", "some entity", "entities"), score: 0.85 },
142
- ]);
143
- const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
144
-
145
- expect(result.action).toBe("create");
146
- expect(result.reason).toBe("no-similar-preferences");
147
- });
148
-
149
- it("returns skip for high similarity without LLM", async () => {
150
- const store = createMockStore([
151
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.95 },
152
- ]);
153
- const result = await matchPreference("I like dark mode", vector, scope, store as any, null);
154
-
155
- expect(result.action).toBe("skip");
156
- expect(result.reason).toContain("no-llm");
157
- });
158
-
159
- it("returns create for moderate similarity without LLM", async () => {
160
- const store = createMockStore([
161
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.82 },
162
- ]);
163
- const result = await matchPreference("I like vim keybindings", vector, scope, store as any, null);
164
-
165
- expect(result.action).toBe("create");
166
- expect(result.reason).toContain("no-llm");
167
- });
168
-
169
- it("uses LLM to decide SKIP", async () => {
170
- const store = createMockStore([
171
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
172
- ]);
173
- const llm = createMockLLM({ action: "SKIP", reason: "same preference" });
174
-
175
- const result = await matchPreference("I like dark mode", vector, scope, store as any, llm);
176
-
177
- expect(result.action).toBe("skip");
178
- expect(result.reason).toContain("llm-skip");
179
- });
180
-
181
- it("uses LLM to decide MERGE and generates merged text", async () => {
182
- const store = createMockStore([
183
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
184
- ]);
185
- const llm = createMockLLM(
186
- { action: "MERGE", reason: "compatible preferences" },
187
- "User prefers dark mode and compact layout",
188
- );
189
-
190
- const result = await matchPreference("I also like compact layout", vector, scope, store as any, llm);
191
-
192
- expect(result.action).toBe("merge");
193
- expect(result.mergeTargetId).toBe("p1");
194
- expect(result.mergedText).toBe("User prefers dark mode and compact layout");
195
- });
196
-
197
- it("uses LLM to decide CREATE", async () => {
198
- const store = createMockStore([
199
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.80 },
200
- ]);
201
- const llm = createMockLLM({ action: "CREATE", reason: "different preference" });
202
-
203
- const result = await matchPreference("I prefer vim keybindings", vector, scope, store as any, llm);
204
-
205
- expect(result.action).toBe("create");
206
- expect(result.reason).toContain("llm-create");
207
- });
208
-
209
- it("falls back to create on LLM failure", async () => {
210
- const store = createMockStore([
211
- { entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
212
- ]);
213
- const llm = {
214
- async dedupDecision() { throw new Error("LLM down"); },
215
- async synthesizeFragments() { return null; },
216
- } as any;
217
-
218
- const result = await matchPreference("test pref", vector, scope, store as any, llm);
219
-
220
- expect(result.action).toBe("create");
221
- expect(result.reason).toBe("llm-error");
222
- });
223
- });
224
-
225
- // ---------------------------------------------------------------------------
226
- // applyPreferenceMatch tests
227
- // ---------------------------------------------------------------------------
228
-
229
- describe("applyPreferenceMatch", () => {
230
- it("handles skip action", async () => {
231
- const store = createMockStore([]);
232
- const result = await applyPreferenceMatch(
233
- { action: "skip", reason: "duplicate" },
234
- store as any,
235
- "project:test",
236
- mockDeps,
237
- );
238
-
239
- expect(result.handled).toBe(true);
240
- expect(result.entry).toBeUndefined();
241
- });
242
-
243
- it("handles merge action — updates existing entry text", async () => {
244
- const existing = makeEntry("p1", "old preference text");
245
- const store = createMockStore([{ entry: existing, score: 0.9 }]);
246
-
247
- const result = await applyPreferenceMatch(
248
- {
249
- action: "merge",
250
- mergeTargetId: "p1",
251
- mergedText: "merged preference text",
252
- reason: "llm-merge",
253
- },
254
- store as any,
255
- "project:test",
256
- mockDeps,
257
- );
258
-
259
- expect(result.handled).toBe(true);
260
- expect(result.entry).toBeDefined();
261
- expect(store.updates.length).toBe(1);
262
- expect(store.updates[0].text).toBe("merged preference text");
263
-
264
- // Merging replaces the target's wording — the pre-merge version must survive.
265
- expect(store.archived).toHaveLength(1);
266
- expect(store.archived[0].text).toBe("old preference text");
267
- expect(parseEvolution(store.archived[0].metadata).status).toBe("superseded");
268
- expect(parseEvolution(store.updates[0].metadata).supersedes).toBe(store.archived[0].id);
269
- });
270
-
271
- it("returns not handled for create action", async () => {
272
- const store = createMockStore([]);
273
- const result = await applyPreferenceMatch(
274
- { action: "create", reason: "new" },
275
- store as any,
276
- "project:test",
277
- mockDeps,
278
- );
279
-
280
- expect(result.handled).toBe(false);
281
- });
282
- });
@@ -1,205 +0,0 @@
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
- });
@@ -1,162 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- countDistinctSources,
5
- extractSourceKey,
6
- judgeDistinctSources,
7
- scanForPromotions,
8
- UNKNOWN_SOURCE_PREFIX,
9
- } from "../skill-promotion.js";
10
- import type { MemoryEntry, MemorySearchResult } from "../store.js";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Mock(与 skill-promotion.test.ts 同款生产语义:listPage 恒无向量,靠 getVectors 回填)
14
- // ---------------------------------------------------------------------------
15
-
16
- let seq = 0;
17
- function makeCase(srcTag: string | null, sim = 1.0, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
18
- seq += 1;
19
- const complement = Math.sqrt(1 - sim * sim);
20
- return {
21
- id: `c-${String(seq).padStart(4, "0")}`,
22
- text: `Docker 部署时缺系统库导致构建失败 #${seq}`,
23
- vector: [sim, complement, 0],
24
- category: "cases",
25
- scope: "memory:pivot",
26
- importance: 0.7,
27
- timestamp: Date.UTC(2026, 7, 20) + seq * 1000,
28
- metadata: JSON.stringify({
29
- ...(srcTag ? { tags: [srcTag, "pinned"] } : {}),
30
- evolution: {
31
- status: "active", version: 1, accessCount: 0, lastAccessedAt: null,
32
- supersededBy: null, consolidatedInto: null, sourceMemories: [],
33
- validFrom: Date.now(), validUntil: null,
34
- },
35
- }),
36
- ...overrides,
37
- };
38
- }
39
-
40
- function createMockStore(entries: MemoryEntry[]) {
41
- return {
42
- async listPage(opts: { category?: string; limit?: number; offset?: number } = {}): Promise<MemoryEntry[]> {
43
- const { category, limit = 1000, offset = 0 } = opts;
44
- const filtered = category ? entries.filter(e => e.category === category) : entries;
45
- return filtered.slice(offset, offset + limit).map(e => ({ ...e, vector: [] }));
46
- },
47
- async getVectors(ids: string[]): Promise<Map<string, number[]>> {
48
- const m = new Map<string, number[]>();
49
- for (const id of ids) {
50
- const e = entries.find(x => x.id === id);
51
- if (e?.vector?.length) m.set(id, e.vector);
52
- }
53
- return m;
54
- },
55
- async vectorSearch(): Promise<MemorySearchResult[]> { return []; },
56
- };
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // 来源识别
61
- // ---------------------------------------------------------------------------
62
-
63
- describe("source identity", () => {
64
- it("prefers evidenceMemories over the src: tag", () => {
65
- const key = extractSourceKey({
66
- metadata: JSON.stringify({ evidenceMemories: ["m2", "m1"], tags: ["src:aaaa1111"] }),
67
- scope: "memory:pivot",
68
- timestamp: Date.now(),
69
- });
70
- // 同一批源不管顺序都是同一个来源
71
- expect(key).toBe("ev:m1|m2");
72
- });
73
-
74
- it("falls back to the src: tag that §5.3 requires on pivot writes", () => {
75
- const key = extractSourceKey({
76
- metadata: JSON.stringify({ tags: ["pinned", "src:cac4f155", "2026-08-23"] }),
77
- scope: "memory:pivot",
78
- timestamp: Date.now(),
79
- });
80
- expect(key).toBe("src:cac4f155");
81
- });
82
-
83
- it("marks the day-bucket fallback as unknown so callers can abstain", () => {
84
- const key = extractSourceKey({ metadata: "{}", scope: "project:x", timestamp: Date.UTC(2026, 7, 23) });
85
- expect(key.startsWith(UNKNOWN_SOURCE_PREFIX)).toBe(true);
86
- expect(key).toBe("day:project:x:2026-08-23");
87
- });
88
-
89
- it("counts three records from one session as one source", () => {
90
- const members = [1, 2, 3].map(() => makeCase("src:aaaa1111"));
91
- expect(countDistinctSources(members)).toBe(1);
92
- });
93
- });
94
-
95
- describe("judgeDistinctSources", () => {
96
- it("rejects a cluster that is one experience repeated", () => {
97
- const verdict = judgeDistinctSources([1, 2, 3].map(() => makeCase("src:aaaa1111")), 2);
98
- expect(verdict.status).toBe("rejected");
99
- });
100
-
101
- it("accepts a cluster spanning two different sessions", () => {
102
- const verdict = judgeDistinctSources(
103
- [makeCase("src:aaaa1111"), makeCase("src:aaaa1111"), makeCase("src:bbbb2222")],
104
- 2,
105
- );
106
- expect(verdict).toEqual({ status: "ok", distinctSources: 2 });
107
- });
108
-
109
- it("abstains — never rejects — when no record carries a real source pointer", () => {
110
- // 全库 cases 只有 1.2% 带 src:,一律硬判会把「同一天解决的三个不同问题」误伤掉。
111
- expect(judgeDistinctSources([1, 2, 3].map(() => makeCase(null)), 2)).toEqual({ status: "abstained" });
112
- });
113
-
114
- it("is disabled at minDistinctSources=1 (the pre-P2 behaviour)", () => {
115
- const verdict = judgeDistinctSources([1, 2, 3].map(() => makeCase("src:aaaa1111")), 1);
116
- expect(verdict.status).toBe("ok");
117
- });
118
- });
119
-
120
- // ---------------------------------------------------------------------------
121
- // 改前 / 改后对照 —— 「同一个坑踩三次」的具体例子
122
- // ---------------------------------------------------------------------------
123
-
124
- describe("promotion scan: same pothole three times vs two real tasks", () => {
125
- it("promotes it under the old rule and rejects it under the new one", async () => {
126
- const samePothole = [1, 2, 3].map(() => makeCase("src:aaaa1111", 1.0));
127
- const store = createMockStore(samePothole);
128
-
129
- const before = await scanForPromotions(store as never, "memory:pivot", { minDistinctSources: 1 });
130
- expect(before.candidates.filter(c => c.type === "case_to_pattern").length).toBe(1);
131
- expect(before.rejectedSingleSource).toBe(0);
132
-
133
- const after = await scanForPromotions(store as never, "memory:pivot", { minDistinctSources: 2 });
134
- expect(after.candidates.filter(c => c.type === "case_to_pattern").length).toBe(0);
135
- expect(after.rejectedSingleSource).toBe(1);
136
- });
137
-
138
- it("keeps promoting a cluster that really spans different tasks", async () => {
139
- // 审计原话:两个完全不同的任务(Alpine 装 lxml vs Docker 部署 Django)撞上同类子问题
140
- // (容器缺系统库),才诱导出一条可迁移的规律。
141
- const crossTask = [
142
- makeCase("src:aaaa1111", 1.0),
143
- makeCase("src:bbbb2222", 1.0),
144
- makeCase("src:cccc3333", 1.0),
145
- ];
146
- const store = createMockStore(crossTask);
147
-
148
- const result = await scanForPromotions(store as never, "memory:pivot", { minDistinctSources: 2 });
149
- const candidate = result.candidates.find(c => c.type === "case_to_pattern");
150
- expect(candidate).toBeDefined();
151
- expect(candidate!.distinctSources).toBe(3);
152
- expect(result.rejectedSingleSource).toBe(0);
153
- });
154
-
155
- it("abstains (and says so) when the scope has no source pointers at all", async () => {
156
- const store = createMockStore([1, 2, 3].map(() => makeCase(null, 1.0)));
157
- const result = await scanForPromotions(store as never, "project:legacy", { minDistinctSources: 2 });
158
- expect(result.candidates.filter(c => c.type === "case_to_pattern").length).toBe(1);
159
- expect(result.abstainedUnknownSource).toBe(1);
160
- expect(result.rejectedSingleSource).toBe(0);
161
- });
162
- });