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,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
- });