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
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Memory Knowledge Graph Export — generates an interactive HTML visualization
3
+ * of memory entries as a force-directed graph.
4
+ *
5
+ * Uses D3.js v7 for layout and rendering. The output is a self-contained HTML
6
+ * file that can be opened in any browser.
7
+ */
8
+
9
+ import type { MemoryEntry, MemoryStore } from "./store.js";
10
+ import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
11
+ import { cosineSimilarity } from "./multi-vector.js";
12
+ import { parseNarrative } from "./narrative-schema.js";
13
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { metaDir } from "./compat.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface GraphNode {
22
+ id: string;
23
+ label: string; // truncated text
24
+ category: string;
25
+ scope: string;
26
+ importance: number;
27
+ timestamp: number;
28
+ accessCount: number;
29
+ }
30
+
31
+ export interface GraphEdge {
32
+ source: string;
33
+ target: string;
34
+ type: "supersede" | "consolidation" | "cluster" | "scope" | "semantic" | "narrative";
35
+ }
36
+
37
+ export interface MemoryGraph {
38
+ nodes: GraphNode[];
39
+ edges: GraphEdge[];
40
+ }
41
+
42
+ export interface GraphExportOptions {
43
+ scope?: string;
44
+ maxNodes?: number; // default 200
45
+ outputPath?: string; // default data/exports/
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function truncateText(text: string, maxLen: number): string {
53
+ if (text.length <= maxLen) return text;
54
+ return text.slice(0, maxLen - 1) + "\u2026";
55
+ }
56
+
57
+ interface ParsedMetadata {
58
+ evolution?: Record<string, unknown>;
59
+ clustered_with?: string;
60
+ cluster_members?: string[];
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ function parseMetadataSafe(metadata: string | undefined): ParsedMetadata {
65
+ if (!metadata) return {};
66
+ try {
67
+ return JSON.parse(metadata) as ParsedMetadata;
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Node Selection — diverse, connected, balanced
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Select nodes for the graph with diversity in mind:
79
+ * 1. First, include entries that have evolution links (they form interesting connected subgraphs)
80
+ * 2. Then, fill remaining slots with round-robin across categories (sorted by importance within each)
81
+ *
82
+ * This avoids the "all patterns, no edges" problem that pure importance sorting causes.
83
+ */
84
+ function selectDiverseNodes(entries: MemoryEntry[], maxNodes: number): MemoryEntry[] {
85
+ if (entries.length <= maxNodes) return entries;
86
+
87
+ const selected = new Map<string, MemoryEntry>();
88
+
89
+ // Phase 1: Prioritize entries with evolution links (they create edges)
90
+ for (const entry of entries) {
91
+ if (selected.size >= maxNodes) break;
92
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
93
+ const meta = parseMetadataSafe(entry.metadata);
94
+ const hasLinks = evo.supersededBy || evo.supersedes || evo.consolidatedInto ||
95
+ typeof meta.clustered_with === "string" || Array.isArray(meta.cluster_members);
96
+ if (hasLinks && !selected.has(entry.id)) {
97
+ selected.set(entry.id, entry);
98
+ }
99
+ }
100
+
101
+ // Phase 2: Round-robin across categories, picking top-importance entries
102
+ const byCategory = new Map<string, MemoryEntry[]>();
103
+ for (const entry of entries) {
104
+ if (selected.has(entry.id)) continue;
105
+ const list = byCategory.get(entry.category) ?? [];
106
+ list.push(entry);
107
+ byCategory.set(entry.category, list);
108
+ }
109
+ // Sort each category by importance descending
110
+ for (const [, list] of byCategory) {
111
+ list.sort((a, b) => b.importance - a.importance);
112
+ }
113
+
114
+ // Round-robin: take one from each category in turn
115
+ const categories = [...byCategory.keys()];
116
+ const indices = new Map<string, number>(categories.map(c => [c, 0]));
117
+ let added = true;
118
+ while (selected.size < maxNodes && added) {
119
+ added = false;
120
+ for (const cat of categories) {
121
+ if (selected.size >= maxNodes) break;
122
+ const list = byCategory.get(cat)!;
123
+ const idx = indices.get(cat)!;
124
+ if (idx < list.length) {
125
+ selected.set(list[idx].id, list[idx]);
126
+ indices.set(cat, idx + 1);
127
+ added = true;
128
+ }
129
+ }
130
+ }
131
+
132
+ return [...selected.values()];
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Graph Builder
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export async function buildMemoryGraph(
140
+ store: Pick<MemoryStore, "list"> & Partial<Pick<MemoryStore, "getVectors">>,
141
+ options?: GraphExportOptions,
142
+ ): Promise<MemoryGraph> {
143
+ const maxNodes = options?.maxNodes ?? 200;
144
+ const scopeFilter = options?.scope ? [options.scope] : undefined;
145
+
146
+ // 1. List all entries
147
+ const entries = await store.list(scopeFilter, undefined, 10000, 0);
148
+
149
+ // 2. Filter to active entries only
150
+ const active = entries.filter(e => isActiveMemory(e.metadata));
151
+
152
+ // 3. Select nodes with diversity: prioritize connected nodes, then balance categories
153
+ const selected = selectDiverseNodes(active, maxNodes);
154
+ const nodeIds = new Set(selected.map(e => e.id));
155
+
156
+ // 4. Build nodes
157
+ const nodes: GraphNode[] = selected.map(entry => {
158
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
159
+ return {
160
+ id: entry.id,
161
+ label: truncateText(entry.text, 80),
162
+ category: entry.category,
163
+ scope: entry.scope,
164
+ importance: entry.importance,
165
+ timestamp: entry.timestamp,
166
+ accessCount: evo.accessCount,
167
+ };
168
+ });
169
+
170
+ // 5. Build edges
171
+ const edges: GraphEdge[] = [];
172
+
173
+ for (const entry of selected) {
174
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
175
+ const meta = parseMetadataSafe(entry.metadata);
176
+
177
+ // supersededBy
178
+ if (evo.supersededBy && nodeIds.has(evo.supersededBy)) {
179
+ edges.push({ source: entry.id, target: evo.supersededBy, type: "supersede" });
180
+ }
181
+
182
+ // supersedes
183
+ if (evo.supersedes && nodeIds.has(evo.supersedes)) {
184
+ edges.push({ source: evo.supersedes, target: entry.id, type: "supersede" });
185
+ }
186
+
187
+ // consolidatedInto
188
+ if (evo.consolidatedInto && nodeIds.has(evo.consolidatedInto)) {
189
+ edges.push({ source: entry.id, target: evo.consolidatedInto, type: "consolidation" });
190
+ }
191
+
192
+ // clustered_with (single ID stored at top level of metadata)
193
+ if (typeof meta.clustered_with === "string" && nodeIds.has(meta.clustered_with)) {
194
+ edges.push({ source: entry.id, target: meta.clustered_with, type: "cluster" });
195
+ }
196
+
197
+ // cluster_members (array at top level of metadata)
198
+ if (Array.isArray(meta.cluster_members)) {
199
+ for (const memberId of meta.cluster_members) {
200
+ if (typeof memberId === "string" && nodeIds.has(memberId)) {
201
+ edges.push({ source: entry.id, target: memberId, type: "cluster" });
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // 6. Scope edges — group entries by scope, connect within groups (≤ 20 per scope)
208
+ const byScope = new Map<string, string[]>();
209
+ for (const node of nodes) {
210
+ const list = byScope.get(node.scope) ?? [];
211
+ list.push(node.id);
212
+ byScope.set(node.scope, list);
213
+ }
214
+
215
+ for (const [, ids] of byScope) {
216
+ if (ids.length > 15 || ids.length < 2) continue;
217
+ // Connect each node to the next in the group (chain, not full mesh)
218
+ for (let i = 0; i < ids.length - 1; i++) {
219
+ edges.push({ source: ids[i], target: ids[i + 1], type: "scope" });
220
+ }
221
+ }
222
+
223
+ // 7. HP-narrative: Narrative edges — connect entries sharing the same generalEventId
224
+ const byGeneralEvent = new Map<string, string[]>();
225
+ for (const entry of selected) {
226
+ const narrative = parseNarrative(entry.metadata);
227
+ if (!narrative) continue;
228
+ const list = byGeneralEvent.get(narrative.generalEventId) ?? [];
229
+ list.push(entry.id);
230
+ byGeneralEvent.set(narrative.generalEventId, list);
231
+ }
232
+ for (const [, ids] of byGeneralEvent) {
233
+ if (ids.length < 2 || ids.length > 15) continue;
234
+ // Chain: connect each node to the next in the narrative group
235
+ for (let i = 0; i < ids.length - 1; i++) {
236
+ edges.push({ source: ids[i], target: ids[i + 1], type: "narrative" });
237
+ }
238
+ }
239
+
240
+ // 8. Cross-scope semantic bridges — connect entries from DIFFERENT scopes
241
+ // that are semantically similar (cosine ≥ 0.65). This reveals hidden
242
+ // cross-domain knowledge connections that the user's interdisciplinary
243
+ // thinking creates but explicit metadata doesn't capture.
244
+ // Vectors must be fetched separately since list() omits them for perf.
245
+ const SEMANTIC_BRIDGE_THRESHOLD = 0.65;
246
+ const MAX_SEMANTIC_EDGES = 30; // cap to avoid visual clutter
247
+
248
+ if (store.getVectors) {
249
+ const vectorMap = await store.getVectors(selected.map(e => e.id));
250
+ const semanticCandidates: { source: string; target: string; sim: number }[] = [];
251
+
252
+ for (let i = 0; i < selected.length; i++) {
253
+ for (let j = i + 1; j < selected.length; j++) {
254
+ const a = selected[i];
255
+ const b = selected[j];
256
+ // Only cross-scope — same-scope connections are handled by scope chains
257
+ if (a.scope === b.scope) continue;
258
+ const vecA = vectorMap.get(a.id);
259
+ const vecB = vectorMap.get(b.id);
260
+ if (!vecA || !vecB) continue;
261
+ const sim = cosineSimilarity(vecA, vecB);
262
+ if (sim >= SEMANTIC_BRIDGE_THRESHOLD) {
263
+ semanticCandidates.push({ source: a.id, target: b.id, sim });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Keep top-N by similarity to avoid clutter
269
+ semanticCandidates.sort((a, b) => b.sim - a.sim);
270
+ for (const { source, target } of semanticCandidates.slice(0, MAX_SEMANTIC_EDGES)) {
271
+ edges.push({ source, target, type: "semantic" });
272
+ }
273
+ }
274
+
275
+ // 8. Deduplicate edges
276
+ const edgeKey = (e: GraphEdge) => `${e.source}|${e.target}|${e.type}`;
277
+ const seen = new Set<string>();
278
+ const uniqueEdges: GraphEdge[] = [];
279
+ for (const edge of edges) {
280
+ const key = edgeKey(edge);
281
+ if (!seen.has(key)) {
282
+ seen.add(key);
283
+ uniqueEdges.push(edge);
284
+ }
285
+ }
286
+
287
+ return { nodes, edges: uniqueEdges };
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // HTML Renderer
292
+ // ---------------------------------------------------------------------------
293
+
294
+ export function renderGraphHTML(graph: MemoryGraph): string {
295
+ const graphJSON = JSON.stringify(graph);
296
+
297
+ return `<!DOCTYPE html>
298
+ <html lang="en">
299
+ <head>
300
+ <meta charset="UTF-8">
301
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
302
+ <title>RecallNest Knowledge Graph</title>
303
+ <style>
304
+ * { margin: 0; padding: 0; box-sizing: border-box; }
305
+ body {
306
+ background: #0d1117;
307
+ color: #c9d1d9;
308
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
309
+ overflow: hidden;
310
+ }
311
+ #header {
312
+ position: fixed;
313
+ top: 16px;
314
+ left: 16px;
315
+ z-index: 10;
316
+ pointer-events: none;
317
+ }
318
+ #header h1 {
319
+ font-size: 20px;
320
+ font-weight: 600;
321
+ color: #e6edf3;
322
+ }
323
+ #header .stats {
324
+ font-size: 13px;
325
+ color: #8b949e;
326
+ margin-top: 4px;
327
+ }
328
+ #legend {
329
+ position: fixed;
330
+ top: 16px;
331
+ right: 16px;
332
+ z-index: 10;
333
+ background: rgba(22, 27, 34, 0.9);
334
+ border: 1px solid #30363d;
335
+ border-radius: 6px;
336
+ padding: 12px 16px;
337
+ font-size: 12px;
338
+ }
339
+ #legend h3 {
340
+ font-size: 13px;
341
+ margin-bottom: 8px;
342
+ color: #e6edf3;
343
+ }
344
+ .legend-item {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 8px;
348
+ margin-bottom: 4px;
349
+ }
350
+ .legend-dot {
351
+ width: 10px;
352
+ height: 10px;
353
+ border-radius: 50%;
354
+ flex-shrink: 0;
355
+ }
356
+ .legend-line {
357
+ width: 20px;
358
+ height: 0;
359
+ border-top: 2px solid #fbbf24;
360
+ flex-shrink: 0;
361
+ }
362
+ .legend-line.dashed { border-top-style: dashed; border-top-color: #60a5fa; }
363
+ .legend-line.dotted { border-top-style: dotted; border-top-color: #9ca3af; }
364
+ #tooltip {
365
+ position: fixed;
366
+ display: none;
367
+ background: rgba(22, 27, 34, 0.95);
368
+ border: 1px solid #30363d;
369
+ border-radius: 6px;
370
+ padding: 10px 14px;
371
+ font-size: 12px;
372
+ max-width: 360px;
373
+ pointer-events: none;
374
+ z-index: 20;
375
+ line-height: 1.5;
376
+ }
377
+ #tooltip .tt-label { font-weight: 600; color: #e6edf3; margin-bottom: 4px; word-break: break-word; }
378
+ #tooltip .tt-meta { color: #8b949e; }
379
+ #tooltip .tt-meta-line { margin-top: 2px; }
380
+ svg { width: 100vw; height: 100vh; }
381
+ </style>
382
+ </head>
383
+ <body>
384
+ <div id="header">
385
+ <h1>RecallNest Knowledge Graph</h1>
386
+ <div class="stats" id="stats"></div>
387
+ </div>
388
+
389
+ <div id="legend">
390
+ <h3>Categories</h3>
391
+ <div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div><span>profile</span></div>
392
+ <div class="legend-item"><div class="legend-dot" style="background:#3b82f6"></div><span>preferences</span></div>
393
+ <div class="legend-item"><div class="legend-dot" style="background:#10b981"></div><span>entities</span></div>
394
+ <div class="legend-item"><div class="legend-dot" style="background:#6b7280"></div><span>events</span></div>
395
+ <div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div><span>cases</span></div>
396
+ <div class="legend-item"><div class="legend-dot" style="background:#8b5cf6"></div><span>patterns</span></div>
397
+ <h3 style="margin-top:10px">Edge Types</h3>
398
+ <div class="legend-item"><div class="legend-line"></div><span>supersede</span></div>
399
+ <div class="legend-item"><div class="legend-line dashed"></div><span>cluster</span></div>
400
+ <div class="legend-item"><div class="legend-line dotted"></div><span>scope</span></div>
401
+ <div class="legend-item"><div class="legend-line" style="border-top-color:#f472b6;border-top-style:dashed"></div><span>semantic bridge</span></div>
402
+ <div class="legend-item"><div class="legend-line" style="border-top-color:#34d399"></div><span>narrative</span></div>
403
+ </div>
404
+
405
+ <div id="tooltip">
406
+ <div class="tt-label" id="tt-label"></div>
407
+ <div class="tt-meta" id="tt-meta"></div>
408
+ </div>
409
+
410
+ <svg id="graph"></svg>
411
+
412
+ <script src="https://d3js.org/d3.v7.min.js"></script>
413
+ <script>
414
+ const GRAPH_DATA = ${graphJSON};
415
+
416
+ const CATEGORY_COLORS = {
417
+ profile: "#f59e0b",
418
+ preferences: "#3b82f6",
419
+ entities: "#10b981",
420
+ events: "#6b7280",
421
+ cases: "#ef4444",
422
+ patterns: "#8b5cf6",
423
+ };
424
+ const DEFAULT_COLOR = "#9ca3af";
425
+
426
+ function categoryColor(cat) {
427
+ return CATEGORY_COLORS[cat] || DEFAULT_COLOR;
428
+ }
429
+
430
+ function nodeRadius(importance) {
431
+ return 4 + importance * 8; // 4-12
432
+ }
433
+
434
+ function edgeStroke(type) {
435
+ if (type === "supersede") return "";
436
+ if (type === "cluster") return "8,4";
437
+ if (type === "consolidation") return "";
438
+ if (type === "semantic") return "6,3,2,3"; // dash-dot pattern
439
+ return "3,3";
440
+ }
441
+
442
+ function edgeColor(type) {
443
+ if (type === "supersede" || type === "consolidation") return "#fbbf24";
444
+ if (type === "cluster") return "#60a5fa";
445
+ if (type === "semantic") return "#f472b6";
446
+ if (type === "narrative") return "#34d399";
447
+ return "#9ca3af";
448
+ }
449
+
450
+ function edgeOpacity(type) {
451
+ if (type === "scope") return 0.4;
452
+ if (type === "cluster") return 0.65;
453
+ if (type === "semantic") return 0.6;
454
+ return 0.7;
455
+ }
456
+
457
+ // Stats
458
+ document.getElementById("stats").textContent =
459
+ GRAPH_DATA.nodes.length + " nodes, " + GRAPH_DATA.edges.length + " edges";
460
+
461
+ var svg = d3.select("#graph");
462
+ var width = window.innerWidth;
463
+ var height = window.innerHeight;
464
+
465
+ var g = svg.append("g");
466
+
467
+ // Zoom
468
+ var zoom = d3.zoom()
469
+ .scaleExtent([0.1, 8])
470
+ .on("zoom", function(event) { g.attr("transform", event.transform); });
471
+ svg.call(zoom);
472
+
473
+ // Simulation
474
+ var simulation = d3.forceSimulation(GRAPH_DATA.nodes)
475
+ .force("link", d3.forceLink(GRAPH_DATA.edges)
476
+ .id(function(d) { return d.id; })
477
+ .distance(80))
478
+ .force("charge", d3.forceManyBody().strength(-120))
479
+ .force("center", d3.forceCenter(width / 2, height / 2))
480
+ .force("collide", d3.forceCollide().radius(function(d) { return nodeRadius(d.importance) + 2; }));
481
+
482
+ // Edges
483
+ var link = g.append("g")
484
+ .selectAll("line")
485
+ .data(GRAPH_DATA.edges)
486
+ .join("line")
487
+ .attr("stroke", function(d) { return edgeColor(d.type); })
488
+ .attr("stroke-width", 2)
489
+ .attr("stroke-dasharray", function(d) { return edgeStroke(d.type); })
490
+ .attr("stroke-opacity", function(d) { return edgeOpacity(d.type); });
491
+
492
+ // Nodes
493
+ var node = g.append("g")
494
+ .selectAll("circle")
495
+ .data(GRAPH_DATA.nodes)
496
+ .join("circle")
497
+ .attr("r", function(d) { return nodeRadius(d.importance); })
498
+ .attr("fill", function(d) { return categoryColor(d.category); })
499
+ .attr("stroke", "#0d1117")
500
+ .attr("stroke-width", 1.5)
501
+ .call(d3.drag()
502
+ .on("start", dragstarted)
503
+ .on("drag", dragged)
504
+ .on("end", dragended));
505
+
506
+ // Tooltip
507
+ var tooltip = document.getElementById("tooltip");
508
+ var ttLabel = document.getElementById("tt-label");
509
+ var ttMeta = document.getElementById("tt-meta");
510
+
511
+ function formatAge(ts) {
512
+ var days = Math.floor((Date.now() - ts) / 86400000);
513
+ if (days < 1) return "today";
514
+ if (days === 1) return "1 day ago";
515
+ if (days < 30) return days + " days ago";
516
+ var months = Math.floor(days / 30);
517
+ return months + (months === 1 ? " month ago" : " months ago");
518
+ }
519
+
520
+ function updateTooltip(d) {
521
+ ttLabel.textContent = d.label;
522
+ // Clear previous meta lines
523
+ while (ttMeta.firstChild) ttMeta.removeChild(ttMeta.firstChild);
524
+ var lines = [
525
+ "Category: " + d.category,
526
+ "Scope: " + d.scope,
527
+ "Importance: " + d.importance.toFixed(2),
528
+ "Age: " + formatAge(d.timestamp),
529
+ "Accesses: " + d.accessCount,
530
+ ];
531
+ for (var i = 0; i < lines.length; i++) {
532
+ var div = document.createElement("div");
533
+ div.className = "tt-meta-line";
534
+ div.textContent = lines[i];
535
+ ttMeta.appendChild(div);
536
+ }
537
+ }
538
+
539
+ node
540
+ .on("mouseover", function(event, d) {
541
+ updateTooltip(d);
542
+ tooltip.style.display = "block";
543
+ })
544
+ .on("mousemove", function(event) {
545
+ tooltip.style.left = (event.clientX + 12) + "px";
546
+ tooltip.style.top = (event.clientY + 12) + "px";
547
+ })
548
+ .on("mouseout", function() {
549
+ tooltip.style.display = "none";
550
+ });
551
+
552
+ // Tick
553
+ simulation.on("tick", function() {
554
+ link
555
+ .attr("x1", function(d) { return d.source.x; })
556
+ .attr("y1", function(d) { return d.source.y; })
557
+ .attr("x2", function(d) { return d.target.x; })
558
+ .attr("y2", function(d) { return d.target.y; });
559
+ node
560
+ .attr("cx", function(d) { return d.x; })
561
+ .attr("cy", function(d) { return d.y; });
562
+ });
563
+
564
+ function dragstarted(event, d) {
565
+ if (!event.active) simulation.alphaTarget(0.3).restart();
566
+ d.fx = d.x;
567
+ d.fy = d.y;
568
+ }
569
+ function dragged(event, d) {
570
+ d.fx = event.x;
571
+ d.fy = event.y;
572
+ }
573
+ function dragended(event, d) {
574
+ if (!event.active) simulation.alphaTarget(0);
575
+ d.fx = null;
576
+ d.fy = null;
577
+ }
578
+ </script>
579
+ </body>
580
+ </html>`;
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Export
585
+ // ---------------------------------------------------------------------------
586
+
587
+ export async function exportMemoryGraph(
588
+ store: Pick<MemoryStore, "list">,
589
+ options?: GraphExportOptions,
590
+ ): Promise<{ path: string; graph: MemoryGraph }> {
591
+ const graph = await buildMemoryGraph(store, options);
592
+ const html = renderGraphHTML(graph);
593
+
594
+ const outputDir = options?.outputPath
595
+ ? resolve(options.outputPath, "..")
596
+ : resolve(metaDir(import.meta), "../data/exports");
597
+
598
+ if (!existsSync(outputDir)) {
599
+ mkdirSync(outputDir, { recursive: true });
600
+ }
601
+
602
+ const filePath = options?.outputPath
603
+ ?? join(outputDir, `memory-graph-${Date.now()}.html`);
604
+
605
+ writeFileSync(filePath, html, "utf-8");
606
+
607
+ return { path: filePath, graph };
608
+ }
609
+
610
+ // ---------------------------------------------------------------------------
611
+ // Format Result
612
+ // ---------------------------------------------------------------------------
613
+
614
+ export function formatGraphExportResult(path: string, graph: MemoryGraph): string {
615
+ // Count categories
616
+ const catCounts = new Map<string, number>();
617
+ for (const node of graph.nodes) {
618
+ catCounts.set(node.category, (catCounts.get(node.category) ?? 0) + 1);
619
+ }
620
+ const catSummary = Array.from(catCounts.entries())
621
+ .sort((a, b) => b[1] - a[1])
622
+ .map(([cat, count]) => `${cat}(${count})`)
623
+ .join(", ");
624
+
625
+ // Count edge types
626
+ const edgeTypeCounts = new Map<string, number>();
627
+ for (const edge of graph.edges) {
628
+ edgeTypeCounts.set(edge.type, (edgeTypeCounts.get(edge.type) ?? 0) + 1);
629
+ }
630
+ const semanticCount = edgeTypeCounts.get("semantic") ?? 0;
631
+ const narrativeCount = edgeTypeCounts.get("narrative") ?? 0;
632
+ const edgeDetails: string[] = [];
633
+ if (semanticCount > 0) edgeDetails.push(`${semanticCount} semantic`);
634
+ if (narrativeCount > 0) edgeDetails.push(`${narrativeCount} narrative`);
635
+ const edgeSummary = edgeDetails.length > 0
636
+ ? ` (${edgeDetails.join(", ")})`
637
+ : "";
638
+
639
+ return [
640
+ `Knowledge Graph exported: ${path}`,
641
+ `Nodes: ${graph.nodes.length} | Edges: ${graph.edges.length}${edgeSummary}`,
642
+ `Categories: ${catSummary || "none"}`,
643
+ ].join("\n");
644
+ }