recallnest 2.3.0 → 2.5.4
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.
- package/CHANGELOG.md +150 -0
- package/README.md +17 -31
- package/README_CN.md +12 -22
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +169 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +95 -6
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +9 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +31 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,509 @@
|
|
|
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
|
+
});
|