recallnest 3.0.0 → 3.0.1

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