recallnest 1.4.1 → 2.2.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 (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
@@ -0,0 +1,227 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ GovernorSession,
5
+ governResults,
6
+ resolveGovernorConfig,
7
+ truncateQuery,
8
+ } from "../recall-governor.js";
9
+ import type { RetrievalResult } from "../retriever.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function makeResult(
16
+ id: string,
17
+ text: string,
18
+ score: number,
19
+ metadata = "{}",
20
+ ): RetrievalResult {
21
+ return {
22
+ entry: {
23
+ id,
24
+ text,
25
+ vector: [],
26
+ category: "events",
27
+ scope: "project:test",
28
+ importance: 0.8,
29
+ timestamp: Date.now(),
30
+ metadata,
31
+ },
32
+ score,
33
+ sources: { fused: { score } },
34
+ };
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // truncateQuery
39
+ // ---------------------------------------------------------------------------
40
+
41
+ describe("truncateQuery", () => {
42
+ it("returns query unchanged when within limit", () => {
43
+ expect(truncateQuery("short", 1000)).toBe("short");
44
+ });
45
+
46
+ it("truncates query exceeding maxQueryChars", () => {
47
+ const long = "a".repeat(2000);
48
+ const result = truncateQuery(long, 500);
49
+ expect(result.length).toBe(500);
50
+ });
51
+
52
+ it("handles exact boundary", () => {
53
+ const exact = "x".repeat(100);
54
+ expect(truncateQuery(exact, 100)).toBe(exact);
55
+ });
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // resolveGovernorConfig
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("resolveGovernorConfig", () => {
63
+ it("returns defaults when no overrides", () => {
64
+ const cfg = resolveGovernorConfig();
65
+ expect(cfg.maxQueryChars).toBe(1000);
66
+ expect(cfg.charBudget).toBe(8000);
67
+ expect(cfg.maxItems).toBe(10);
68
+ });
69
+
70
+ it("merges partial overrides", () => {
71
+ const cfg = resolveGovernorConfig({ charBudget: 4000 });
72
+ expect(cfg.charBudget).toBe(4000);
73
+ expect(cfg.maxItems).toBe(10); // unchanged
74
+ });
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // GovernorSession
79
+ // ---------------------------------------------------------------------------
80
+
81
+ describe("GovernorSession", () => {
82
+ it("tracks injected IDs", () => {
83
+ const session = new GovernorSession();
84
+ expect(session.wasInjected("a")).toBe(false);
85
+ session.markInjected("a");
86
+ expect(session.wasInjected("a")).toBe(true);
87
+ expect(session.size).toBe(1);
88
+ });
89
+
90
+ it("markAll registers all result IDs", () => {
91
+ const session = new GovernorSession();
92
+ const results = [makeResult("x", "hi", 0.9), makeResult("y", "hello", 0.8)];
93
+ session.markAll(results);
94
+ expect(session.wasInjected("x")).toBe(true);
95
+ expect(session.wasInjected("y")).toBe(true);
96
+ expect(session.size).toBe(2);
97
+ });
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // governResults — evolution state filter
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe("governResults — evolution filter", () => {
105
+ it("keeps active memories", () => {
106
+ const results = [makeResult("a1", "active memory", 0.9, '{"evolution":{"status":"active"}}')];
107
+ const governed = governResults(results);
108
+ expect(governed).toHaveLength(1);
109
+ });
110
+
111
+ it("keeps pending_review memories", () => {
112
+ const results = [makeResult("p1", "pending review", 0.9, '{"evolution":{"status":"pending_review"}}')];
113
+ const governed = governResults(results);
114
+ expect(governed).toHaveLength(1);
115
+ });
116
+
117
+ it("drops archived memories", () => {
118
+ const results = [makeResult("ar1", "archived", 0.9, '{"evolution":{"status":"archived"}}')];
119
+ const governed = governResults(results);
120
+ expect(governed).toHaveLength(0);
121
+ });
122
+
123
+ it("drops superseded memories", () => {
124
+ const results = [makeResult("s1", "superseded", 0.9, '{"evolution":{"status":"superseded"}}')];
125
+ const governed = governResults(results);
126
+ expect(governed).toHaveLength(0);
127
+ });
128
+
129
+ it("keeps memories with no evolution (defaults to active)", () => {
130
+ const results = [makeResult("n1", "no status", 0.9, "{}")];
131
+ const governed = governResults(results);
132
+ expect(governed).toHaveLength(1);
133
+ });
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // governResults — budget control
138
+ // ---------------------------------------------------------------------------
139
+
140
+ describe("governResults — budget control", () => {
141
+ it("respects maxItems limit", () => {
142
+ const results = Array.from({ length: 5 }, (_, i) =>
143
+ makeResult(`b${i}`, "x".repeat(10), 0.9 - i * 0.01),
144
+ );
145
+ const governed = governResults(results, undefined, { maxItems: 3 });
146
+ expect(governed).toHaveLength(3);
147
+ });
148
+
149
+ it("respects charBudget limit", () => {
150
+ const results = [
151
+ makeResult("c1", "a".repeat(3000), 0.95),
152
+ makeResult("c2", "b".repeat(3000), 0.90),
153
+ makeResult("c3", "c".repeat(3000), 0.85),
154
+ ];
155
+ const governed = governResults(results, undefined, { charBudget: 5000 });
156
+ // First item (3000) fits, second (6000 total) would exceed 5000 → stops after first
157
+ expect(governed).toHaveLength(1);
158
+ });
159
+
160
+ it("always allows at least one result even if it exceeds budget", () => {
161
+ const results = [makeResult("big", "x".repeat(10000), 0.95)];
162
+ const governed = governResults(results, undefined, { charBudget: 100 });
163
+ expect(governed).toHaveLength(1);
164
+ });
165
+ });
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // governResults — session dedup
169
+ // ---------------------------------------------------------------------------
170
+
171
+ describe("governResults — session dedup", () => {
172
+ it("drops already-injected IDs", () => {
173
+ const session = new GovernorSession();
174
+ session.markInjected("dup1");
175
+
176
+ const results = [
177
+ makeResult("dup1", "already seen", 0.95),
178
+ makeResult("new1", "fresh memory", 0.90),
179
+ ];
180
+ const governed = governResults(results, session);
181
+ expect(governed).toHaveLength(1);
182
+ expect(governed[0].entry.id).toBe("new1");
183
+ });
184
+
185
+ it("marks newly governed results in the session", () => {
186
+ const session = new GovernorSession();
187
+ const results = [makeResult("x1", "hello", 0.9)];
188
+ governResults(results, session);
189
+ expect(session.wasInjected("x1")).toBe(true);
190
+ });
191
+
192
+ it("skips dedup when no session provided", () => {
193
+ const results = [makeResult("a", "text", 0.9), makeResult("a", "text", 0.8)];
194
+ // Without session, same ID can appear (though unusual from retriever)
195
+ const governed = governResults(results);
196
+ expect(governed).toHaveLength(2);
197
+ });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // governResults — empty input
202
+ // ---------------------------------------------------------------------------
203
+
204
+ describe("governResults — edge cases", () => {
205
+ it("returns empty array for empty input", () => {
206
+ expect(governResults([])).toHaveLength(0);
207
+ });
208
+
209
+ it("composes all layers correctly", () => {
210
+ const session = new GovernorSession();
211
+ session.markInjected("already");
212
+
213
+ const results = [
214
+ makeResult("already", "seen before", 0.99), // dedup
215
+ makeResult("archived", "old", 0.95, '{"evolution":{"status":"archived"}}'), // evolution
216
+ makeResult("keep1", "a".repeat(4000), 0.90), // kept
217
+ makeResult("keep2", "b".repeat(4000), 0.85), // kept
218
+ makeResult("over-budget", "c".repeat(4000), 0.80), // budget
219
+ ];
220
+ const governed = governResults(results, session, { charBudget: 8000 });
221
+ // archived dropped by evolution, already dropped by dedup,
222
+ // keep1 (4000) + keep2 (8000) = at budget, over-budget dropped
223
+ expect(governed.map((r) => r.entry.id)).toEqual(["keep1", "keep2"]);
224
+ expect(session.wasInjected("keep1")).toBe(true);
225
+ expect(session.wasInjected("keep2")).toBe(true);
226
+ });
227
+ });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { composeResumeContext } from "../context-composer.js";
3
+ import { composeLightResumeContext, composeResumeContext } from "../context-composer.js";
4
4
  import type { RetrievalContext, RetrievalResult } from "../retriever.js";
5
5
  import type { SessionCheckpointRecord } from "../session-schema.js";
6
6
 
@@ -4741,3 +4741,158 @@ describe("composeResumeContext", () => {
4741
4741
  expect(response.relevantPatterns.join(" ")).toContain("resume_context");
4742
4742
  });
4743
4743
  });
4744
+
4745
+ // ---------------------------------------------------------------------------
4746
+ // MP-3: composeLightResumeContext — Ultra-Light Wake-up Mode
4747
+ // ---------------------------------------------------------------------------
4748
+
4749
+ describe("composeLightResumeContext", () => {
4750
+ const checkpoint: SessionCheckpointRecord = {
4751
+ checkpointId: "cp-light-1",
4752
+ sessionId: "session-light",
4753
+ resolvedScope: "project:recallnest",
4754
+ summary: "Implemented LME-9 circuit breaker for LLM degradation",
4755
+ task: "LME-9 circuit breaker",
4756
+ decisions: ["Three-layer degradation"],
4757
+ openLoops: ["Soak test pending"],
4758
+ nextActions: ["Run full eval"],
4759
+ entities: ["RecallNest"],
4760
+ files: ["src/llm-client.ts"],
4761
+ updatedAt: "2026-04-06T10:00:00.000Z",
4762
+ };
4763
+
4764
+ it("returns checkpoint summary and stable memories under token budget", async () => {
4765
+ const retriever = {
4766
+ async retrieve(): Promise<RetrievalResult[]> {
4767
+ return [
4768
+ buildResult("p1", "profile", "User builds local-first memory systems with RecallNest."),
4769
+ buildResult("pref1", "preferences", "User prefers concise technical replies without fluff."),
4770
+ buildResult("e1", "entities", "RecallNest is a cross-terminal shared memory layer."),
4771
+ ];
4772
+ },
4773
+ };
4774
+
4775
+ const result = await composeLightResumeContext({
4776
+ retriever,
4777
+ checkpointStore: { async getLatest() { return checkpoint; } },
4778
+ listPins: () => [],
4779
+ }, {
4780
+ task: "继续 RecallNest 项目",
4781
+ scope: "project:recallnest",
4782
+ includeLatestCheckpoint: true,
4783
+ limitPerSection: 3,
4784
+ });
4785
+
4786
+ expect(result.text).toContain("session-light");
4787
+ expect(result.text).toContain("LME-9");
4788
+ expect(result.text).toContain("resume_context(mode='full')");
4789
+ expect(result.resolvedScope).toBe("project:recallnest");
4790
+ expect(result.generatedAt).toBeTruthy();
4791
+ // Budget check: <300 tokens ≈ <1200 chars
4792
+ expect(result.text.length).toBeLessThan(1200);
4793
+ });
4794
+
4795
+ it("works without checkpoint", async () => {
4796
+ const retriever = {
4797
+ async retrieve(): Promise<RetrievalResult[]> {
4798
+ return [
4799
+ buildResult("p1", "profile", "Data scientist working on observability."),
4800
+ ];
4801
+ },
4802
+ };
4803
+
4804
+ const result = await composeLightResumeContext({
4805
+ retriever,
4806
+ checkpointStore: { async getLatest() { return null; } },
4807
+ listPins: () => [],
4808
+ }, {
4809
+ includeLatestCheckpoint: true,
4810
+ limitPerSection: 3,
4811
+ });
4812
+
4813
+ expect(result.text).not.toContain("Last session");
4814
+ expect(result.text).toContain("observability");
4815
+ expect(result.text).toContain("resume_context(mode='full')");
4816
+ });
4817
+
4818
+ it("includes pin assets as stable context", async () => {
4819
+ const retriever = {
4820
+ async retrieve(): Promise<RetrievalResult[]> {
4821
+ return [];
4822
+ },
4823
+ };
4824
+
4825
+ const result = await composeLightResumeContext({
4826
+ retriever,
4827
+ checkpointStore: { async getLatest() { return checkpoint; } },
4828
+ listPins: () => [{
4829
+ path: "/tmp/pin1.md",
4830
+ title: "Auth migration decision",
4831
+ summary: "Session tokens must comply with new legal requirements",
4832
+ pinId: "pin-1",
4833
+ memoryId: "mem-1",
4834
+ createdAt: "2026-04-01T00:00:00.000Z",
4835
+ }],
4836
+ }, {
4837
+ scope: "project:recallnest",
4838
+ includeLatestCheckpoint: true,
4839
+ limitPerSection: 3,
4840
+ });
4841
+
4842
+ expect(result.text).toContain("legal requirements");
4843
+ });
4844
+
4845
+ it("deduplicates pin content from stable memories", async () => {
4846
+ const retriever = {
4847
+ async retrieve(): Promise<RetrievalResult[]> {
4848
+ return [
4849
+ buildResult("dup1", "profile", "Session tokens must comply with new legal requirements"),
4850
+ ];
4851
+ },
4852
+ };
4853
+
4854
+ const result = await composeLightResumeContext({
4855
+ retriever,
4856
+ checkpointStore: { async getLatest() { return null; } },
4857
+ listPins: () => [{
4858
+ path: "/tmp/pin1.md",
4859
+ title: "Auth migration",
4860
+ summary: "Session tokens must comply with new legal requirements",
4861
+ pinId: "pin-1",
4862
+ memoryId: "mem-1",
4863
+ createdAt: "2026-04-01T00:00:00.000Z",
4864
+ }],
4865
+ }, {
4866
+ includeLatestCheckpoint: true,
4867
+ limitPerSection: 3,
4868
+ });
4869
+
4870
+ // Should not have duplicate entries
4871
+ const matches = result.text.match(/legal requirements/g);
4872
+ expect(matches?.length).toBeLessThanOrEqual(1);
4873
+ });
4874
+
4875
+ it("respects scope from input over checkpoint", async () => {
4876
+ const calls: RetrievalContext[] = [];
4877
+ const retriever = {
4878
+ async retrieve(ctx: RetrievalContext): Promise<RetrievalResult[]> {
4879
+ calls.push(ctx);
4880
+ return [];
4881
+ },
4882
+ };
4883
+
4884
+ const result = await composeLightResumeContext({
4885
+ retriever,
4886
+ checkpointStore: { async getLatest() { return checkpoint; } },
4887
+ listPins: () => [],
4888
+ }, {
4889
+ scope: "project:custom",
4890
+ includeLatestCheckpoint: true,
4891
+ limitPerSection: 3,
4892
+ });
4893
+
4894
+ expect(result.resolvedScope).toBe("project:custom");
4895
+ // retrieveCandidates fires scoped + global in parallel; at least one must have the scope filter
4896
+ expect(calls.some(c => c.scopeFilter?.includes("project:custom"))).toBe(true);
4897
+ });
4898
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import type { LLMClient } from "../llm-client.js";
3
+
4
+ function createMockLLM(response: string): LLMClient {
5
+ return {
6
+ async chat() {
7
+ return response;
8
+ },
9
+ async assessImportance(text: string) {
10
+ const parsed = JSON.parse(response);
11
+ return parsed.importance ?? null;
12
+ },
13
+ async reassessImportanceBatch(
14
+ entries: Array<{ id: string; text: string; category: string; currentImportance: number }>,
15
+ ) {
16
+ const results = new Map<string, number>();
17
+ try {
18
+ const parsed = JSON.parse(response);
19
+ if (Array.isArray(parsed)) {
20
+ for (const item of parsed) {
21
+ if (item.idx >= 0 && item.idx < entries.length) {
22
+ results.set(entries[item.idx].id, item.importance);
23
+ }
24
+ }
25
+ }
26
+ } catch {
27
+ // match real implementation: non-fatal
28
+ }
29
+ return results;
30
+ },
31
+ } as any;
32
+ }
33
+
34
+ describe("HP-4: Salience scoring anchoring", () => {
35
+ describe("assessImportance anchored tiers", () => {
36
+ it("identity/correction → 0.85+", async () => {
37
+ const llm = createMockLLM(JSON.stringify({ importance: 0.92, reason: "用户明确纠正" }));
38
+ const result = await llm.assessImportance("不要用 rm 删除文件", "profile");
39
+ expect(result).toBeGreaterThanOrEqual(0.85);
40
+ });
41
+
42
+ it("decision/case → 0.70-0.84", async () => {
43
+ const llm = createMockLLM(JSON.stringify({ importance: 0.75, reason: "架构决策" }));
44
+ const result = await llm.assessImportance("选用 LanceDB 作为向量存储", "cases");
45
+ expect(result).toBeGreaterThanOrEqual(0.70);
46
+ expect(result).toBeLessThan(0.85);
47
+ });
48
+
49
+ it("entity/background → 0.55-0.69", async () => {
50
+ const llm = createMockLLM(JSON.stringify({ importance: 0.60, reason: "项目信息" }));
51
+ const result = await llm.assessImportance("RecallNest 使用 Bun 运行时", "entities");
52
+ expect(result).toBeGreaterThanOrEqual(0.55);
53
+ expect(result).toBeLessThan(0.70);
54
+ });
55
+
56
+ it("event/one-off → 0.40-0.54", async () => {
57
+ const llm = createMockLLM(JSON.stringify({ importance: 0.45, reason: "一次性操作" }));
58
+ const result = await llm.assessImportance("今天部署了 v2.1", "events");
59
+ expect(result).toBeGreaterThanOrEqual(0.40);
60
+ expect(result).toBeLessThan(0.55);
61
+ });
62
+
63
+ it("chatter/temp → 0.10-0.30", async () => {
64
+ const llm = createMockLLM(JSON.stringify({ importance: 0.15, reason: "临时上下文" }));
65
+ const result = await llm.assessImportance("好的,我看一下", "events");
66
+ expect(result).toBeGreaterThanOrEqual(0.10);
67
+ expect(result).toBeLessThan(0.40);
68
+ });
69
+ });
70
+
71
+ describe("reassessImportanceBatch", () => {
72
+ it("returns adjusted entries with ≥ 0.1 delta", async () => {
73
+ const batchResponse = JSON.stringify([
74
+ { idx: 0, importance: 0.90 },
75
+ { idx: 2, importance: 0.30 },
76
+ ]);
77
+ const llm = createMockLLM(batchResponse);
78
+ const entries = [
79
+ { id: "a", text: "永远不要用 rm", category: "profile", currentImportance: 0.70 },
80
+ { id: "b", text: "选用 LanceDB", category: "cases", currentImportance: 0.75 },
81
+ { id: "c", text: "今天天气不错", category: "events", currentImportance: 0.70 },
82
+ ];
83
+ const result = await llm.reassessImportanceBatch(entries);
84
+ expect(result.size).toBe(2);
85
+ expect(result.get("a")).toBe(0.90);
86
+ expect(result.get("c")).toBe(0.30);
87
+ expect(result.has("b")).toBe(false);
88
+ });
89
+
90
+ it("returns empty map on empty input", async () => {
91
+ const llm = createMockLLM("[]");
92
+ const result = await llm.reassessImportanceBatch([]);
93
+ expect(result.size).toBe(0);
94
+ });
95
+
96
+ it("handles invalid LLM response gracefully", async () => {
97
+ const llm = createMockLLM("not-json");
98
+ const entries = [
99
+ { id: "a", text: "test", category: "events", currentImportance: 0.5 },
100
+ ];
101
+ // Should not throw
102
+ const result = await llm.reassessImportanceBatch(entries);
103
+ expect(result.size).toBe(0);
104
+ });
105
+ });
106
+ });