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,108 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
-
3
- describe("Tiered storage: L0/L1/L2 metadata parsing", () => {
4
- it("parses L0/L1/L2 from metadata JSON", () => {
5
- const metadata = JSON.stringify({
6
- l0_abstract: "User prefers TypeScript",
7
- l1_overview: "User consistently chooses TypeScript over JavaScript for new projects, citing type safety.",
8
- l2_content: "Full detailed content here...",
9
- });
10
- const parsed = JSON.parse(metadata);
11
- expect(parsed.l0_abstract).toBe("User prefers TypeScript");
12
- expect(parsed.l1_overview.length).toBeGreaterThan(parsed.l0_abstract.length);
13
- });
14
-
15
- it("handles missing L0/L1 fields gracefully", () => {
16
- const metadata = JSON.stringify({ accessCount: 5 });
17
- const parsed = JSON.parse(metadata);
18
- expect(parsed.l0_abstract).toBeUndefined();
19
- expect(parsed.l1_overview).toBeUndefined();
20
- });
21
-
22
- it("handles malformed metadata JSON", () => {
23
- const metadata = "not-json";
24
- let parsed = null;
25
- try {
26
- parsed = JSON.parse(metadata);
27
- } catch {
28
- // Expected
29
- }
30
- expect(parsed).toBeNull();
31
- });
32
- });
33
-
34
- describe("Tiered storage: drill-down level selection", () => {
35
- it("overview level returns L1 when available", () => {
36
- const meta = {
37
- l0_abstract: "Short summary",
38
- l1_overview: "Detailed overview with context and reasoning",
39
- };
40
- const fullText = "Very long full text content...";
41
- const level = "overview";
42
-
43
- const result = level === "overview" && meta.l1_overview
44
- ? meta.l1_overview
45
- : fullText;
46
-
47
- expect(result).toBe(meta.l1_overview);
48
- });
49
-
50
- it("overview level falls back to full text when L1 missing", () => {
51
- const meta: Record<string, string> = { l0_abstract: "Short summary" };
52
- const fullText = "Very long full text content...";
53
- const level = "overview";
54
-
55
- const result = level === "overview" && meta.l1_overview
56
- ? meta.l1_overview
57
- : fullText;
58
-
59
- expect(result).toBe(fullText);
60
- });
61
-
62
- it("full level always returns full text", () => {
63
- const meta = {
64
- l0_abstract: "Short summary",
65
- l1_overview: "Detailed overview",
66
- };
67
- const fullText = "Very long full text content...";
68
- const level = "full";
69
-
70
- const result = level === "full" ? fullText : meta.l1_overview || fullText;
71
- expect(result).toBe(fullText);
72
- });
73
- });
74
-
75
- describe("Tiered storage: recallDepthDefault config", () => {
76
- it("accepts valid depth values", () => {
77
- for (const depth of ["l0", "l1", "full"]) {
78
- expect(["l0", "l1", "full"].includes(depth)).toBe(true);
79
- }
80
- });
81
-
82
- it("rejects invalid depth values", () => {
83
- for (const depth of ["l3", "summary", "compact"]) {
84
- expect(["l0", "l1", "full"].includes(depth)).toBe(false);
85
- }
86
- });
87
-
88
- it("defaults to full when not set (backward compat)", () => {
89
- const configValue = undefined;
90
- const resolved = configValue || "full";
91
- expect(resolved).toBe("full");
92
- });
93
- });
94
-
95
- describe("Tiered storage: L0 injection format", () => {
96
- it("L0 abstract is compact enough for context injection", () => {
97
- const l0 = "User prefers TypeScript for all new projects";
98
- // L0 should be under 100 tokens (~400 chars)
99
- expect(l0.length).toBeLessThan(400);
100
- });
101
-
102
- it("L1 overview provides medium detail", () => {
103
- const l1 = "## Preference\nUser consistently chooses TypeScript over JavaScript. Cited reasons: type safety, better IDE support, easier refactoring.";
104
- // L1 should be under 500 tokens (~2000 chars)
105
- expect(l1.length).toBeLessThan(2000);
106
- expect(l1.length).toBeGreaterThan(50);
107
- });
108
- });
@@ -1,122 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { Embedder } from "../embedder.js";
4
-
5
- describe("Embedder transient retry", () => {
6
- it("retries a transient single-embedding connection error", async () => {
7
- const embedder = new Embedder({
8
- provider: "openai-compatible",
9
- apiKey: "test-key",
10
- model: "text-embedding-3-small",
11
- dimensions: 3,
12
- });
13
-
14
- let calls = 0;
15
- (embedder as any).client = {
16
- embeddings: {
17
- create: async () => {
18
- calls += 1;
19
- if (calls === 1) {
20
- throw new Error("Connection error. ECONNRESET");
21
- }
22
- return {
23
- data: [{ embedding: [1, 2, 3] }],
24
- };
25
- },
26
- },
27
- };
28
-
29
- await expect(embedder.embedPassage("hello")).resolves.toEqual([1, 2, 3]);
30
- expect(calls).toBe(2);
31
- });
32
-
33
- it("retries a transient batch embedding connection error", async () => {
34
- const embedder = new Embedder({
35
- provider: "openai-compatible",
36
- apiKey: "test-key",
37
- model: "text-embedding-3-small",
38
- dimensions: 3,
39
- });
40
-
41
- let calls = 0;
42
- (embedder as any).client = {
43
- embeddings: {
44
- create: async () => {
45
- calls += 1;
46
- if (calls === 1) {
47
- throw new Error("The socket connection was closed unexpectedly. ECONNRESET");
48
- }
49
- return {
50
- data: [
51
- { embedding: [1, 2, 3] },
52
- { embedding: [4, 5, 6] },
53
- ],
54
- };
55
- },
56
- },
57
- };
58
-
59
- await expect(embedder.embedBatchPassage(["a", "b"])).resolves.toEqual([
60
- [1, 2, 3],
61
- [4, 5, 6],
62
- ]);
63
- expect(calls).toBe(2);
64
- });
65
-
66
- it("does not retry non-transient embedding errors", async () => {
67
- const embedder = new Embedder({
68
- provider: "openai-compatible",
69
- apiKey: "test-key",
70
- model: "text-embedding-3-small",
71
- dimensions: 3,
72
- });
73
-
74
- let calls = 0;
75
- (embedder as any).client = {
76
- embeddings: {
77
- create: async () => {
78
- calls += 1;
79
- throw new Error("Authentication failed");
80
- },
81
- },
82
- };
83
-
84
- await expect(embedder.embedPassage("hello")).rejects.toThrow("Failed to generate embedding: Authentication failed");
85
- expect(calls).toBe(1);
86
- });
87
-
88
- it("limits concurrent embedding requests", async () => {
89
- const embedder = new Embedder({
90
- provider: "openai-compatible",
91
- apiKey: "test-key",
92
- model: "text-embedding-3-small",
93
- dimensions: 3,
94
- });
95
-
96
- (embedder as any)._maxConcurrentRequests = 1;
97
-
98
- let inFlight = 0;
99
- let maxInFlight = 0;
100
- (embedder as any).client = {
101
- embeddings: {
102
- create: async () => {
103
- inFlight += 1;
104
- maxInFlight = Math.max(maxInFlight, inFlight);
105
- await new Promise((resolve) => setTimeout(resolve, 20));
106
- inFlight -= 1;
107
- return {
108
- data: [{ embedding: [1, 2, 3] }],
109
- };
110
- },
111
- },
112
- };
113
-
114
- await Promise.all([
115
- embedder.embedPassage("first"),
116
- embedder.embedPassage("second"),
117
- embedder.embedPassage("third"),
118
- ]);
119
-
120
- expect(maxInFlight).toBe(1);
121
- });
122
- });
@@ -1,46 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { weibullDecay, adjustHalfLifeForEmotion, computeArousalBoost } from "../decay-engine.js";
3
-
4
- describe("emotion-adjusted decay", () => {
5
- test("returns base half-life when no emotion", () => {
6
- expect(adjustHalfLifeForEmotion(60, undefined)).toBe(60);
7
- expect(adjustHalfLifeForEmotion(60, null)).toBe(60);
8
- });
9
-
10
- test("strong negative emotion extends half-life ~27%", () => {
11
- const adjusted = adjustHalfLifeForEmotion(60, { valence: -0.9, arousal: 0.5, label: "frustration" });
12
- expect(adjusted).toBeCloseTo(60 * 1.27, 0);
13
- });
14
-
15
- test("strong positive emotion extends half-life ~24%", () => {
16
- const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.8, arousal: 0.3, label: "excitement" });
17
- expect(adjusted).toBeCloseTo(60 * 1.24, 0);
18
- });
19
-
20
- test("neutral emotion has negligible effect", () => {
21
- const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.05, arousal: 0.1, label: "neutral" });
22
- expect(adjusted).toBeCloseTo(60, 0);
23
- });
24
-
25
- test("emotional memory decays slower at 30 days", () => {
26
- const baseHL = 60;
27
- const emotionalHL = adjustHalfLifeForEmotion(baseHL, { valence: -0.8, arousal: 0.7, label: "frustration" });
28
- const neutralDecay = weibullDecay(30, baseHL, "working");
29
- const emotionalDecay = weibullDecay(30, emotionalHL, "working");
30
- expect(emotionalDecay).toBeGreaterThan(neutralDecay);
31
- });
32
-
33
- test("arousal boost is 1.0 for zero arousal", () => {
34
- expect(computeArousalBoost({ valence: 0.5, arousal: 0, label: "positive" })).toBe(1.0);
35
- });
36
-
37
- test("arousal boost max ~1.1 for high arousal", () => {
38
- const boost = computeArousalBoost({ valence: 0.5, arousal: 0.9, label: "excitement" });
39
- expect(boost).toBeCloseTo(1.09, 1);
40
- });
41
-
42
- test("arousal boost is 1.0 for null emotion", () => {
43
- expect(computeArousalBoost(undefined)).toBe(1.0);
44
- expect(computeArousalBoost(null)).toBe(1.0);
45
- });
46
- });
@@ -1,56 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { detectEmotion } from "../emotion-detector.js";
3
-
4
- describe("emotion-detector: heuristic", () => {
5
- test("detects strong negative text", () => {
6
- const result = detectEmotion("This bug is driving me crazy, everything is broken and wrong");
7
- expect(result.valence).toBeLessThan(-0.3);
8
- expect(result.label).toBe("negative");
9
- });
10
-
11
- test("detects strong positive text", () => {
12
- const result = detectEmotion("Finally solved it! Works perfectly, great progress today");
13
- expect(result.valence).toBeGreaterThan(0.3);
14
- expect(result.label).toBe("positive");
15
- });
16
-
17
- test("detects neutral text", () => {
18
- const result = detectEmotion("The user prefers dark mode and uses VS Code");
19
- expect(Math.abs(result.valence)).toBeLessThan(0.3);
20
- expect(result.label).toBe("neutral");
21
- });
22
-
23
- test("detects high arousal", () => {
24
- const result = detectEmotion("URGENT! Critical production issue, fix immediately!");
25
- expect(result.arousal).toBeGreaterThan(0.3);
26
- });
27
-
28
- test("detects low arousal for calm text", () => {
29
- const result = detectEmotion("The default configuration uses port 3000");
30
- expect(result.arousal).toBeLessThan(0.3);
31
- });
32
-
33
- test("handles mixed signals with net positive", () => {
34
- const result = detectEmotion("Had a frustrating bug but finally solved it perfectly");
35
- expect(result.valence).toBeGreaterThan(0);
36
- });
37
-
38
- test("handles empty text gracefully", () => {
39
- const result = detectEmotion("");
40
- expect(result.valence).toBe(0);
41
- expect(result.arousal).toBe(0);
42
- expect(result.label).toBe("neutral");
43
- });
44
-
45
- test("clamps valence to [-1, 1]", () => {
46
- const result = detectEmotion("broken error wrong bug failure hate broken error wrong");
47
- expect(result.valence).toBeGreaterThanOrEqual(-1);
48
- expect(result.valence).toBeLessThanOrEqual(1);
49
- });
50
-
51
- test("clamps arousal to [0, 1]", () => {
52
- const result = detectEmotion("URGENT! CRITICAL! IMMEDIATELY! ASAP!");
53
- expect(result.arousal).toBeGreaterThanOrEqual(0);
54
- expect(result.arousal).toBeLessThanOrEqual(1);
55
- });
56
- });
@@ -1,153 +0,0 @@
1
- /**
2
- * Phase 1 integration tests: Emotional Valence in Decay pipeline.
3
- *
4
- * Validates the full wiring:
5
- * 1. detectEmotion → salience + source fields
6
- * 2. computeDecayScore → emotion-aware weights (0.15/0.25/0.45/0.15)
7
- * 3. Backward compatibility when emotion flag is off or metadata absent
8
- */
9
- import { describe, test, expect } from "bun:test";
10
- import { detectEmotion } from "../emotion-detector.js";
11
- import { computeDecayScore, type EvolutionMetadata } from "../memory-evolution.js";
12
-
13
- // ---------------------------------------------------------------------------
14
- // 1. detectEmotion: salience + source fields
15
- // ---------------------------------------------------------------------------
16
-
17
- describe("detectEmotion salience & source", () => {
18
- test("returns salience field as (|valence| + arousal) / 2", () => {
19
- const result = detectEmotion("Finally fixed that horrible production bug after 3 days!");
20
- expect(result.salience).toBeDefined();
21
- expect(typeof result.salience).toBe("number");
22
- // salience = (|valence| + arousal) / 2
23
- const expected = (Math.abs(result.valence) + result.arousal) / 2;
24
- expect(result.salience).toBeCloseTo(expected, 5);
25
- });
26
-
27
- test("returns source: keyword", () => {
28
- const result = detectEmotion("This is a great success!");
29
- expect(result.source).toBe("keyword");
30
- });
31
-
32
- test("neutral text returns salience 0", () => {
33
- const result = detectEmotion("The meeting is at 3pm tomorrow.");
34
- expect(result.salience).toBe(0);
35
- });
36
-
37
- test("empty text returns salience 0 and source keyword", () => {
38
- const result = detectEmotion("");
39
- expect(result.salience).toBe(0);
40
- expect(result.source).toBe("keyword");
41
- });
42
-
43
- test("high emotion + high arousal gives high salience", () => {
44
- const result = detectEmotion("CRITICAL BUG! Everything is broken! Urgent fix needed!");
45
- expect(result.salience).toBeGreaterThan(0.2);
46
- });
47
-
48
- test("salience is clamped to [0, 1]", () => {
49
- const result = detectEmotion("fail fail fail broken broken crash crash urgent urgent critical critical !!!");
50
- expect(result.salience).toBeGreaterThanOrEqual(0);
51
- expect(result.salience).toBeLessThanOrEqual(1);
52
- });
53
- });
54
-
55
- // ---------------------------------------------------------------------------
56
- // 2. computeDecayScore: emotion-aware weights
57
- // ---------------------------------------------------------------------------
58
-
59
- describe("computeDecayScore with emotion", () => {
60
- const now = Date.now();
61
- const baseEvo: EvolutionMetadata = {
62
- status: "active",
63
- version: 1,
64
- accessCount: 3,
65
- lastAccessedAt: now - 7 * 86_400_000, // 7 days ago
66
- supersededBy: null,
67
- supersedes: null,
68
- evolutionNote: null,
69
- consolidatedInto: null,
70
- contributedToPattern: null,
71
- sourceMemories: [],
72
- validFrom: now - 30 * 86_400_000, // 30 days old
73
- validUntil: null,
74
- };
75
-
76
- // Save and restore env
77
- const origFlag = process.env.RECALLNEST_EMOTION_SCORING;
78
-
79
- test("with emotion flag ON and metadata, emotional memory scores higher", () => {
80
- process.env.RECALLNEST_EMOTION_SCORING = "true";
81
- try {
82
- const emotionalMeta = JSON.stringify({
83
- emotion: { valence: -0.9, arousal: 0.8, label: "frustration", salience: 0.85, source: "keyword" },
84
- });
85
- const neutralMeta = JSON.stringify({
86
- emotion: { valence: 0, arousal: 0, label: "neutral", salience: 0, source: "keyword" },
87
- });
88
-
89
- const emotionalScore = computeDecayScore(baseEvo, 0.5, now, emotionalMeta);
90
- const neutralScore = computeDecayScore(baseEvo, 0.5, now, neutralMeta);
91
-
92
- expect(emotionalScore).toBeGreaterThan(neutralScore);
93
- } finally {
94
- process.env.RECALLNEST_EMOTION_SCORING = origFlag;
95
- }
96
- });
97
-
98
- test("with emotion flag ON, weights sum close to 1.0", () => {
99
- process.env.RECALLNEST_EMOTION_SCORING = "true";
100
- try {
101
- // At max values: time=1, freq=1, importance=1, salience=1
102
- // Score should be 0.15 + 0.25 + 0.45 + 0.15 = 1.0
103
- const freshEvo: EvolutionMetadata = {
104
- ...baseEvo,
105
- accessCount: 100,
106
- lastAccessedAt: now,
107
- validFrom: now, // just created
108
- };
109
- const maxMeta = JSON.stringify({
110
- emotion: { valence: 1, arousal: 1, label: "positive", salience: 1, source: "keyword" },
111
- });
112
-
113
- const score = computeDecayScore(freshEvo, 1.0, now, maxMeta);
114
- expect(score).toBeCloseTo(1.0, 1);
115
- } finally {
116
- process.env.RECALLNEST_EMOTION_SCORING = origFlag;
117
- }
118
- });
119
-
120
- test("backward compat: no metadata param still works", () => {
121
- const score = computeDecayScore(baseEvo, 0.5, now);
122
- expect(score).toBeGreaterThan(0);
123
- expect(score).toBeLessThanOrEqual(1);
124
- });
125
-
126
- test("backward compat: metadata without emotion still works", () => {
127
- process.env.RECALLNEST_EMOTION_SCORING = "true";
128
- try {
129
- const noEmotionMeta = JSON.stringify({ tier: "working" });
130
- const score = computeDecayScore(baseEvo, 0.5, now, noEmotionMeta);
131
- expect(score).toBeGreaterThan(0);
132
- expect(score).toBeLessThanOrEqual(1);
133
- } finally {
134
- process.env.RECALLNEST_EMOTION_SCORING = origFlag;
135
- }
136
- });
137
-
138
- test("with emotion flag OFF, metadata is ignored (base weights)", () => {
139
- process.env.RECALLNEST_EMOTION_SCORING = "";
140
- try {
141
- const emotionalMeta = JSON.stringify({
142
- emotion: { valence: -0.9, arousal: 0.8, label: "frustration", salience: 0.85, source: "keyword" },
143
- });
144
-
145
- const withMeta = computeDecayScore(baseEvo, 0.5, now, emotionalMeta);
146
- const withoutMeta = computeDecayScore(baseEvo, 0.5, now);
147
-
148
- expect(withMeta).toBeCloseTo(withoutMeta, 5);
149
- } finally {
150
- process.env.RECALLNEST_EMOTION_SCORING = origFlag;
151
- }
152
- });
153
- });
@@ -1,56 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { applyEmotionWeight } from "../retriever.js";
3
-
4
- describe("applyEmotionWeight", () => {
5
- const makeResult = (id: string, score: number, emotionJson?: object) => ({
6
- id,
7
- text: "test",
8
- vector: [],
9
- category: "events" as const,
10
- scope: "test",
11
- importance: 0.5,
12
- timestamp: Date.now(),
13
- metadata: emotionJson ? JSON.stringify({ emotion: emotionJson }) : "{}",
14
- score,
15
- sources: {},
16
- });
17
-
18
- test("null query emotion returns unchanged", () => {
19
- const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
20
- const output = applyEmotionWeight(results, null);
21
- expect(output[0].score).toBe(0.8);
22
- });
23
-
24
- test("low-valence query returns unchanged", () => {
25
- const queryEmotion = { valence: 0.1, arousal: 0, label: "neutral" };
26
- const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
27
- const output = applyEmotionWeight(results, queryEmotion);
28
- expect(output[0].score).toBe(0.8);
29
- });
30
-
31
- test("negative query boosts negative memories", () => {
32
- const queryEmotion = { valence: -0.8, arousal: 0.5, label: "frustration" };
33
- const results = [
34
- makeResult("neg", 0.7, { valence: -0.7, arousal: 0.5 }),
35
- makeResult("pos", 0.7, { valence: 0.8, arousal: 0.3 }),
36
- ];
37
- const output = applyEmotionWeight(results, queryEmotion);
38
- expect(output.find(r => r.id === "neg")!.score).toBeGreaterThan(
39
- output.find(r => r.id === "pos")!.score
40
- );
41
- });
42
-
43
- test("no emotion data leaves score unchanged", () => {
44
- const queryEmotion = { valence: -0.8, arousal: 0.5, label: "negative" };
45
- const results = [makeResult("no-emo", 0.7)];
46
- const output = applyEmotionWeight(results, queryEmotion);
47
- expect(output[0].score).toBe(0.7);
48
- });
49
-
50
- test("max boost is 15%", () => {
51
- const queryEmotion = { valence: 1.0, arousal: 1.0, label: "positive" };
52
- const results = [makeResult("a", 1.0, { valence: 1.0, arousal: 1.0 })];
53
- const output = applyEmotionWeight(results, queryEmotion);
54
- expect(output[0].score).toBeCloseTo(1.15, 2);
55
- });
56
- });
@@ -1,58 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { EntityResolver, BUILTIN_ALIASES, defaultResolver, resolveQueryEntities } from "../entity-resolver.js";
4
-
5
- describe("EntityResolver", () => {
6
- it("resolves known aliases", () => {
7
- expect(defaultResolver.resolve("ts")).toBe("typescript");
8
- expect(defaultResolver.resolve("TS")).toBe("typescript");
9
- expect(defaultResolver.resolve("k8s")).toBe("kubernetes");
10
- expect(defaultResolver.resolve("pg")).toBe("postgresql");
11
- });
12
-
13
- it("returns term as-is for unknown aliases", () => {
14
- expect(defaultResolver.resolve("unknownThing")).toBe("unknownThing");
15
- expect(defaultResolver.resolve("")).toBe("");
16
- });
17
-
18
- it("supports user aliases that override builtins", () => {
19
- const resolver = new EntityResolver(BUILTIN_ALIASES, { "ts": "my-custom-ts" });
20
- expect(resolver.resolve("ts")).toBe("my-custom-ts");
21
- });
22
-
23
- it("resolveText replaces aliases in context", () => {
24
- const text = "I'm using TS with k8s and pg for the backend";
25
- const resolved = defaultResolver.resolveText(text);
26
- expect(resolved).toContain("typescript");
27
- expect(resolved).toContain("kubernetes");
28
- expect(resolved).toContain("postgresql");
29
- });
30
-
31
- it("resolveText handles longer aliases first", () => {
32
- const text = "Using docker-compose for local dev";
33
- const resolved = defaultResolver.resolveText(text);
34
- expect(resolved).toContain("docker compose");
35
- });
36
-
37
- it("hasAlias returns true for known aliases", () => {
38
- expect(defaultResolver.hasAlias("ts")).toBe(true);
39
- expect(defaultResolver.hasAlias("unknown")).toBe(false);
40
- });
41
-
42
- it("size returns the number of aliases", () => {
43
- expect(defaultResolver.size).toBeGreaterThan(40);
44
- });
45
- });
46
-
47
- describe("resolveQueryEntities", () => {
48
- it("normalizes entities in a search query", () => {
49
- const result = resolveQueryEntities("search for ts config in k8s");
50
- expect(result).toContain("typescript");
51
- expect(result).toContain("kubernetes");
52
- });
53
-
54
- it("passes through queries with no aliases", () => {
55
- const result = resolveQueryEntities("how to deploy a web app");
56
- expect(result).toBe("how to deploy a web app");
57
- });
58
- });
@@ -1,80 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- /**
6
- * Architecture guard for the invariant env-config.ts declares in its own header:
7
- * it is the single source of truth for every fixed `RECALLNEST_*` env read, and
8
- * consumers must call its accessors instead of touching `process.env` directly.
9
- *
10
- * That invariant was documented but unenforced, and it had already sprung a leak:
11
- * `cli.ts` read RECALLNEST_DREAM_BUDGET_MS inline and — precisely because it
12
- * bypassed the accessor layer — was the one read in the repo with no
13
- * `Number.isFinite` guard, so `""` silently became a 0ms dream budget and any
14
- * non-numeric value became a NaN deadline that disabled the guard entirely.
15
- * Bypassing the chokepoint IS how the guard gets lost; hence a test, not a note.
16
- *
17
- * Baseline: 0 violations as of 2026-08-18. If this goes red, the fix is normally
18
- * to add an accessor to env-config.ts — not to widen EXEMPT.
19
- *
20
- * Deliberately NOT matched (these are the exemptions env-config.ts's header
21
- * already names, and each reads differently so the pattern never sees them):
22
- * - scope-policy.ts → `env.RECALLNEST_*` on an injected env object
23
- * - store.ts → RECALLNEST_NS is a local constant, not an env var
24
- * - runtime-config / embedder / llm-client → dynamic `process.env[name]`
25
- * config-template expansion, which cannot be statically consolidated
26
- */
27
-
28
- // Add a file here ONLY with a reason, and only when an accessor genuinely can't
29
- // work (e.g. the read must happen before env-config's module-init boundary).
30
- const EXEMPT: ReadonlyArray<{ file: string; why: string }> = [];
31
-
32
- const DIRECT_READ = /process\.env\.RECALLNEST_[A-Z_0-9]+/g;
33
-
34
- function collectSourceFiles(dir: string, out: string[] = []): string[] {
35
- for (const entry of readdirSync(dir)) {
36
- const full = join(dir, entry);
37
- if (statSync(full).isDirectory()) {
38
- if (entry === "__tests__" || entry === "node_modules") continue;
39
- collectSourceFiles(full, out);
40
- continue;
41
- }
42
- if (!entry.endsWith(".ts") || entry.endsWith(".test.ts") || entry === "env-config.ts") continue;
43
- out.push(full);
44
- }
45
- return out;
46
- }
47
-
48
- describe("env-config is the chokepoint for RECALLNEST_* reads", () => {
49
- it("no src/ module reads process.env.RECALLNEST_* directly", () => {
50
- const exemptFiles = new Set(EXEMPT.map((e) => e.file));
51
- const violations: string[] = [];
52
-
53
- for (const file of collectSourceFiles("src")) {
54
- if (exemptFiles.has(file)) continue;
55
- const lines = readFileSync(file, "utf8").split("\n");
56
- lines.forEach((line, i) => {
57
- for (const hit of line.match(DIRECT_READ) ?? []) {
58
- violations.push(`${file}:${i + 1} ${hit}`);
59
- }
60
- });
61
- }
62
-
63
- expect(
64
- violations,
65
- violations.length === 0
66
- ? ""
67
- : `Direct RECALLNEST_* env reads bypass env-config.ts's guards:\n ${violations.join("\n ")}\n` +
68
- `Fix: add an accessor to src/env-config.ts (with its parsing + fallback) and call it here.`,
69
- ).toEqual([]);
70
- });
71
-
72
- it("guard actually detects a violation (so a green run means something)", () => {
73
- const synthetic = 'const x = Number(process.env.RECALLNEST_SOMETHING_NEW ?? 1);';
74
- expect(synthetic.match(DIRECT_READ)).toHaveLength(1);
75
- // And it must not fire on the exempted read styles:
76
- expect('env.RECALLNEST_SESSION_ID'.match(DIRECT_READ)).toBeNull();
77
- expect('process.env[envVar]'.match(DIRECT_READ)).toBeNull();
78
- expect('const RECALLNEST_NS = "recallnest:v1";'.match(DIRECT_READ)).toBeNull();
79
- });
80
- });