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,279 +0,0 @@
1
- /**
2
- * Verbatim self-recall: can a memory be found using words copied out of its own body?
3
- *
4
- * ## Where this came from
5
- *
6
- * On 2026-08-13 a window tried to correct a stored case (`4cb4853e`) and could not reach
7
- * it. `explain_memory` was called twice, eleven recall slots in total, with queries taken
8
- * verbatim from that case's own text. The target came back zero times. Ranking first in
9
- * the same result sets were two short, frequently-read `session_distill` entries.
10
- *
11
- * The correction was abandoned rather than guessed at — writing without the canonical key
12
- * would have produced a second, parallel memory instead of a revision. So the failure was
13
- * recorded as an anecdote and nothing more. This file is that anecdote turned into
14
- * something that can be run.
15
- *
16
- * ## What the reproduction shows
17
- *
18
- * The fixture below is the shape that failed: one long, older, never-read `cases` entry
19
- * competing against a dozen short, recent, frequently-read ones, with the long entry
20
- * holding by far the best raw content score because the query is lifted from its text.
21
- *
22
- * Measured against the production `DEFAULT_RETRIEVAL_CONFIG`:
23
- *
24
- * raw vector score target 0.920 distractors 0.565–0.620
25
- * after the chain target 0.469 distractors 0.47–0.71, target ranked 13 of 13
26
- *
27
- * The important detail — and it corrects the looser "the threshold cuts candidates down
28
- * to single digits" reading this was first filed under — is that **nothing filters the
29
- * target out**. Stage counts show `hard_min_score`, `layer_admission` and `noise_filter`
30
- * each dropping zero rows; 0.469 clears `hardMinScore` 0.35 comfortably. The entry is
31
- * admitted and then ranked last, so at any realistic `limit` it is simply never seen.
32
- *
33
- * The demotion is cumulative rather than attributable to one stage: importance weight,
34
- * length normalisation, time decay and the hotness/frequency boosts each move a long,
35
- * old, unread entry down and a short, fresh, popular one up. An ablation confirmed no
36
- * single one of them is decisive — restoring any one factor on its own still leaves the
37
- * target outside the top 5.
38
- *
39
- * ## Why these tests assert the failure instead of the fix
40
- *
41
- * Repairing the ranking chain is a design-level change that is deliberately out of scope
42
- * here: it needs a shadow run and a per-row account of what drops out, and the candidate
43
- * pool widening people reach for first was already measured and rejected
44
- * (`rejected-recall-widen-candidate-pool`, 2026-08-22). So this is a characterisation
45
- * test. It pins the current behaviour precisely enough that any change to the chain has
46
- * to come past it, and it states the property that ought to hold.
47
- *
48
- * **If a test in the second block starts failing, that is very likely good news.** It
49
- * means self-recall improved. Re-read the numbers, then move the assertion into the first
50
- * block as a real invariant.
51
- */
52
-
53
- import { describe, expect, it } from "bun:test";
54
-
55
- import { createRetriever, DEFAULT_RETRIEVAL_CONFIG } from "../retriever.js";
56
- import { TraceCollector } from "../retrieval-trace.js";
57
-
58
- const DAY_MS = 86_400_000;
59
-
60
- /** One paragraph of the kind of long case entry that failed — repeated to realistic length. */
61
- const CASE_PARAGRAPH =
62
- "冷启动抑制已证伪:hotness 改成 bonus-only 之后,阈值风险队列从 52.25% 降到 33.12%。" +
63
- "互审拦下三处过度断言,shadow 报告的口径与生产分类器不一致。";
64
-
65
- const TARGET_TEXT = CASE_PARAGRAPH.repeat(10);
66
- const TARGET_ID = "4cb4853e-0000-0000-0000-000000000000";
67
-
68
- /** Lifted word-for-word out of TARGET_TEXT — this is what "its own phrasing" means. */
69
- const VERBATIM_QUERY = "冷启动抑制已证伪 阈值风险队列 互审拦下三处过度断言 shadow 报告";
70
-
71
- const DISTRACTOR_COUNT = 12;
72
-
73
- function buildFixture() {
74
- const now = Date.now();
75
-
76
- // The target: long, two months old, never read. Best possible content match.
77
- const target = {
78
- id: TARGET_ID,
79
- text: TARGET_TEXT,
80
- vector: [1, 0, 0],
81
- category: "cases",
82
- scope: "memory",
83
- importance: 0.75,
84
- timestamp: now - 60 * DAY_MS,
85
- metadata: JSON.stringify({
86
- evolution: {
87
- status: "active",
88
- version: 1,
89
- accessCount: 0,
90
- lastAccessedAt: null,
91
- validFrom: now - 60 * DAY_MS,
92
- validUntil: null,
93
- },
94
- boundary: { layer: "durable", authority: "structured-memory", conflictPolicy: "latest-wins" },
95
- }),
96
- };
97
-
98
- // The distractors: short, fresh, heavily read. Weak content match on this query.
99
- const distractors = Array.from({ length: DISTRACTOR_COUNT }, (_, i) => ({
100
- id: `dddddddd-${String(i).padStart(4, "0")}-0000-0000-000000000000`,
101
- text: `会话提炼片段 ${i}:确认了一个与检索质量相关的方向。`,
102
- vector: [1, 0, 0],
103
- category: "events",
104
- scope: "memory",
105
- importance: 0.9,
106
- timestamp: now - 2 * DAY_MS,
107
- metadata: JSON.stringify({
108
- evolution: {
109
- status: "active",
110
- version: 1,
111
- accessCount: 40,
112
- lastAccessedAt: now - DAY_MS,
113
- validFrom: now - 2 * DAY_MS,
114
- validUntil: null,
115
- },
116
- boundary: { layer: "durable", authority: "structured-memory", conflictPolicy: "latest-wins" },
117
- }),
118
- }));
119
-
120
- return [
121
- { entry: target, score: 0.92 },
122
- ...distractors.map((entry, i) => ({ entry, score: 0.62 - i * 0.005 })),
123
- ];
124
- }
125
-
126
- function buildRetriever(candidates: ReturnType<typeof buildFixture>) {
127
- return createRetriever(
128
- {
129
- hasFtsSupport: false,
130
- async vectorSearch() {
131
- return candidates;
132
- },
133
- } as any,
134
- {
135
- async embedQuery() {
136
- return [1, 0, 0];
137
- },
138
- async embedPassage() {
139
- return [1, 0, 0];
140
- },
141
- } as any,
142
- // Production defaults, minus the reranker (it needs a vendor key and a network call).
143
- { ...DEFAULT_RETRIEVAL_CONFIG, rerank: "none" as const },
144
- );
145
- }
146
-
147
- async function retrieveWithTrace(limit: number) {
148
- const candidates = buildFixture();
149
- const retriever = buildRetriever(candidates);
150
- const trace = new TraceCollector();
151
- const results = await retriever.retrieve({
152
- query: VERBATIM_QUERY,
153
- scopeFilter: ["memory"],
154
- limit,
155
- trace,
156
- });
157
- return { results, stages: trace.finalize(VERBATIM_QUERY, "vector").stages };
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // Invariants — these must keep holding.
162
- // ---------------------------------------------------------------------------
163
-
164
- describe("verbatim self-recall: what the fixture guarantees", () => {
165
- it("gives the target the strictly best raw content score", () => {
166
- // Guards the premise. If the fixture ever stops modelling "the query matches this one
167
- // best", the tests below would be measuring something else entirely.
168
- const candidates = buildFixture();
169
- const target = candidates.find((c) => c.entry.id === TARGET_ID);
170
- const best = Math.max(...candidates.filter((c) => c.entry.id !== TARGET_ID).map((c) => c.score));
171
-
172
- expect(target?.score).toBe(0.92);
173
- expect(target!.score).toBeGreaterThan(best);
174
- });
175
-
176
- it("admits the target through every gate — it is not filtered out", async () => {
177
- const { stages } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
178
-
179
- // This is the part that must never regress into an actual filter. Losing a
180
- // verbatim-matching memory to a ranking is bad; dropping it at a threshold would be
181
- // worse, and would need a different fix.
182
- for (const name of ["hard_min_score", "noise_filter"]) {
183
- const stage = stages.find((s) => s.name === name);
184
- expect(stage, `stage ${name} missing from trace`).toBeDefined();
185
- expect(stage?.droppedCount, `stage ${name} dropped rows`).toBe(0);
186
- }
187
-
188
- // `layer_admission` is env-gated: applyLayerAdmission returns before touching the
189
- // trace when RECALLNEST_LAYER_ADMISSION is off, which is the default. Asserting it
190
- // unconditionally would only be asserting the developer's local .env — so check it
191
- // when it ran, and rely on the whole-result assertion below when it did not.
192
- const admission = stages.find((s) => s.name === "layer_admission");
193
- if (admission) expect(admission.droppedCount, "layer admission dropped rows").toBe(0);
194
- });
195
-
196
- it("returns every candidate — nothing is removed on the way through", async () => {
197
- // The environment-independent form of the claim above: whatever stages ran, the
198
- // result set still holds all of them, so no gate removed the target.
199
- const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
200
-
201
- expect(results).toHaveLength(DISTRACTOR_COUNT + 1);
202
- expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(true);
203
- });
204
-
205
- it("keeps the target's final score above the hard minimum", async () => {
206
- const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
207
- const target = results.find((r) => r.entry.id === TARGET_ID);
208
-
209
- expect(target).toBeDefined();
210
- expect(target!.score).toBeGreaterThan(DEFAULT_RETRIEVAL_CONFIG.hardMinScore);
211
- });
212
- });
213
-
214
- // ---------------------------------------------------------------------------
215
- // The defect — currently reproduced. A failure here probably means it got better.
216
- // ---------------------------------------------------------------------------
217
-
218
- describe("verbatim self-recall: the reported failure, reproduced", () => {
219
- it("does not return the target in a default-sized result window", async () => {
220
- // The original report in one line: eleven recall slots across two calls, queries taken
221
- // from the entry's own text, target never returned.
222
- const { results } = await retrieveWithTrace(5);
223
-
224
- expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(false);
225
- });
226
-
227
- it("still does not return it when the window is doubled", async () => {
228
- // Widening the window is the obvious first reflex, so it is worth recording that it
229
- // does not work here either — the entry is last, not marginal.
230
- const { results } = await retrieveWithTrace(10);
231
-
232
- expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(false);
233
- });
234
-
235
- it("ranks the best content match last once every candidate is returned", async () => {
236
- const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
237
- const rank = results.findIndex((r) => r.entry.id === TARGET_ID);
238
-
239
- expect(results).toHaveLength(DISTRACTOR_COUNT + 1);
240
- // Went in first by a wide margin, comes out last: a complete inversion of the
241
- // content signal by the weighting stages that follow it.
242
- expect(rank).toBe(DISTRACTOR_COUNT);
243
- });
244
-
245
- it("shows the inversion is cumulative, not caused by one stage", async () => {
246
- // Ablation summary: restoring any single disadvantage on its own — importance,
247
- // recency, length, access count — still leaves the target out of the top 5. Recorded
248
- // here so nobody re-runs the search for a single culprit.
249
- const candidates = buildFixture();
250
- const now = Date.now();
251
-
252
- const variants: Array<[string, () => typeof candidates]> = [
253
- ["importance raised to match", () =>
254
- candidates.map((c) => c.entry.id === TARGET_ID
255
- ? { ...c, entry: { ...c.entry, importance: 0.9 } }
256
- : c)],
257
- ["age reduced to match", () =>
258
- candidates.map((c) => c.entry.id === TARGET_ID
259
- ? { ...c, entry: { ...c.entry, timestamp: now - 2 * DAY_MS } }
260
- : c)],
261
- ["text shortened", () =>
262
- candidates.map((c) => c.entry.id === TARGET_ID
263
- ? { ...c, entry: { ...c.entry, text: CASE_PARAGRAPH } }
264
- : c)],
265
- ];
266
-
267
- for (const [label, build] of variants) {
268
- const results = await buildRetriever(build()).retrieve({
269
- query: VERBATIM_QUERY,
270
- scopeFilter: ["memory"],
271
- limit: 5,
272
- });
273
- expect(
274
- results.some((r) => r.entry.id === TARGET_ID),
275
- `${label}: target unexpectedly recalled — the ablation result changed`,
276
- ).toBe(false);
277
- }
278
- });
279
- });
@@ -1,248 +0,0 @@
1
- /**
2
- * Tests for Tier 3.3: Version Coexistence + Competition
3
- *
4
- * Validates:
5
- * 1. computeVersionRank formula
6
- * 2. createVersionGroup tags both entries with same group
7
- * 3. deduplicateByVersionGroup keeps top-ranked per group
8
- * 4. Entries without version_group pass through unchanged
9
- * 5. Multiple version groups are handled independently
10
- * 6. Existing version_group is reused (not duplicated)
11
- */
12
- import { describe, expect, it } from "bun:test";
13
- import {
14
- computeVersionRank,
15
- deduplicateByVersionGroup,
16
- createVersionGroup,
17
- clearVersionGroupMetadata,
18
- planSingletonVersionGroupRepairs,
19
- } from "../version-manager.js";
20
- import type { MemoryEntry } from "../store.js";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
-
26
- function makeEntry(
27
- id: string,
28
- overrides: {
29
- confidence?: number;
30
- accessCount?: number;
31
- version_group?: string;
32
- version_rank?: number;
33
- } = {},
34
- ): MemoryEntry {
35
- const meta: Record<string, unknown> = {
36
- confidence: overrides.confidence ?? 0.7,
37
- accessCount: overrides.accessCount ?? 0,
38
- };
39
- if (overrides.version_group) meta.version_group = overrides.version_group;
40
- if (overrides.version_rank !== undefined) meta.version_rank = overrides.version_rank;
41
-
42
- return {
43
- id,
44
- text: `memory-${id}`,
45
- vector: [1, 0, 0],
46
- category: "events",
47
- scope: "project:test",
48
- importance: 0.7,
49
- timestamp: Date.now(),
50
- metadata: JSON.stringify(meta),
51
- };
52
- }
53
-
54
- function createMockStore(entries: MemoryEntry[]) {
55
- const data = new Map(entries.map(e => [e.id, { ...e }]));
56
- const updates: Array<{ id: string; metadata: string }> = [];
57
-
58
- return {
59
- data,
60
- updates,
61
- async getById(id: string) {
62
- return data.get(id) ?? null;
63
- },
64
- async update(id: string, upd: { metadata?: string }, _scope?: string[]) {
65
- const entry = data.get(id);
66
- if (!entry) return null;
67
- if (upd.metadata) {
68
- entry.metadata = upd.metadata;
69
- updates.push({ id, metadata: upd.metadata });
70
- }
71
- return entry;
72
- },
73
- };
74
- }
75
-
76
- // ---------------------------------------------------------------------------
77
- // computeVersionRank
78
- // ---------------------------------------------------------------------------
79
-
80
- describe("computeVersionRank", () => {
81
- it("computes rank from importance, confidence, and accessCount", () => {
82
- const entry = makeEntry("a", { confidence: 1.0, accessCount: 10 });
83
- const rank = computeVersionRank(entry);
84
- // quality = 0.5*0.7 + 0.5*1.0 = 0.85 (importance=0.7 from makeEntry)
85
- // rank = 0.85 * (1 + ln(11)) ≈ 0.85 * 3.398 = 2.888
86
- expect(rank).toBeGreaterThan(2.8);
87
- expect(rank).toBeLessThan(3.0);
88
- });
89
-
90
- it("returns baseline for default confidence and zero access", () => {
91
- const entry = makeEntry("b", { confidence: 0.7, accessCount: 0 });
92
- const rank = computeVersionRank(entry);
93
- // quality = 0.5*0.7 + 0.5*0.7 = 0.7
94
- // rank = 0.7 * (1 + 0) = 0.7
95
- expect(rank).toBeCloseTo(0.7, 5);
96
- });
97
-
98
- it("higher confidence wins over lower with same access", () => {
99
- const high = computeVersionRank(makeEntry("h", { confidence: 1.0, accessCount: 5 }));
100
- const low = computeVersionRank(makeEntry("l", { confidence: 0.3, accessCount: 5 }));
101
- expect(high).toBeGreaterThan(low);
102
- });
103
-
104
- it("higher access wins over lower with same confidence", () => {
105
- const frequent = computeVersionRank(makeEntry("f", { confidence: 0.7, accessCount: 20 }));
106
- const rare = computeVersionRank(makeEntry("r", { confidence: 0.7, accessCount: 1 }));
107
- expect(frequent).toBeGreaterThan(rare);
108
- });
109
- });
110
-
111
- // ---------------------------------------------------------------------------
112
- // createVersionGroup
113
- // ---------------------------------------------------------------------------
114
-
115
- describe("createVersionGroup", () => {
116
- it("tags both entries with the same version_group", async () => {
117
- const canonical = makeEntry("canon", { confidence: 1.0, accessCount: 5 });
118
- const member = makeEntry("member", { confidence: 0.5, accessCount: 1 });
119
- const store = createMockStore([canonical, member]);
120
-
121
- const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
122
-
123
- expect(groupId).toBeTruthy();
124
- expect(store.updates.length).toBe(2);
125
-
126
- const canonMeta = JSON.parse(store.data.get("canon")!.metadata);
127
- const memberMeta = JSON.parse(store.data.get("member")!.metadata);
128
-
129
- expect(canonMeta.version_group).toBe(groupId);
130
- expect(memberMeta.version_group).toBe(groupId);
131
- expect(canonMeta.version_rank).toBeGreaterThan(memberMeta.version_rank);
132
- });
133
-
134
- it("reuses existing version_group from canonical", async () => {
135
- const canonical = makeEntry("canon", {
136
- confidence: 1.0,
137
- accessCount: 5,
138
- version_group: "vg-existing",
139
- version_rank: 3.0,
140
- });
141
- const member = makeEntry("new", { confidence: 0.5, accessCount: 0 });
142
- const store = createMockStore([canonical, member]);
143
-
144
- const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
145
-
146
- expect(groupId).toBe("vg-existing");
147
- const memberMeta = JSON.parse(store.data.get("new")!.metadata);
148
- expect(memberMeta.version_group).toBe("vg-existing");
149
- });
150
- });
151
-
152
- // ---------------------------------------------------------------------------
153
- // deduplicateByVersionGroup
154
- // ---------------------------------------------------------------------------
155
-
156
- describe("deduplicateByVersionGroup", () => {
157
- it("keeps only the top-ranked entry per version group", () => {
158
- const results = [
159
- { entry: makeEntry("a", { version_group: "vg1", version_rank: 3.0 }), score: 0.9, sources: {} },
160
- { entry: makeEntry("b", { version_group: "vg1", version_rank: 1.0 }), score: 0.85, sources: {} },
161
- { entry: makeEntry("c"), score: 0.8, sources: {} },
162
- ];
163
-
164
- const deduped = deduplicateByVersionGroup(results);
165
-
166
- expect(deduped.length).toBe(2);
167
- expect(deduped.map(r => r.entry.id)).toContain("a");
168
- expect(deduped.map(r => r.entry.id)).toContain("c");
169
- expect(deduped.map(r => r.entry.id)).not.toContain("b");
170
- });
171
-
172
- it("passes through entries without version_group", () => {
173
- const results = [
174
- { entry: makeEntry("x"), score: 0.9, sources: {} },
175
- { entry: makeEntry("y"), score: 0.8, sources: {} },
176
- ];
177
-
178
- const deduped = deduplicateByVersionGroup(results);
179
-
180
- expect(deduped.length).toBe(2);
181
- });
182
-
183
- it("handles multiple independent version groups", () => {
184
- const results = [
185
- { entry: makeEntry("a1", { version_group: "vg1", version_rank: 3.0 }), score: 0.95, sources: {} },
186
- { entry: makeEntry("a2", { version_group: "vg1", version_rank: 1.0 }), score: 0.90, sources: {} },
187
- { entry: makeEntry("b1", { version_group: "vg2", version_rank: 2.0 }), score: 0.85, sources: {} },
188
- { entry: makeEntry("b2", { version_group: "vg2", version_rank: 4.0 }), score: 0.80, sources: {} },
189
- { entry: makeEntry("solo"), score: 0.75, sources: {} },
190
- ];
191
-
192
- const deduped = deduplicateByVersionGroup(results);
193
-
194
- expect(deduped.length).toBe(3);
195
- const ids = deduped.map(r => r.entry.id);
196
- expect(ids).toContain("a1"); // vg1 winner (rank 3.0)
197
- expect(ids).toContain("b2"); // vg2 winner (rank 4.0)
198
- expect(ids).toContain("solo");
199
- });
200
-
201
- it("handles empty array", () => {
202
- expect(deduplicateByVersionGroup([])).toEqual([]);
203
- });
204
-
205
- it("handles single entry in a version group", () => {
206
- const results = [
207
- { entry: makeEntry("only", { version_group: "vg1", version_rank: 2.0 }), score: 0.9, sources: {} },
208
- ];
209
-
210
- const deduped = deduplicateByVersionGroup(results);
211
-
212
- expect(deduped.length).toBe(1);
213
- expect(deduped[0].entry.id).toBe("only");
214
- });
215
- });
216
-
217
- describe("singleton version-group repair", () => {
218
- it("plans only groups with one surviving member", () => {
219
- const singleton = makeEntry("solo", { version_group: "vg-single", version_rank: 1 });
220
- const pairA = makeEntry("pair-a", { version_group: "vg-pair", version_rank: 2 });
221
- const pairB = makeEntry("pair-b", { version_group: "vg-pair", version_rank: 1 });
222
- const malformed = makeEntry("bad");
223
- malformed.metadata = "{not-json";
224
-
225
- expect(planSingletonVersionGroupRepairs([
226
- singleton,
227
- pairA,
228
- pairB,
229
- makeEntry("ungrouped"),
230
- malformed,
231
- ])).toEqual([{ id: "solo", groupId: "vg-single" }]);
232
- });
233
-
234
- it("clears only version-group keys and preserves unrelated metadata", () => {
235
- const meta: Record<string, unknown> = {
236
- version_group: "vg-single",
237
- version_rank: 2.3,
238
- version_created: "2026-07-07T00:00:00.000Z",
239
- confidence: 0.9,
240
- evolution: { status: "active" },
241
- };
242
-
243
- expect(clearVersionGroupMetadata(meta, "vg-single")).toEqual({
244
- confidence: 0.9,
245
- evolution: { status: "active" },
246
- });
247
- });
248
- });