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,509 +0,0 @@
1
- /**
2
- * Phase 4: Constructive Retrieval — tests for candidate expansion,
3
- * source-map grounding, contradiction detection, and reconstruction pipeline.
4
- */
5
- import { describe, test, expect } from "bun:test";
6
- import {
7
- expandCandidates,
8
- detectContradictions,
9
- computeSourceMapCoverage,
10
- buildPrompt,
11
- reconstruct,
12
- extractCitedIds,
13
- shouldReconstruct,
14
- type CandidateExpansionDeps,
15
- type ReconstructionInput,
16
- type ReconstructionLLMClient,
17
- type ReconstructionOutput,
18
- } from "../context-reconstructor.js";
19
- import type { RetrievalResult } from "../retriever.js";
20
-
21
- // ============================================================================
22
- // Test Helpers
23
- // ============================================================================
24
-
25
- function makeResult(id: string, text: string, importance = 0.7, score = 0.8): RetrievalResult {
26
- return {
27
- entry: {
28
- id,
29
- text,
30
- importance,
31
- category: "entities",
32
- scope: "test",
33
- timestamp: Date.now(),
34
- metadata: "{}",
35
- vector: new Float32Array(0),
36
- },
37
- score,
38
- sources: {},
39
- };
40
- }
41
-
42
- function makeMockLLM(response: string | null): ReconstructionLLMClient {
43
- return {
44
- generateReconstruction: async () => response,
45
- };
46
- }
47
-
48
- // ============================================================================
49
- // expandCandidates
50
- // ============================================================================
51
-
52
- describe("expandCandidates", () => {
53
- test("tags direct results and returns source map", async () => {
54
- const direct = [makeResult("a", "memory A"), makeResult("b", "memory B")];
55
- const { results, sourceMap } = await expandCandidates(direct, {});
56
- expect(results).toHaveLength(2);
57
- expect(sourceMap.get("a")).toEqual({ type: "direct" });
58
- expect(sourceMap.get("b")).toEqual({ type: "direct" });
59
- });
60
-
61
- test("expands via KG neighbors", async () => {
62
- const direct = [makeResult("a", "memory A")];
63
- const deps: CandidateExpansionDeps = {
64
- expandViaKG: async () => [makeResult("kg1", "KG neighbor")],
65
- };
66
- const { results, sourceMap } = await expandCandidates(direct, deps);
67
- expect(results).toHaveLength(2);
68
- expect(sourceMap.get("kg1")).toEqual({ type: "kg_neighbor" });
69
- });
70
-
71
- test("expands via evolution chains", async () => {
72
- const direct = [makeResult("a", "memory A")];
73
- const deps: CandidateExpansionDeps = {
74
- expandViaEvolution: async () => [makeResult("evo1", "evolution successor")],
75
- };
76
- const { results, sourceMap } = await expandCandidates(direct, deps);
77
- expect(results).toHaveLength(2);
78
- expect(sourceMap.get("evo1")).toEqual({ type: "evolution_chain" });
79
- });
80
-
81
- test("expands via clusters", async () => {
82
- const direct = [makeResult("a", "memory A")];
83
- const deps: CandidateExpansionDeps = {
84
- expandViaClusters: async () => [makeResult("cl1", "cluster member")],
85
- };
86
- const { results, sourceMap } = await expandCandidates(direct, deps);
87
- expect(results).toHaveLength(2);
88
- expect(sourceMap.get("cl1")).toEqual({ type: "cluster_member" });
89
- });
90
-
91
- test("expands via narrative siblings", async () => {
92
- const direct = [makeResult("a", "memory A")];
93
- const deps: CandidateExpansionDeps = {
94
- expandViaNarrative: async () => [makeResult("ns1", "narrative sibling")],
95
- };
96
- const { results, sourceMap } = await expandCandidates(direct, deps);
97
- expect(results).toHaveLength(2);
98
- expect(sourceMap.get("ns1")).toEqual({ type: "narrative_sibling" });
99
- });
100
-
101
- test("deduplicates across expansion sources", async () => {
102
- const direct = [makeResult("a", "memory A")];
103
- const deps: CandidateExpansionDeps = {
104
- expandViaKG: async () => [makeResult("shared", "from KG")],
105
- expandViaEvolution: async () => [makeResult("shared", "from evo")],
106
- };
107
- const { results, sourceMap } = await expandCandidates(direct, deps);
108
- expect(results).toHaveLength(2);
109
- // First expansion wins
110
- expect(sourceMap.get("shared")).toEqual({ type: "kg_neighbor" });
111
- });
112
-
113
- test("respects EXPANSION_CAP of 20", async () => {
114
- const direct = Array.from({ length: 15 }, (_, i) => makeResult(`d${i}`, `direct ${i}`));
115
- const deps: CandidateExpansionDeps = {
116
- expandViaKG: async () => Array.from({ length: 10 }, (_, i) => makeResult(`kg${i}`, `kg ${i}`)),
117
- };
118
- const { results } = await expandCandidates(direct, deps);
119
- expect(results.length).toBeLessThanOrEqual(20);
120
- });
121
-
122
- test("handles expansion failures gracefully", async () => {
123
- const direct = [makeResult("a", "memory A")];
124
- const deps: CandidateExpansionDeps = {
125
- expandViaKG: async () => { throw new Error("KG unavailable"); },
126
- expandViaEvolution: async () => [makeResult("evo1", "evolution ok")],
127
- };
128
- const { results } = await expandCandidates(direct, deps);
129
- expect(results).toHaveLength(2); // direct + evolution (KG failed silently)
130
- });
131
-
132
- test("no expansion deps returns only direct results", async () => {
133
- const direct = [makeResult("a", "A"), makeResult("b", "B")];
134
- const { results, sourceMap } = await expandCandidates(direct, {});
135
- expect(results).toHaveLength(2);
136
- expect(sourceMap.size).toBe(2);
137
- });
138
-
139
- test("parallel expansion sources all contribute", async () => {
140
- const direct = [makeResult("a", "memory A")];
141
- const deps: CandidateExpansionDeps = {
142
- expandViaKG: async () => [makeResult("kg1", "KG")],
143
- expandViaEvolution: async () => [makeResult("evo1", "evo")],
144
- expandViaClusters: async () => [makeResult("cl1", "cluster")],
145
- expandViaNarrative: async () => [makeResult("ns1", "narrative")],
146
- };
147
- const { results, sourceMap } = await expandCandidates(direct, deps);
148
- expect(results).toHaveLength(5);
149
- expect(sourceMap.get("kg1")?.type).toBe("kg_neighbor");
150
- expect(sourceMap.get("evo1")?.type).toBe("evolution_chain");
151
- expect(sourceMap.get("cl1")?.type).toBe("cluster_member");
152
- expect(sourceMap.get("ns1")?.type).toBe("narrative_sibling");
153
- });
154
- });
155
-
156
- // ============================================================================
157
- // detectContradictions
158
- // ============================================================================
159
-
160
- describe("detectContradictions", () => {
161
- test("detects negation-pattern contradiction", () => {
162
- const results = [
163
- makeResult("a", "You should always use TypeScript for projects"),
164
- makeResult("b", "You should not always use TypeScript for every project"),
165
- ];
166
- const contradictions = detectContradictions(results);
167
- expect(contradictions.length).toBeGreaterThanOrEqual(1);
168
- expect(contradictions[0].memoryIds).toEqual(["a", "b"]);
169
- });
170
-
171
- test("no contradictions for compatible memories", () => {
172
- const results = [
173
- makeResult("a", "User prefers dark mode"),
174
- makeResult("b", "User uses Bun runtime"),
175
- ];
176
- expect(detectContradictions(results)).toHaveLength(0);
177
- });
178
-
179
- test("caps at 5 contradictions", () => {
180
- // Create many contradicting pairs
181
- const results: RetrievalResult[] = [];
182
- for (let i = 0; i < 12; i++) {
183
- const text = i % 2 === 0
184
- ? `Feature ${Math.floor(i / 2)} must always be enabled for deployment`
185
- : `Feature ${Math.floor(i / 2)} should never be enabled for deployment`;
186
- results.push(makeResult(`m${i}`, text));
187
- }
188
- const contradictions = detectContradictions(results);
189
- expect(contradictions.length).toBeLessThanOrEqual(5);
190
- });
191
-
192
- test("handles empty results", () => {
193
- expect(detectContradictions([])).toHaveLength(0);
194
- });
195
-
196
- test("handles single result", () => {
197
- expect(detectContradictions([makeResult("a", "solo")])).toHaveLength(0);
198
- });
199
- });
200
-
201
- // ============================================================================
202
- // computeSourceMapCoverage
203
- // ============================================================================
204
-
205
- describe("computeSourceMapCoverage", () => {
206
- test("full coverage when all sentences have valid citations", () => {
207
- const text = "User works on RecallNest [src:a]. They use Bun runtime [src:b].";
208
- const validIds = new Set(["a", "b"]);
209
- expect(computeSourceMapCoverage(text, validIds)).toBe(1.0);
210
- });
211
-
212
- test("partial coverage when some sentences lack citations", () => {
213
- const text = "User works on RecallNest [src:a]. No citation here. Another fact [src:b].";
214
- const validIds = new Set(["a", "b"]);
215
- const coverage = computeSourceMapCoverage(text, validIds);
216
- expect(coverage).toBeGreaterThan(0.5);
217
- expect(coverage).toBeLessThan(1.0);
218
- });
219
-
220
- test("zero coverage when no valid citations", () => {
221
- const text = "Some text [src:invalid]. More text [src:also_invalid].";
222
- const validIds = new Set(["real_id"]);
223
- expect(computeSourceMapCoverage(text, validIds)).toBe(0);
224
- });
225
-
226
- test("handles empty text", () => {
227
- expect(computeSourceMapCoverage("", new Set(["a"]))).toBe(0);
228
- });
229
-
230
- test("filters out short sentences (<=5 chars)", () => {
231
- const text = "OK. User works on RecallNest [src:a].";
232
- const coverage = computeSourceMapCoverage(text, new Set(["a"]));
233
- expect(coverage).toBe(1.0); // "OK." is filtered, only the cited sentence counts
234
- });
235
- });
236
-
237
- // ============================================================================
238
- // buildPrompt (checkpoint context injection)
239
- // ============================================================================
240
-
241
- describe("buildPrompt with checkpoint context", () => {
242
- const baseInput: ReconstructionInput = {
243
- query: "test query",
244
- results: [makeResult("a", "memory A"), makeResult("b", "memory B"), makeResult("c", "memory C")],
245
- mode: "resume",
246
- };
247
-
248
- test("includes open loops in system prompt", () => {
249
- const input: ReconstructionInput = {
250
- ...baseInput,
251
- checkpointContext: {
252
- openLoops: ["Fix CI pipeline", "Review PR #42"],
253
- },
254
- };
255
- const { system } = buildPrompt(input);
256
- expect(system).toContain("Open loops");
257
- expect(system).toContain("Fix CI pipeline");
258
- expect(system).toContain("Review PR #42");
259
- });
260
-
261
- test("includes next actions in system prompt", () => {
262
- const input: ReconstructionInput = {
263
- ...baseInput,
264
- checkpointContext: {
265
- nextActions: ["Deploy to staging", "Run integration tests"],
266
- },
267
- };
268
- const { system } = buildPrompt(input);
269
- expect(system).toContain("Next actions");
270
- expect(system).toContain("Deploy to staging");
271
- });
272
-
273
- test("includes scope in system prompt", () => {
274
- const input: ReconstructionInput = {
275
- ...baseInput,
276
- checkpointContext: { scope: "project:recallnest" },
277
- };
278
- const { system } = buildPrompt(input);
279
- expect(system).toContain("Active scope: project:recallnest");
280
- });
281
-
282
- test("no checkpoint context = no checkpoint hint", () => {
283
- const { system } = buildPrompt(baseInput);
284
- expect(system).not.toContain("Checkpoint state");
285
- });
286
-
287
- test("empty checkpoint context = no checkpoint hint", () => {
288
- const input: ReconstructionInput = {
289
- ...baseInput,
290
- checkpointContext: {},
291
- };
292
- const { system } = buildPrompt(input);
293
- expect(system).not.toContain("Checkpoint state");
294
- });
295
- });
296
-
297
- // ============================================================================
298
- // reconstruct (full pipeline)
299
- // ============================================================================
300
-
301
- describe("reconstruct pipeline", () => {
302
- const baseResults = [
303
- makeResult("m1", "User prefers TypeScript"),
304
- makeResult("m2", "Project uses Bun runtime"),
305
- makeResult("m3", "Deploy target is Docker"),
306
- ];
307
-
308
- test("returns reconstructed text with typed sources", async () => {
309
- const llm = makeMockLLM(
310
- "The user prefers TypeScript [src:m1]. The project uses Bun [src:m2]. Deployed via Docker [src:m3]."
311
- );
312
- const result = await reconstruct(
313
- { query: "project setup", results: baseResults, mode: "search" },
314
- llm,
315
- );
316
- expect(result.reconstructed).toBeTruthy();
317
- expect(result.sources.length).toBeGreaterThanOrEqual(1);
318
- // Sources have typed structure
319
- for (const s of result.sources) {
320
- expect(s).toHaveProperty("id");
321
- expect(s).toHaveProperty("source");
322
- expect(s.source).toHaveProperty("type");
323
- expect(s).toHaveProperty("contribution");
324
- }
325
- });
326
-
327
- test("returns contradictions array (even when empty)", async () => {
328
- const llm = makeMockLLM("Summary [src:m1]. Info [src:m2]. Details [src:m3].");
329
- const result = await reconstruct(
330
- { query: "test", results: baseResults, mode: "search" },
331
- llm,
332
- );
333
- expect(result).toHaveProperty("contradictions");
334
- expect(Array.isArray(result.contradictions)).toBe(true);
335
- });
336
-
337
- test("returns coverage score", async () => {
338
- const llm = makeMockLLM("Cited fact [src:m1]. Another fact [src:m2]. Third fact [src:m3].");
339
- const result = await reconstruct(
340
- { query: "test", results: baseResults, mode: "search" },
341
- llm,
342
- );
343
- expect(result).toHaveProperty("coverage");
344
- expect(typeof result.coverage).toBe("number");
345
- });
346
-
347
- test("fallback on LLM empty response", async () => {
348
- const llm = makeMockLLM(null);
349
- const result = await reconstruct(
350
- { query: "test", results: baseResults, mode: "search" },
351
- llm,
352
- );
353
- expect(result.reconstructed).toBeNull();
354
- expect(result.fallbackReason).toBe("llm_empty");
355
- expect(result.sources).toHaveLength(0);
356
- expect(result.contradictions).toHaveLength(0);
357
- });
358
-
359
- test("fallback on low grounding coverage", async () => {
360
- // Response with no valid citations
361
- const llm = makeMockLLM("Some text without any citations. Another ungrounded sentence.");
362
- const result = await reconstruct(
363
- { query: "test", results: baseResults, mode: "search" },
364
- llm,
365
- );
366
- expect(result.reconstructed).toBeNull();
367
- expect(result.fallbackReason).toBe("low_grounding");
368
- });
369
-
370
- test("removes sentences with invalid IDs and lowers confidence", async () => {
371
- const llm = makeMockLLM(
372
- "Valid fact [src:m1]. Fake source [src:FAKE]. Another valid [src:m2]. Third valid [src:m3]."
373
- );
374
- const result = await reconstruct(
375
- { query: "test", results: baseResults, mode: "search" },
376
- llm,
377
- );
378
- if (result.reconstructed) {
379
- expect(result.reconstructed).not.toContain("[src:FAKE]");
380
- expect(result.confidence).toBeLessThan(1.0);
381
- }
382
- });
383
-
384
- test("respects 3s timeout", async () => {
385
- const slowLLM: ReconstructionLLMClient = {
386
- generateReconstruction: async () => {
387
- await new Promise(r => setTimeout(r, 4000));
388
- return "too late [src:m1]";
389
- },
390
- };
391
- const result = await reconstruct(
392
- { query: "test", results: baseResults, mode: "search" },
393
- slowLLM,
394
- );
395
- expect(result.fallbackReason).toBe("timeout");
396
- }, 6000);
397
-
398
- test("with expansion deps — expanded candidates used in reconstruction", async () => {
399
- const expanded = makeResult("exp1", "expanded info");
400
- const deps: CandidateExpansionDeps = {
401
- expandViaKG: async () => [expanded],
402
- };
403
- const llm = makeMockLLM(
404
- "Main fact [src:m1]. KG expanded [src:exp1]. More info [src:m2]. Detail [src:m3]."
405
- );
406
- const result = await reconstruct(
407
- { query: "test", results: baseResults, mode: "search" },
408
- llm,
409
- deps,
410
- );
411
- expect(result.reconstructed).toBeTruthy();
412
- // Check that expanded source is tracked as kg_neighbor
413
- const expSource = result.sources.find(s => s.id === "exp1");
414
- expect(expSource).toBeTruthy();
415
- expect(expSource!.source.type).toBe("kg_neighbor");
416
- });
417
-
418
- test("source contributions extracted from cited sentences", async () => {
419
- const llm = makeMockLLM(
420
- "The user loves TypeScript and uses it everywhere [src:m1]. Bun is the runtime [src:m2]. Docker deploys [src:m3]."
421
- );
422
- const result = await reconstruct(
423
- { query: "test", results: baseResults, mode: "search" },
424
- llm,
425
- );
426
- if (result.sources.length > 0) {
427
- const m1Source = result.sources.find(s => s.id === "m1");
428
- expect(m1Source?.contribution).toBeTruthy();
429
- expect(m1Source?.contribution).toContain("TypeScript");
430
- }
431
- });
432
-
433
- test("checkpoint context passed through to LLM", async () => {
434
- let capturedSystem = "";
435
- const spy: ReconstructionLLMClient = {
436
- generateReconstruction: async (system, _user) => {
437
- capturedSystem = system;
438
- return "Fact [src:m1]. Detail [src:m2]. Info [src:m3].";
439
- },
440
- };
441
- await reconstruct(
442
- {
443
- query: "test",
444
- results: baseResults,
445
- mode: "resume",
446
- checkpointContext: {
447
- openLoops: ["Fix bug #99"],
448
- scope: "project:test",
449
- },
450
- },
451
- spy,
452
- );
453
- expect(capturedSystem).toContain("Fix bug #99");
454
- expect(capturedSystem).toContain("project:test");
455
- });
456
- });
457
-
458
- // ============================================================================
459
- // Integration: ReconstructionOutput shape contract
460
- // ============================================================================
461
-
462
- describe("ReconstructionOutput shape", () => {
463
- test("successful reconstruction has all required fields", async () => {
464
- const results = [
465
- makeResult("a", "fact A"),
466
- makeResult("b", "fact B"),
467
- makeResult("c", "fact C"),
468
- ];
469
- const llm = makeMockLLM("Summary of A [src:a]. Summary of B [src:b]. Summary of C [src:c].");
470
- const output = await reconstruct(
471
- { query: "test", results, mode: "search" },
472
- llm,
473
- );
474
-
475
- // Shape validation
476
- expect(output).toHaveProperty("reconstructed");
477
- expect(output).toHaveProperty("sources");
478
- expect(output).toHaveProperty("confidence");
479
- expect(output).toHaveProperty("contradictions");
480
- expect(output).toHaveProperty("coverage");
481
- expect(output).toHaveProperty("raw");
482
-
483
- // Type validation
484
- expect(typeof output.confidence).toBe("number");
485
- expect(typeof output.coverage).toBe("number");
486
- expect(Array.isArray(output.sources)).toBe(true);
487
- expect(Array.isArray(output.contradictions)).toBe(true);
488
- expect(Array.isArray(output.raw)).toBe(true);
489
- });
490
-
491
- test("failed reconstruction has all required fields", async () => {
492
- const results = [
493
- makeResult("a", "fact A"),
494
- makeResult("b", "fact B"),
495
- makeResult("c", "fact C"),
496
- ];
497
- const llm = makeMockLLM(null);
498
- const output = await reconstruct(
499
- { query: "test", results, mode: "search" },
500
- llm,
501
- );
502
-
503
- expect(output.reconstructed).toBeNull();
504
- expect(output.sources).toHaveLength(0);
505
- expect(output.contradictions).toHaveLength(0);
506
- expect(output.coverage).toBe(0);
507
- expect(output.fallbackReason).toBeTruthy();
508
- });
509
- });