recallnest 2.2.0 → 2.3.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 (249) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +30 -5
  4. package/README_CN.md +30 -5
  5. package/config.json +55 -0
  6. package/connectors/examples/email-adapter.ts +73 -0
  7. package/connectors/examples/logs-adapter.ts +103 -0
  8. package/connectors/examples/rss-adapter.ts +95 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +60 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/ingest-2026-04-13.log +87 -0
  14. package/logs/launchagent-stderr.log +0 -0
  15. package/logs/launchagent-stdout.log +0 -0
  16. package/package.json +1 -1
  17. package/src/cli.ts +30 -0
  18. package/src/connector-types.ts +57 -0
  19. package/src/conversation-importer.ts +25 -1
  20. package/src/data-checkup.ts +31 -1
  21. package/src/doctor.ts +29 -0
  22. package/src/ingest.ts +203 -0
  23. package/src/mcp-server.ts +2 -2
  24. package/src/obsidian-connector.ts +282 -0
  25. package/src/session-schema.ts +2 -2
  26. package/src/source-heartbeat.ts +108 -0
  27. package/.claude-plugin/CLAUDE.md +0 -29
  28. package/.claude-plugin/marketplace.json +0 -18
  29. package/.dockerignore +0 -11
  30. package/.github/workflows/ci.yml +0 -28
  31. package/.mcp.json +0 -9
  32. package/CHANGELOG.md +0 -51
  33. package/DEMO.md +0 -94
  34. package/Dockerfile +0 -20
  35. package/EVAL.md +0 -89
  36. package/FAILURES.md +0 -75
  37. package/OPERATIONS.md +0 -59
  38. package/ROADMAP.md +0 -245
  39. package/SKILL.md +0 -250
  40. package/assets/dashboard.png +0 -0
  41. package/assets/knowledge-graph.png +0 -0
  42. package/assets/recallnest-workbench.png +0 -0
  43. package/assets/screenshots/ui-full.png +0 -0
  44. package/assets/screenshots/ui-overview.png +0 -0
  45. package/assets/screenshots/ui-search.png +0 -0
  46. package/assets/ui/app.js +0 -1031
  47. package/assets/ui/index.html +0 -224
  48. package/assets/ui/styles.css +0 -781
  49. package/assets/wechat-qr.jpg +0 -0
  50. package/bun.lock +0 -337
  51. package/daily-reflections/2026-03-05.md +0 -15
  52. package/daily-reflections/2026-03-06.md +0 -15
  53. package/daily-reflections/2026-03-07.md +0 -7
  54. package/daily-reflections/2026-03-08.md +0 -34
  55. package/daily-reflections/2026-03-09.md +0 -35
  56. package/daily-reflections/2026-03-10.md +0 -36
  57. package/daily-reflections/2026-03-11.md +0 -35
  58. package/daily-reflections/2026-03-12.md +0 -36
  59. package/daily-reflections/2026-03-13.md +0 -35
  60. package/daily-reflections/2026-03-14.md +0 -35
  61. package/daily-reflections/2026-03-15.md +0 -35
  62. package/daily-reflections/2026-03-16.md +0 -41
  63. package/daily-reflections/2026-03-17.md +0 -10
  64. package/daily-reflections/2026-03-18.md +0 -10
  65. package/daily-reflections/2026-03-19.md +0 -10
  66. package/daily-reflections/2026-03-20.md +0 -10
  67. package/daily-reflections/2026-03-21.md +0 -10
  68. package/daily-reflections/2026-03-22.md +0 -10
  69. package/daily-reflections/2026-03-23.md +0 -10
  70. package/daily-reflections/2026-03-24.md +0 -10
  71. package/daily-reflections/2026-03-25.md +0 -32
  72. package/daily-reflections/2026-03-26.md +0 -37
  73. package/daily-reflections/2026-03-27.md +0 -35
  74. package/daily-reflections/2026-03-28.md +0 -35
  75. package/daily-reflections/2026-03-29.md +0 -35
  76. package/daily-reflections/2026-03-30.md +0 -36
  77. package/daily-reflections/2026-03-31.md +0 -35
  78. package/daily-reflections/2026-04-01.md +0 -35
  79. package/daily-reflections/2026-04-02.md +0 -35
  80. package/daily-reflections/2026-04-03.md +0 -37
  81. package/daily-reflections/2026-04-04.md +0 -37
  82. package/daily-reflections/2026-04-05.md +0 -35
  83. package/daily-reflections/2026-04-06.md +0 -36
  84. package/daily-reflections/2026-04-07.md +0 -35
  85. package/daily-reflections/2026-04-08.md +0 -35
  86. package/daily-reflections/2026-04-12.md +0 -35
  87. package/data/alias-map.example.json +0 -22
  88. package/docs/api-reference.md +0 -1291
  89. package/docs/architecture.md +0 -168
  90. package/docs/memory-boundary-contract.md +0 -226
  91. package/docs/memory-categories.md +0 -119
  92. package/docs/pipeline-audit-ultramemory.md +0 -177
  93. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  94. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  95. package/docs/plans/2026-04-01-recall-boost.md +0 -100
  96. package/docs/plans/mcp-tiering-plan.md +0 -126
  97. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  98. package/docs/self-evolution.md +0 -198
  99. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  100. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
  101. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  102. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  103. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
  104. package/eval/continuity/results-2026-03-16.md +0 -117
  105. package/eval/continuity/results-2026-03-19.md +0 -453
  106. package/eval/continuity/runbook.md +0 -110
  107. package/eval/continuity/seed-cases.md +0 -54
  108. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  109. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  110. package/eval/reports/2026-03-06-baseline.md +0 -82
  111. package/eval/reports/query-expansion.md +0 -138
  112. package/glama.json +0 -6
  113. package/lm +0 -19
  114. package/plan-philosophy-upgrades.md +0 -363
  115. package/plan-product-upgrades.md +0 -155
  116. package/plan-v1.2-distributable.md +0 -105
  117. package/plan.md +0 -140
  118. package/research-memory-evolution.md +0 -440
  119. package/research-rl-agent-memory.md +0 -330
  120. package/scripts/archive-distilled-junk.ts +0 -116
  121. package/scripts/backfill-anchors.ts +0 -130
  122. package/scripts/backfill-l0l1l2.ts +0 -98
  123. package/scripts/blind-scan.ts +0 -588
  124. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  125. package/scripts/cleanup-step2-template-dups.ts +0 -113
  126. package/scripts/cleanup-step3-stale-content.ts +0 -149
  127. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  128. package/scripts/delete-distilled-junk.ts +0 -88
  129. package/scripts/diagnose-vector-search.ts +0 -132
  130. package/scripts/distill-facts.ts +0 -305
  131. package/scripts/health-check.ts +0 -343
  132. package/scripts/incremental-ingest.sh +0 -71
  133. package/scripts/migrate-language.ts +0 -75
  134. package/scripts/mlp-health-check.ts +0 -382
  135. package/scripts/pull-claude-desktop.ts +0 -515
  136. package/scripts/re-ingest-cc.sh +0 -66
  137. package/scripts/rebalance-memory-health.ts +0 -181
  138. package/scripts/run-desktop-ingest.sh +0 -11
  139. package/scripts/start-server.sh +0 -12
  140. package/scripts/verify-emotion-decay.ts +0 -148
  141. package/scripts/viz-dashboard.html +0 -423
  142. package/scripts/viz-export.ts +0 -703
  143. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  144. package/src/__tests__/activity-counter.test.ts +0 -96
  145. package/src/__tests__/admission-control.test.ts +0 -206
  146. package/src/__tests__/anchor-generator.test.ts +0 -67
  147. package/src/__tests__/auto-consolidation.test.ts +0 -168
  148. package/src/__tests__/auto-recall.test.ts +0 -131
  149. package/src/__tests__/babel-integration.test.ts +0 -139
  150. package/src/__tests__/batch-dedup.test.ts +0 -65
  151. package/src/__tests__/capture-engine.test.ts +0 -1144
  152. package/src/__tests__/capture-heuristic.test.ts +0 -171
  153. package/src/__tests__/cascade-forget.test.ts +0 -210
  154. package/src/__tests__/category-min-scores.test.ts +0 -33
  155. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  156. package/src/__tests__/circuit-breaker.test.ts +0 -99
  157. package/src/__tests__/cluster-consolidation.test.ts +0 -712
  158. package/src/__tests__/confidence-metadata.test.ts +0 -125
  159. package/src/__tests__/confidence-tracker.test.ts +0 -249
  160. package/src/__tests__/conflict-advisor.test.ts +0 -206
  161. package/src/__tests__/conflict-escalation.test.ts +0 -149
  162. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  163. package/src/__tests__/conflict-output.test.ts +0 -85
  164. package/src/__tests__/conflict-store.test.ts +0 -374
  165. package/src/__tests__/consolidation-engine.test.ts +0 -181
  166. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  167. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  168. package/src/__tests__/context-composer-pins.test.ts +0 -64
  169. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  170. package/src/__tests__/context-reconstructor.test.ts +0 -65
  171. package/src/__tests__/context-renderer.test.ts +0 -91
  172. package/src/__tests__/continuity-eval.test.ts +0 -275
  173. package/src/__tests__/continuity-schema.test.ts +0 -97
  174. package/src/__tests__/conversation-importer.test.ts +0 -692
  175. package/src/__tests__/core-recall-eval.test.ts +0 -235
  176. package/src/__tests__/core-summary.test.ts +0 -109
  177. package/src/__tests__/data-checkup.test.ts +0 -225
  178. package/src/__tests__/decay-exemption.test.ts +0 -84
  179. package/src/__tests__/dedup-actions.test.ts +0 -58
  180. package/src/__tests__/dedup-guard.test.ts +0 -39
  181. package/src/__tests__/deterministic-id.test.ts +0 -39
  182. package/src/__tests__/distill-lock.test.ts +0 -176
  183. package/src/__tests__/doctor.test.ts +0 -303
  184. package/src/__tests__/dream-pipeline.test.ts +0 -232
  185. package/src/__tests__/drill-down.test.ts +0 -108
  186. package/src/__tests__/embedder.test.ts +0 -122
  187. package/src/__tests__/emotion-decay.test.ts +0 -46
  188. package/src/__tests__/emotion-detector.test.ts +0 -56
  189. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  190. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  191. package/src/__tests__/entity-resolver.test.ts +0 -58
  192. package/src/__tests__/ethics-layer.test.ts +0 -476
  193. package/src/__tests__/event-segmenter.test.ts +0 -153
  194. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  195. package/src/__tests__/frequency-tracker.test.ts +0 -113
  196. package/src/__tests__/graph-export.test.ts +0 -489
  197. package/src/__tests__/hotness-blend.test.ts +0 -76
  198. package/src/__tests__/ingest.test.ts +0 -360
  199. package/src/__tests__/interference-detector.test.ts +0 -203
  200. package/src/__tests__/kg-extractor.test.ts +0 -184
  201. package/src/__tests__/kg-store.test.ts +0 -198
  202. package/src/__tests__/llm-consolidation.test.ts +0 -226
  203. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  204. package/src/__tests__/memory-boundaries.test.ts +0 -237
  205. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
  206. package/src/__tests__/memory-evolution.test.ts +0 -271
  207. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  208. package/src/__tests__/memory-lint.test.ts +0 -271
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/narrative-architecture.test.ts +0 -456
  212. package/src/__tests__/pending-review.test.ts +0 -77
  213. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  214. package/src/__tests__/ppr-traversal.test.ts +0 -256
  215. package/src/__tests__/prediction-engine.test.ts +0 -349
  216. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  217. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  218. package/src/__tests__/preference-matcher.test.ts +0 -230
  219. package/src/__tests__/preference-slots.test.ts +0 -205
  220. package/src/__tests__/prospective-memory.test.ts +0 -283
  221. package/src/__tests__/query-entity-detector.test.ts +0 -171
  222. package/src/__tests__/query-expander-alias.test.ts +0 -89
  223. package/src/__tests__/query-expander.test.ts +0 -34
  224. package/src/__tests__/recall-governor.test.ts +0 -227
  225. package/src/__tests__/recall-mode.test.ts +0 -64
  226. package/src/__tests__/result-synthesizer.test.ts +0 -198
  227. package/src/__tests__/resume-context.test.ts +0 -4898
  228. package/src/__tests__/retrieval-trace.test.ts +0 -213
  229. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  230. package/src/__tests__/retroactive-boost.test.ts +0 -223
  231. package/src/__tests__/salience-scoring.test.ts +0 -106
  232. package/src/__tests__/scope-inventory.test.ts +0 -200
  233. package/src/__tests__/scope-policy.test.ts +0 -69
  234. package/src/__tests__/session-checkpoint.test.ts +0 -259
  235. package/src/__tests__/session-distiller.test.ts +0 -611
  236. package/src/__tests__/session-gc.test.ts +0 -215
  237. package/src/__tests__/skill-engine.test.ts +0 -546
  238. package/src/__tests__/skill-promotion.test.ts +0 -349
  239. package/src/__tests__/store-scope-audit.test.ts +0 -81
  240. package/src/__tests__/supersede-chain.test.ts +0 -154
  241. package/src/__tests__/temporal-parser.test.ts +0 -241
  242. package/src/__tests__/temporal-validity.test.ts +0 -160
  243. package/src/__tests__/term-registry.test.ts +0 -121
  244. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  245. package/src/__tests__/topic-tag.test.ts +0 -113
  246. package/src/__tests__/trusted-memory.test.ts +0 -828
  247. package/src/__tests__/version-manager.test.ts +0 -213
  248. package/src/__tests__/workflow-observation.test.ts +0 -400
  249. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,349 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { scanForPromotions, formatPromotionResult, type PromotionScanResult } from "../skill-promotion.js";
4
- import type { MemoryEntry, MemorySearchResult } from "../store.js";
5
-
6
- // ---------------------------------------------------------------------------
7
- // Mock Helpers
8
- // ---------------------------------------------------------------------------
9
-
10
- let idSeq = 0;
11
-
12
- function makeEntry(overrides: Partial<MemoryEntry> = {}): MemoryEntry {
13
- idSeq += 1;
14
- return {
15
- id: `entry-${String(idSeq).padStart(4, "0")}`,
16
- text: `Case about deploying to production #${idSeq}`,
17
- vector: [1, 0, 0],
18
- category: "cases",
19
- scope: "project:test",
20
- importance: 0.7,
21
- timestamp: 1_700_000_000_000 + idSeq,
22
- metadata: JSON.stringify({
23
- evolution: {
24
- status: "active",
25
- version: 1,
26
- accessCount: 0,
27
- lastAccessedAt: null,
28
- supersededBy: null,
29
- consolidatedInto: null,
30
- sourceMemories: [],
31
- validFrom: Date.now(),
32
- validUntil: null,
33
- },
34
- }),
35
- ...overrides,
36
- };
37
- }
38
-
39
- /** Create a vector that is similar to the base [1,0,0] with controlled similarity. */
40
- function similarVector(sim: number): number[] {
41
- // For cosine similarity, we want cos(theta) = sim
42
- // Use 2D vector [sim, sqrt(1-sim^2), 0] dot [1, 0, 0] = sim
43
- const complement = Math.sqrt(1 - sim * sim);
44
- return [sim, complement, 0];
45
- }
46
-
47
- function createMockStore(entries: MemoryEntry[]) {
48
- return {
49
- async list(
50
- _scopeFilter?: string[],
51
- _category?: string,
52
- _limit?: number,
53
- _offset?: number,
54
- ): Promise<MemoryEntry[]> {
55
- return entries;
56
- },
57
- async vectorSearch(
58
- vector: number[],
59
- limit = 5,
60
- minScore = 0.3,
61
- _scopeFilter?: string[],
62
- ): Promise<MemorySearchResult[]> {
63
- // Simple mock: return entries sorted by cosine similarity to vector
64
- const scored = entries
65
- .filter(e => e.vector?.length > 0)
66
- .map(e => {
67
- let dot = 0, normA = 0, normB = 0;
68
- for (let i = 0; i < vector.length; i++) {
69
- dot += vector[i] * (e.vector[i] ?? 0);
70
- normA += vector[i] * vector[i];
71
- normB += (e.vector[i] ?? 0) * (e.vector[i] ?? 0);
72
- }
73
- const score = (normA > 0 && normB > 0)
74
- ? dot / (Math.sqrt(normA) * Math.sqrt(normB))
75
- : 0;
76
- return { entry: e, score };
77
- })
78
- .filter(r => r.score >= minScore)
79
- .sort((a, b) => b.score - a.score)
80
- .slice(0, limit);
81
- return scored;
82
- },
83
- };
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Tests
88
- // ---------------------------------------------------------------------------
89
-
90
- describe("scanForPromotions", () => {
91
- it("returns 0 candidates for empty scope", async () => {
92
- const store = createMockStore([]);
93
- const result = await scanForPromotions(store, "project:empty");
94
-
95
- expect(result.candidates).toHaveLength(0);
96
- expect(result.scannedCases).toBe(0);
97
- expect(result.scannedPatterns).toBe(0);
98
- });
99
-
100
- it("returns 0 candidates when fewer than minCaseOccurrences similar cases", async () => {
101
- // Two cases with the same vector — below default threshold of 3
102
- const entries = [
103
- makeEntry({ vector: [1, 0, 0] }),
104
- makeEntry({ vector: [0.99, 0.14, 0] }),
105
- ];
106
- const store = createMockStore(entries);
107
- const result = await scanForPromotions(store, "project:test");
108
-
109
- expect(result.candidates).toHaveLength(0);
110
- expect(result.scannedCases).toBe(2);
111
- });
112
-
113
- it("produces case_to_pattern candidate when 3+ similar cases exist", async () => {
114
- const entries = [
115
- makeEntry({ text: "Deploy error: missing env var", vector: [1, 0, 0] }),
116
- makeEntry({ text: "Deploy failure: env variable not set", vector: similarVector(0.95) }),
117
- makeEntry({ text: "Deploy issue: environment config missing", vector: similarVector(0.90) }),
118
- ];
119
- const store = createMockStore(entries);
120
- const result = await scanForPromotions(store, "project:test", {
121
- minCaseOccurrences: 3,
122
- caseSimilarityThreshold: 0.75,
123
- });
124
-
125
- expect(result.candidates.length).toBeGreaterThanOrEqual(1);
126
- const caseCandidate = result.candidates.find(c => c.type === "case_to_pattern");
127
- expect(caseCandidate).toBeDefined();
128
- expect(caseCandidate!.sourceEntries.length).toBeGreaterThanOrEqual(3);
129
- expect(caseCandidate!.suggestedName).toBeTruthy();
130
- expect(caseCandidate!.suggestedDescription).toContain("cases");
131
- });
132
-
133
- it("does not produce candidates from dissimilar cases", async () => {
134
- // 3 cases with very different vectors
135
- const entries = [
136
- makeEntry({ text: "Case A", vector: [1, 0, 0] }),
137
- makeEntry({ text: "Case B", vector: [0, 1, 0] }),
138
- makeEntry({ text: "Case C", vector: [0, 0, 1] }),
139
- ];
140
- const store = createMockStore(entries);
141
- const result = await scanForPromotions(store, "project:test", {
142
- minCaseOccurrences: 3,
143
- caseSimilarityThreshold: 0.75,
144
- });
145
-
146
- expect(result.candidates).toHaveLength(0);
147
- });
148
-
149
- it("computes confidence using Bayesian smoothing", async () => {
150
- // 4 similar cases: confidence = 4 / (4 + 2) = 0.667
151
- const entries = [
152
- makeEntry({ text: "Deploy error A", vector: [1, 0, 0] }),
153
- makeEntry({ text: "Deploy error B", vector: similarVector(0.95) }),
154
- makeEntry({ text: "Deploy error C", vector: similarVector(0.92) }),
155
- makeEntry({ text: "Deploy error D", vector: similarVector(0.88) }),
156
- ];
157
- const store = createMockStore(entries);
158
- const result = await scanForPromotions(store, "project:test", {
159
- minCaseOccurrences: 3,
160
- caseSimilarityThreshold: 0.75,
161
- });
162
-
163
- expect(result.candidates.length).toBeGreaterThanOrEqual(1);
164
- const c = result.candidates[0];
165
- // cluster of 4: confidence = 4/(4+2) = 0.6667
166
- expect(c.confidence).toBeCloseTo(4 / 6, 2);
167
- });
168
-
169
- it("produces pattern_to_skill when pattern has steps and related cases", async () => {
170
- const patternVec: number[] = [0.9, 0.43, 0]; // cos sim with [1,0,0] ~ 0.9
171
- const entries = [
172
- makeEntry({
173
- category: "patterns",
174
- text: "Production Deploy Pattern\n\nSteps:\n1. Check environment\n2. Run preflight\n3. Deploy containers",
175
- vector: patternVec,
176
- }),
177
- makeEntry({ text: "Deploy case: ran preflight then deployed", vector: [1, 0, 0] }),
178
- makeEntry({ text: "Deploy fix: environment check before push", vector: similarVector(0.88) }),
179
- ];
180
- const store = createMockStore(entries);
181
- const result = await scanForPromotions(store, "project:test", {
182
- caseSimilarityThreshold: 0.75,
183
- });
184
-
185
- const skillCandidate = result.candidates.find(c => c.type === "pattern_to_skill");
186
- expect(skillCandidate).toBeDefined();
187
- expect(skillCandidate!.suggestedImplementation).toContain("Check environment");
188
- expect(skillCandidate!.sourceEntries.length).toBeGreaterThanOrEqual(3); // pattern + 2 cases
189
- });
190
-
191
- it("does not produce pattern_to_skill when pattern lacks structured steps", async () => {
192
- const patternVec: number[] = [0.9, 0.43, 0];
193
- const entries = [
194
- makeEntry({
195
- category: "patterns",
196
- text: "Just a general note about deploying without structured steps",
197
- vector: patternVec,
198
- }),
199
- makeEntry({ text: "Deploy case one", vector: [1, 0, 0] }),
200
- makeEntry({ text: "Deploy case two", vector: similarVector(0.88) }),
201
- ];
202
- const store = createMockStore(entries);
203
- const result = await scanForPromotions(store, "project:test", {
204
- caseSimilarityThreshold: 0.75,
205
- });
206
-
207
- const skillCandidate = result.candidates.find(c => c.type === "pattern_to_skill");
208
- expect(skillCandidate).toBeUndefined();
209
- });
210
-
211
- it("respects maxCandidates limit", async () => {
212
- // Create two distinct clusters of 3, but set maxCandidates=1
213
- const entries = [
214
- // Cluster 1
215
- makeEntry({ text: "Deploy A", vector: [1, 0, 0] }),
216
- makeEntry({ text: "Deploy B", vector: similarVector(0.95) }),
217
- makeEntry({ text: "Deploy C", vector: similarVector(0.90) }),
218
- // Cluster 2
219
- makeEntry({ text: "Auth fix A", vector: [0, 1, 0] }),
220
- makeEntry({ text: "Auth fix B", vector: [0.1, 0.99, 0] }),
221
- makeEntry({ text: "Auth fix C", vector: [0.15, 0.98, 0] }),
222
- ];
223
- const store = createMockStore(entries);
224
- const result = await scanForPromotions(store, "project:test", {
225
- minCaseOccurrences: 3,
226
- caseSimilarityThreshold: 0.75,
227
- maxCandidates: 1,
228
- });
229
-
230
- expect(result.candidates).toHaveLength(1);
231
- });
232
-
233
- it("filters out archived entries", async () => {
234
- const archivedMeta = JSON.stringify({
235
- evolution: {
236
- status: "archived",
237
- version: 1,
238
- accessCount: 0,
239
- lastAccessedAt: null,
240
- supersededBy: null,
241
- consolidatedInto: null,
242
- sourceMemories: [],
243
- validFrom: Date.now(),
244
- validUntil: null,
245
- },
246
- });
247
- const entries = [
248
- makeEntry({ text: "Active case", vector: [1, 0, 0] }),
249
- makeEntry({ text: "Archived case", vector: similarVector(0.95), metadata: archivedMeta }),
250
- makeEntry({ text: "Another archived", vector: similarVector(0.90), metadata: archivedMeta }),
251
- ];
252
- const store = createMockStore(entries);
253
- const result = await scanForPromotions(store, "project:test", {
254
- minCaseOccurrences: 3,
255
- });
256
-
257
- // Only 1 active case, so no candidates
258
- expect(result.candidates).toHaveLength(0);
259
- expect(result.scannedCases).toBe(1);
260
- });
261
-
262
- it("skips entries without vectors", async () => {
263
- const entries = [
264
- makeEntry({ text: "No vector case A", vector: [] }),
265
- makeEntry({ text: "No vector case B", vector: [] }),
266
- makeEntry({ text: "No vector case C", vector: [] }),
267
- ];
268
- const store = createMockStore(entries);
269
- const result = await scanForPromotions(store, "project:test");
270
-
271
- expect(result.candidates).toHaveLength(0);
272
- });
273
-
274
- it("candidates are sorted by confidence descending", async () => {
275
- // Cluster 1: 5 similar cases -> confidence 5/7
276
- // Cluster 2: 3 similar cases -> confidence 3/5
277
- const entries = [
278
- // Large cluster
279
- makeEntry({ text: "Deploy err 1", vector: [1, 0, 0] }),
280
- makeEntry({ text: "Deploy err 2", vector: similarVector(0.95) }),
281
- makeEntry({ text: "Deploy err 3", vector: similarVector(0.93) }),
282
- makeEntry({ text: "Deploy err 4", vector: similarVector(0.91) }),
283
- makeEntry({ text: "Deploy err 5", vector: similarVector(0.89) }),
284
- // Small cluster (orthogonal direction)
285
- makeEntry({ text: "Auth bug 1", vector: [0, 1, 0] }),
286
- makeEntry({ text: "Auth bug 2", vector: [0.1, 0.99, 0] }),
287
- makeEntry({ text: "Auth bug 3", vector: [0.15, 0.98, 0] }),
288
- ];
289
- const store = createMockStore(entries);
290
- const result = await scanForPromotions(store, "project:test", {
291
- minCaseOccurrences: 3,
292
- caseSimilarityThreshold: 0.75,
293
- });
294
-
295
- if (result.candidates.length >= 2) {
296
- expect(result.candidates[0].confidence).toBeGreaterThanOrEqual(result.candidates[1].confidence);
297
- }
298
- });
299
- });
300
-
301
- describe("formatPromotionResult", () => {
302
- it("formats empty results", () => {
303
- const result: PromotionScanResult = {
304
- candidates: [],
305
- scannedCases: 5,
306
- scannedPatterns: 2,
307
- };
308
- const text = formatPromotionResult(result);
309
- expect(text).toContain("5 cases");
310
- expect(text).toContain("2 patterns");
311
- expect(text).toContain("No promotion candidates found");
312
- });
313
-
314
- it("formats candidates with type and confidence", () => {
315
- const result: PromotionScanResult = {
316
- candidates: [{
317
- type: "case_to_pattern",
318
- sourceEntries: [{ id: "a", text: "test", score: 0.9 }],
319
- suggestedName: "Test Pattern",
320
- suggestedDescription: "A test",
321
- confidence: 0.6,
322
- }],
323
- scannedCases: 3,
324
- scannedPatterns: 1,
325
- };
326
- const text = formatPromotionResult(result);
327
- expect(text).toContain("case_to_pattern");
328
- expect(text).toContain("Test Pattern");
329
- expect(text).toContain("60.0%");
330
- });
331
-
332
- it("includes implementation in pattern_to_skill output", () => {
333
- const result: PromotionScanResult = {
334
- candidates: [{
335
- type: "pattern_to_skill",
336
- sourceEntries: [{ id: "a", text: "test", score: 1.0 }],
337
- suggestedName: "Deploy Skill",
338
- suggestedDescription: "Deploy automation",
339
- suggestedImplementation: "1. Check env\n2. Deploy",
340
- confidence: 0.5,
341
- }],
342
- scannedCases: 2,
343
- scannedPatterns: 1,
344
- };
345
- const text = formatPromotionResult(result);
346
- expect(text).toContain("1. Check env");
347
- expect(text).toContain("Implementation:");
348
- });
349
- });
@@ -1,81 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { MemoryStore, classifyLegacyScope } from "../store.js";
7
-
8
- const cleanupPaths: string[] = [];
9
-
10
- afterEach(() => {
11
- while (cleanupPaths.length > 0) {
12
- const target = cleanupPaths.pop();
13
- if (target) {
14
- rmSync(target, { recursive: true, force: true });
15
- }
16
- }
17
- });
18
-
19
- function createStore(): MemoryStore {
20
- const dbPath = mkdtempSync(join(tmpdir(), "recallnest-scope-audit-"));
21
- cleanupPaths.push(dbPath);
22
- return new MemoryStore({
23
- dbPath,
24
- vectorDim: 3,
25
- });
26
- }
27
-
28
- describe("classifyLegacyScope", () => {
29
- it("flags missing, empty, and global scope values", () => {
30
- expect(classifyLegacyScope(undefined)).toBe("missing");
31
- expect(classifyLegacyScope(null)).toBe("missing");
32
- expect(classifyLegacyScope("")).toBe("empty");
33
- expect(classifyLegacyScope(" ")).toBe("empty");
34
- expect(classifyLegacyScope("global")).toBe("global");
35
- expect(classifyLegacyScope(" global ")).toBe("global");
36
- expect(classifyLegacyScope("project:recallnest")).toBeUndefined();
37
- });
38
- });
39
-
40
- describe("MemoryStore.auditLegacyScopes", () => {
41
- it("reports global and empty scope rows without mixing in valid scoped data", async () => {
42
- const store = createStore();
43
-
44
- await store.store({
45
- text: "Scoped memory should not appear in the audit.",
46
- vector: [1, 0, 0],
47
- category: "entities",
48
- scope: "project:recallnest",
49
- importance: 0.8,
50
- metadata: "{}",
51
- });
52
- await store.store({
53
- text: "Historical row that used the global fallback.",
54
- vector: [0, 1, 0],
55
- category: "events",
56
- scope: "global",
57
- importance: 0.5,
58
- metadata: "{}",
59
- });
60
- await store.store({
61
- text: "Historical row that was written without a real scope.",
62
- vector: [0, 0, 1],
63
- category: "events",
64
- scope: " ",
65
- importance: 0.4,
66
- metadata: "{}",
67
- });
68
-
69
- const audit = await store.auditLegacyScopes(10);
70
-
71
- expect(audit.totalCount).toBe(2);
72
- expect(audit.counts).toEqual({
73
- missing: 0,
74
- empty: 1,
75
- global: 1,
76
- });
77
- expect(audit.samples).toHaveLength(2);
78
- expect(audit.samples.map((sample) => sample.kind).sort()).toEqual(["empty", "global"]);
79
- expect(audit.samples.every((sample) => sample.scope !== "project:recallnest")).toBe(true);
80
- });
81
- });
@@ -1,154 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import {
3
- buildSupersedeMetadata,
4
- buildSupersedeMetadataForNew,
5
- parseEvolution,
6
- traceEvolution,
7
- patchEvolution,
8
- defaultEvolution,
9
- } from "../memory-evolution.js";
10
-
11
- describe("HP-1: Superseded chain enhancement", () => {
12
- describe("bidirectional linking", () => {
13
- it("buildSupersedeMetadata marks old → supersededBy", () => {
14
- const oldMeta = patchEvolution("{}", defaultEvolution());
15
- const result = buildSupersedeMetadata(oldMeta, "new-id");
16
- const evo = parseEvolution(result);
17
- expect(evo.status).toBe("superseded");
18
- expect(evo.supersededBy).toBe("new-id");
19
- expect(evo.validUntil).toBeGreaterThan(0);
20
- });
21
-
22
- it("buildSupersedeMetadataForNew marks new → supersedes + note", () => {
23
- const newMeta = patchEvolution("{}", defaultEvolution());
24
- const result = buildSupersedeMetadataForNew(newMeta, "old-id", "updated preference");
25
- const evo = parseEvolution(result);
26
- expect(evo.status).toBe("active"); // new memory stays active
27
- expect(evo.supersedes).toBe("old-id");
28
- expect(evo.evolutionNote).toBe("updated preference");
29
- });
30
-
31
- it("bidirectional: old.supersededBy == new.id AND new.supersedes == old.id", () => {
32
- const oldMeta = patchEvolution("{}", defaultEvolution());
33
- const newMeta = patchEvolution("{}", defaultEvolution());
34
-
35
- const updatedOld = buildSupersedeMetadata(oldMeta, "new-id");
36
- const updatedNew = buildSupersedeMetadataForNew(newMeta, "old-id");
37
-
38
- const oldEvo = parseEvolution(updatedOld);
39
- const newEvo = parseEvolution(updatedNew);
40
-
41
- expect(oldEvo.supersededBy).toBe("new-id");
42
- expect(newEvo.supersedes).toBe("old-id");
43
- });
44
- });
45
-
46
- describe("traceEvolution", () => {
47
- it("traces a 3-node chain", async () => {
48
- // v1 → v2 → v3
49
- const entries: Record<string, { metadata: string; timestamp: number }> = {
50
- v1: {
51
- metadata: patchEvolution("{}", {
52
- status: "superseded",
53
- supersededBy: "v2",
54
- validFrom: 1000,
55
- validUntil: 2000,
56
- }),
57
- timestamp: 1000,
58
- },
59
- v2: {
60
- metadata: patchEvolution("{}", {
61
- status: "superseded",
62
- supersedes: "v1",
63
- supersededBy: "v3",
64
- evolutionNote: "added detail",
65
- validFrom: 2000,
66
- validUntil: 3000,
67
- }),
68
- timestamp: 2000,
69
- },
70
- v3: {
71
- metadata: patchEvolution("{}", {
72
- status: "active",
73
- supersedes: "v2",
74
- evolutionNote: "corrected error",
75
- validFrom: 3000,
76
- }),
77
- timestamp: 3000,
78
- },
79
- };
80
-
81
- const getEntry = async (id: string) => entries[id] ?? null;
82
-
83
- // Trace from v2 (middle)
84
- const trace = await traceEvolution("v2", getEntry);
85
- expect(trace.length).toBe(3);
86
- expect(trace[0].id).toBe("v1");
87
- expect(trace[0].direction).toBe("predecessor");
88
- expect(trace[1].id).toBe("v2");
89
- expect(trace[1].direction).toBe("self");
90
- expect(trace[2].id).toBe("v3");
91
- expect(trace[2].direction).toBe("successor");
92
- });
93
-
94
- it("handles single node (no chain)", async () => {
95
- const getEntry = async (id: string) => {
96
- if (id === "solo") return { metadata: patchEvolution("{}", defaultEvolution()), timestamp: 1000 };
97
- return null;
98
- };
99
-
100
- const trace = await traceEvolution("solo", getEntry);
101
- expect(trace.length).toBe(1);
102
- expect(trace[0].direction).toBe("self");
103
- });
104
-
105
- it("respects maxDepth", async () => {
106
- // Create a long chain: v0 → v1 → v2 → ... → v20
107
- const entries: Record<string, { metadata: string; timestamp: number }> = {};
108
- for (let i = 0; i <= 20; i++) {
109
- entries[`v${i}`] = {
110
- metadata: patchEvolution("{}", {
111
- status: i < 20 ? "superseded" : "active",
112
- supersedes: i > 0 ? `v${i - 1}` : null,
113
- supersededBy: i < 20 ? `v${i + 1}` : null,
114
- validFrom: i * 1000,
115
- }),
116
- timestamp: i * 1000,
117
- };
118
- }
119
-
120
- const getEntry = async (id: string) => entries[id] ?? null;
121
- const trace = await traceEvolution("v10", getEntry, 3);
122
- // maxDepth=3: at most 3 predecessors + self + 3 successors = 7
123
- expect(trace.length).toBeLessThanOrEqual(7);
124
- });
125
-
126
- it("handles missing entries gracefully", async () => {
127
- const entries: Record<string, { metadata: string; timestamp: number }> = {
128
- v1: {
129
- metadata: patchEvolution("{}", {
130
- status: "active",
131
- supersedes: "missing",
132
- validFrom: 1000,
133
- }),
134
- timestamp: 1000,
135
- },
136
- };
137
- const getEntry = async (id: string) => entries[id] ?? null;
138
- const trace = await traceEvolution("v1", getEntry);
139
- expect(trace.length).toBe(1); // only self, missing predecessor stops chain
140
- });
141
- });
142
-
143
- describe("backward compatibility", () => {
144
- it("parseEvolution defaults new fields to null", () => {
145
- // Old metadata without supersedes/evolutionNote
146
- const oldMeta = JSON.stringify({
147
- evolution: { status: "active", version: 1, accessCount: 0 },
148
- });
149
- const evo = parseEvolution(oldMeta);
150
- expect(evo.supersedes).toBeNull();
151
- expect(evo.evolutionNote).toBeNull();
152
- });
153
- });
154
- });