recallnest 1.4.1 → 2.2.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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -1,133 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { compressToolOutput } from "../tool-output-compressor.js";
3
-
4
- describe("compressToolOutput", () => {
5
- // ========================================================================
6
- // Base64 replacement
7
- // ========================================================================
8
-
9
- it("replaces large base64 blocks with size placeholder", () => {
10
- const base64 = "A".repeat(2000);
11
- const text = `Here is a screenshot:\n${base64}\nEnd of screenshot.`;
12
- const result = compressToolOutput(text);
13
- expect(result).toContain("[image:");
14
- expect(result).toContain("base64");
15
- expect(result).not.toContain("AAAA");
16
- });
17
-
18
- it("preserves short base64-like strings", () => {
19
- const text = "The hash is abc123==";
20
- expect(compressToolOutput(text)).toBe(text);
21
- });
22
-
23
- // ========================================================================
24
- // Git output compression
25
- // ========================================================================
26
-
27
- it("compresses git push boilerplate", () => {
28
- const text = `$ git push origin main
29
- Enumerating objects: 5, done.
30
- Counting objects: 100% (5/5), done.
31
- Delta compression using up to 8 threads
32
- Compressing objects: 100% (3/3), done.
33
- Writing objects: 100% (3/3), 450 bytes | 450.00 KiB/s, done.
34
- Total 3 (delta 2), reused 0 (delta 0)
35
- remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
36
- To github.com:user/repo.git
37
- abc1234..def5678 main -> main
38
-
39
- Next I'll check the status.`;
40
-
41
- const result = compressToolOutput(text);
42
- expect(result).toContain("[git: ok");
43
- expect(result).toContain("main");
44
- expect(result).not.toContain("Enumerating objects");
45
- expect(result).toContain("Next I'll check the status");
46
- });
47
-
48
- it("preserves git push failures", () => {
49
- const text = `$ git push
50
- Enumerating objects: 5, done.
51
- error: failed to push some refs to 'origin'
52
- hint: Updates were rejected because the tip of your branch is behind -> main`;
53
-
54
- const result = compressToolOutput(text);
55
- expect(result).toContain("error");
56
- expect(result).toContain("rejected");
57
- });
58
-
59
- // ========================================================================
60
- // Test output compression
61
- // ========================================================================
62
-
63
- it("compresses passing test summary", () => {
64
- const text = `$ bun test
65
- running 15 tests
66
- test a ... ok
67
- test b ... ok
68
- test c ... ok
69
- test result: ok. 15 passed; 0 failed; 0 ignored
70
-
71
- All tests passed.`;
72
-
73
- const result = compressToolOutput(text);
74
- expect(result.length).toBeLessThanOrEqual(text.length);
75
- expect(result).toContain("All tests passed");
76
- });
77
-
78
- it("preserves failing test output", () => {
79
- const text = `$ npm test
80
- Tests: 2 failed, 8 passed
81
- FAIL src/main.test.ts
82
- ✕ should handle edge case`;
83
-
84
- const result = compressToolOutput(text);
85
- expect(result).toContain("failed");
86
- expect(result).toContain("FAIL");
87
- });
88
-
89
- // ========================================================================
90
- // Safety: user/AI content untouched
91
- // ========================================================================
92
-
93
- it("never modifies plain conversation text", () => {
94
- const text = "User: How do I fix this git push error?\n\nAssistant: You need to pull first.";
95
- expect(compressToolOutput(text)).toBe(text);
96
- });
97
-
98
- it("preserves AI reasoning about tool results", () => {
99
- const text = "The test result shows 15 passed, which means our fix works.";
100
- expect(compressToolOutput(text)).toBe(text);
101
- });
102
-
103
- // ========================================================================
104
- // Edge cases
105
- // ========================================================================
106
-
107
- it("handles empty input", () => {
108
- expect(compressToolOutput("")).toBe("");
109
- });
110
-
111
- it("handles short input", () => {
112
- expect(compressToolOutput("hi")).toBe("hi");
113
- });
114
-
115
- it("handles text without tool outputs", () => {
116
- const text = "This is a normal conversation.\nNo tool outputs here.";
117
- expect(compressToolOutput(text)).toBe(text);
118
- });
119
-
120
- // ========================================================================
121
- // Large output truncation
122
- // ========================================================================
123
-
124
- it("truncates large unmatched tool output", () => {
125
- // Use content with spaces/special chars to avoid base64 regex match
126
- const largeOutput = Array.from({ length: 150 }, (_, i) => `line ${i}: some detailed output here`).join("\n");
127
- const text = `$ some-command\n${largeOutput}\n\nNext step.`;
128
- const result = compressToolOutput(text);
129
- expect(result).toContain("[...");
130
- expect(result).toContain("chars truncated");
131
- expect(result.length).toBeLessThan(text.length);
132
- });
133
- });
@@ -1,213 +0,0 @@
1
- /**
2
- * Tests for Tier 3.3: Version Coexistence + Competition
3
- *
4
- * Validates:
5
- * 1. computeVersionRank formula
6
- * 2. createVersionGroup tags both entries with same group
7
- * 3. deduplicateByVersionGroup keeps top-ranked per group
8
- * 4. Entries without version_group pass through unchanged
9
- * 5. Multiple version groups are handled independently
10
- * 6. Existing version_group is reused (not duplicated)
11
- */
12
- import { describe, expect, it } from "bun:test";
13
- import {
14
- computeVersionRank,
15
- deduplicateByVersionGroup,
16
- createVersionGroup,
17
- } from "../version-manager.js";
18
- import type { MemoryEntry } from "../store.js";
19
-
20
- // ---------------------------------------------------------------------------
21
- // Helpers
22
- // ---------------------------------------------------------------------------
23
-
24
- function makeEntry(
25
- id: string,
26
- overrides: {
27
- confidence?: number;
28
- accessCount?: number;
29
- version_group?: string;
30
- version_rank?: number;
31
- } = {},
32
- ): MemoryEntry {
33
- const meta: Record<string, unknown> = {
34
- confidence: overrides.confidence ?? 0.7,
35
- accessCount: overrides.accessCount ?? 0,
36
- };
37
- if (overrides.version_group) meta.version_group = overrides.version_group;
38
- if (overrides.version_rank !== undefined) meta.version_rank = overrides.version_rank;
39
-
40
- return {
41
- id,
42
- text: `memory-${id}`,
43
- vector: [1, 0, 0],
44
- category: "events",
45
- scope: "project:test",
46
- importance: 0.7,
47
- timestamp: Date.now(),
48
- metadata: JSON.stringify(meta),
49
- };
50
- }
51
-
52
- function createMockStore(entries: MemoryEntry[]) {
53
- const data = new Map(entries.map(e => [e.id, { ...e }]));
54
- const updates: Array<{ id: string; metadata: string }> = [];
55
-
56
- return {
57
- data,
58
- updates,
59
- async getById(id: string) {
60
- return data.get(id) ?? null;
61
- },
62
- async update(id: string, upd: { metadata?: string }, _scope?: string[]) {
63
- const entry = data.get(id);
64
- if (!entry) return null;
65
- if (upd.metadata) {
66
- entry.metadata = upd.metadata;
67
- updates.push({ id, metadata: upd.metadata });
68
- }
69
- return entry;
70
- },
71
- };
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // computeVersionRank
76
- // ---------------------------------------------------------------------------
77
-
78
- describe("computeVersionRank", () => {
79
- it("computes rank from importance, confidence, and accessCount", () => {
80
- const entry = makeEntry("a", { confidence: 1.0, accessCount: 10 });
81
- const rank = computeVersionRank(entry);
82
- // quality = 0.5*0.7 + 0.5*1.0 = 0.85 (importance=0.7 from makeEntry)
83
- // rank = 0.85 * (1 + ln(11)) ≈ 0.85 * 3.398 = 2.888
84
- expect(rank).toBeGreaterThan(2.8);
85
- expect(rank).toBeLessThan(3.0);
86
- });
87
-
88
- it("returns baseline for default confidence and zero access", () => {
89
- const entry = makeEntry("b", { confidence: 0.7, accessCount: 0 });
90
- const rank = computeVersionRank(entry);
91
- // quality = 0.5*0.7 + 0.5*0.7 = 0.7
92
- // rank = 0.7 * (1 + 0) = 0.7
93
- expect(rank).toBeCloseTo(0.7, 5);
94
- });
95
-
96
- it("higher confidence wins over lower with same access", () => {
97
- const high = computeVersionRank(makeEntry("h", { confidence: 1.0, accessCount: 5 }));
98
- const low = computeVersionRank(makeEntry("l", { confidence: 0.3, accessCount: 5 }));
99
- expect(high).toBeGreaterThan(low);
100
- });
101
-
102
- it("higher access wins over lower with same confidence", () => {
103
- const frequent = computeVersionRank(makeEntry("f", { confidence: 0.7, accessCount: 20 }));
104
- const rare = computeVersionRank(makeEntry("r", { confidence: 0.7, accessCount: 1 }));
105
- expect(frequent).toBeGreaterThan(rare);
106
- });
107
- });
108
-
109
- // ---------------------------------------------------------------------------
110
- // createVersionGroup
111
- // ---------------------------------------------------------------------------
112
-
113
- describe("createVersionGroup", () => {
114
- it("tags both entries with the same version_group", async () => {
115
- const canonical = makeEntry("canon", { confidence: 1.0, accessCount: 5 });
116
- const member = makeEntry("member", { confidence: 0.5, accessCount: 1 });
117
- const store = createMockStore([canonical, member]);
118
-
119
- const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
120
-
121
- expect(groupId).toBeTruthy();
122
- expect(store.updates.length).toBe(2);
123
-
124
- const canonMeta = JSON.parse(store.data.get("canon")!.metadata);
125
- const memberMeta = JSON.parse(store.data.get("member")!.metadata);
126
-
127
- expect(canonMeta.version_group).toBe(groupId);
128
- expect(memberMeta.version_group).toBe(groupId);
129
- expect(canonMeta.version_rank).toBeGreaterThan(memberMeta.version_rank);
130
- });
131
-
132
- it("reuses existing version_group from canonical", async () => {
133
- const canonical = makeEntry("canon", {
134
- confidence: 1.0,
135
- accessCount: 5,
136
- version_group: "vg-existing",
137
- version_rank: 3.0,
138
- });
139
- const member = makeEntry("new", { confidence: 0.5, accessCount: 0 });
140
- const store = createMockStore([canonical, member]);
141
-
142
- const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
143
-
144
- expect(groupId).toBe("vg-existing");
145
- const memberMeta = JSON.parse(store.data.get("new")!.metadata);
146
- expect(memberMeta.version_group).toBe("vg-existing");
147
- });
148
- });
149
-
150
- // ---------------------------------------------------------------------------
151
- // deduplicateByVersionGroup
152
- // ---------------------------------------------------------------------------
153
-
154
- describe("deduplicateByVersionGroup", () => {
155
- it("keeps only the top-ranked entry per version group", () => {
156
- const results = [
157
- { entry: makeEntry("a", { version_group: "vg1", version_rank: 3.0 }), score: 0.9, sources: {} },
158
- { entry: makeEntry("b", { version_group: "vg1", version_rank: 1.0 }), score: 0.85, sources: {} },
159
- { entry: makeEntry("c"), score: 0.8, sources: {} },
160
- ];
161
-
162
- const deduped = deduplicateByVersionGroup(results);
163
-
164
- expect(deduped.length).toBe(2);
165
- expect(deduped.map(r => r.entry.id)).toContain("a");
166
- expect(deduped.map(r => r.entry.id)).toContain("c");
167
- expect(deduped.map(r => r.entry.id)).not.toContain("b");
168
- });
169
-
170
- it("passes through entries without version_group", () => {
171
- const results = [
172
- { entry: makeEntry("x"), score: 0.9, sources: {} },
173
- { entry: makeEntry("y"), score: 0.8, sources: {} },
174
- ];
175
-
176
- const deduped = deduplicateByVersionGroup(results);
177
-
178
- expect(deduped.length).toBe(2);
179
- });
180
-
181
- it("handles multiple independent version groups", () => {
182
- const results = [
183
- { entry: makeEntry("a1", { version_group: "vg1", version_rank: 3.0 }), score: 0.95, sources: {} },
184
- { entry: makeEntry("a2", { version_group: "vg1", version_rank: 1.0 }), score: 0.90, sources: {} },
185
- { entry: makeEntry("b1", { version_group: "vg2", version_rank: 2.0 }), score: 0.85, sources: {} },
186
- { entry: makeEntry("b2", { version_group: "vg2", version_rank: 4.0 }), score: 0.80, sources: {} },
187
- { entry: makeEntry("solo"), score: 0.75, sources: {} },
188
- ];
189
-
190
- const deduped = deduplicateByVersionGroup(results);
191
-
192
- expect(deduped.length).toBe(3);
193
- const ids = deduped.map(r => r.entry.id);
194
- expect(ids).toContain("a1"); // vg1 winner (rank 3.0)
195
- expect(ids).toContain("b2"); // vg2 winner (rank 4.0)
196
- expect(ids).toContain("solo");
197
- });
198
-
199
- it("handles empty array", () => {
200
- expect(deduplicateByVersionGroup([])).toEqual([]);
201
- });
202
-
203
- it("handles single entry in a version group", () => {
204
- const results = [
205
- { entry: makeEntry("only", { version_group: "vg1", version_rank: 2.0 }), score: 0.9, sources: {} },
206
- ];
207
-
208
- const deduped = deduplicateByVersionGroup(results);
209
-
210
- expect(deduped.length).toBe(1);
211
- expect(deduped[0].entry.id).toBe("only");
212
- });
213
- });