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.
- package/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- 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
|
+
}
|