@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.161
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/AGENTS.md +9 -6
- package/CHANGELOG.md +27 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +177 -20
- package/dist/application/add-note.js +13 -44
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +258 -51
- package/dist/application/frontend/client-html.js +50 -27
- package/dist/application/frontend/client-js.js +1369 -605
- package/dist/application/frontend/client-render-worker-js.js +645 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-contexts.js +33 -0
- package/dist/application/get-graph-layout.js +62 -8
- package/dist/application/get-graph-stream-chunk.js +326 -0
- package/dist/application/get-graph-view.js +246 -0
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/import-legacy-sqlite.js +266 -0
- package/dist/application/index-vault.js +262 -23
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +247 -7
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +924 -14
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +389 -18
- package/dist/domain/markdown.js +53 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +121 -4
- package/dist/infrastructure/file-index.js +76 -6
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +39 -11
- package/dist/mcp/tools.js +183 -7
- package/docs/AGENT_USAGE.md +96 -5
- package/docs/ARCHITECTURE.md +8 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getGraphLayout } from './get-graph-layout.js';
|
|
2
|
+
export const getGraphContexts = async (vaultPath, agentId) => {
|
|
3
|
+
const { layout } = await getGraphLayout(vaultPath, { agentId });
|
|
4
|
+
const nodeIdsByContext = new Map();
|
|
5
|
+
const contextByNodeId = new Map();
|
|
6
|
+
layout.nodes.forEach((node) => {
|
|
7
|
+
const title = node.segment || node.group || 'root';
|
|
8
|
+
const nodeIds = nodeIdsByContext.get(title) ?? new Set();
|
|
9
|
+
nodeIds.add(node.id);
|
|
10
|
+
nodeIdsByContext.set(title, nodeIds);
|
|
11
|
+
contextByNodeId.set(node.id, title);
|
|
12
|
+
});
|
|
13
|
+
const edgeCountByContext = new Map();
|
|
14
|
+
layout.edges.forEach((edge) => {
|
|
15
|
+
if (!edge.target) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const sourceContext = contextByNodeId.get(edge.source);
|
|
19
|
+
const targetContext = contextByNodeId.get(edge.target);
|
|
20
|
+
if (!sourceContext || sourceContext !== targetContext) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
edgeCountByContext.set(sourceContext, (edgeCountByContext.get(sourceContext) ?? 0) + 1);
|
|
24
|
+
});
|
|
25
|
+
return Array.from(nodeIdsByContext.entries())
|
|
26
|
+
.map(([title, nodeIds]) => ({
|
|
27
|
+
id: title,
|
|
28
|
+
title,
|
|
29
|
+
nodeCount: nodeIds.size,
|
|
30
|
+
edgeCount: edgeCountByContext.get(title) ?? 0
|
|
31
|
+
}))
|
|
32
|
+
.sort((left, right) => right.nodeCount - left.nodeCount || left.title.localeCompare(right.title));
|
|
33
|
+
};
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { addVisualContextEdges } from '../domain/graph-contexts.js';
|
|
3
5
|
import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
|
|
4
6
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
5
7
|
import { getGraphSummary } from './get-graph-summary.js';
|
|
8
|
+
const graphLayoutVersion = 9;
|
|
6
9
|
const graphLayoutCache = new Map();
|
|
10
|
+
const safeCacheSegment = (value, fallback) => value?.replace(/[^a-zA-Z0-9_-]/g, '_') || fallback;
|
|
11
|
+
const graphLayoutStoragePath = (vaultPath, options) => {
|
|
12
|
+
const agent = safeCacheSegment(options.agentId, 'all');
|
|
13
|
+
const context = safeCacheSegment(options.context, 'all-contexts');
|
|
14
|
+
return join(vaultPath, '.brainlink', `graph-layout-${agent}-${context}.json`);
|
|
15
|
+
};
|
|
16
|
+
const readPersistedLayout = async (vaultPath, databaseSignature, options) => {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath, options), 'utf8'));
|
|
19
|
+
return parsed.databaseSignature === databaseSignature && parsed.layoutVersion === graphLayoutVersion ? parsed : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const writePersistedLayout = async (vaultPath, options, cached) => {
|
|
26
|
+
const target = graphLayoutStoragePath(vaultPath, options);
|
|
27
|
+
const temp = `${target}.tmp`;
|
|
28
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
29
|
+
await writeFile(temp, `${JSON.stringify(cached)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
30
|
+
await rename(temp, target);
|
|
31
|
+
};
|
|
7
32
|
const readDatabaseSignature = async (vaultPath) => {
|
|
8
33
|
try {
|
|
9
34
|
const info = await stat(indexStoragePath(vaultPath));
|
|
@@ -22,20 +47,49 @@ const createGraphSignature = (graph) => {
|
|
|
22
47
|
.update(`${graph.nodes.length}:${nodesSignature}|${graph.edges.length}:${edgesSignature}`)
|
|
23
48
|
.digest('hex');
|
|
24
49
|
};
|
|
25
|
-
|
|
50
|
+
const createLayout = (graph) => {
|
|
51
|
+
const rawLayout = createCauliflowerGraphLayout(graph);
|
|
52
|
+
return {
|
|
53
|
+
...rawLayout,
|
|
54
|
+
nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const filterGraphByContext = (graph, context) => {
|
|
58
|
+
const baseLayout = createCauliflowerGraphLayout(graph);
|
|
59
|
+
const selectedNodeIds = new Set(baseLayout.nodes
|
|
60
|
+
.filter((node) => node.segment === context)
|
|
61
|
+
.map((node) => node.id));
|
|
62
|
+
return {
|
|
63
|
+
nodes: graph.nodes.filter((node) => selectedNodeIds.has(node.id)),
|
|
64
|
+
edges: graph.edges.filter((edge) => selectedNodeIds.has(edge.source) && Boolean(edge.target && selectedNodeIds.has(edge.target)))
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export const getGraphLayout = async (vaultPath, optionsOrAgentId) => {
|
|
68
|
+
const options = typeof optionsOrAgentId === 'string' ? { agentId: optionsOrAgentId } : optionsOrAgentId ?? {};
|
|
26
69
|
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
27
|
-
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
70
|
+
const cacheKey = `${vaultPath}:${options.agentId ?? ''}:${options.context ?? ''}`;
|
|
28
71
|
const cached = graphLayoutCache.get(cacheKey);
|
|
29
|
-
if (cached?.databaseSignature === databaseSignature) {
|
|
72
|
+
if (cached?.databaseSignature === databaseSignature && cached.layoutVersion === graphLayoutVersion) {
|
|
30
73
|
return {
|
|
31
74
|
signature: cached.signature,
|
|
32
75
|
layout: cached.layout
|
|
33
76
|
};
|
|
34
77
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
const persisted = await readPersistedLayout(vaultPath, databaseSignature, options);
|
|
79
|
+
if (persisted) {
|
|
80
|
+
graphLayoutCache.set(cacheKey, persisted);
|
|
81
|
+
return {
|
|
82
|
+
signature: persisted.signature,
|
|
83
|
+
layout: persisted.layout
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const graph = addVisualContextEdges(await getGraphSummary(vaultPath, options.agentId));
|
|
87
|
+
const scopedGraph = options.context ? filterGraphByContext(graph, options.context) : graph;
|
|
88
|
+
const signature = createGraphSignature(scopedGraph);
|
|
89
|
+
const layout = createLayout(scopedGraph);
|
|
90
|
+
const nextCache = { layoutVersion: graphLayoutVersion, databaseSignature, signature, layout };
|
|
91
|
+
graphLayoutCache.set(cacheKey, nextCache);
|
|
92
|
+
await writePersistedLayout(vaultPath, options, nextCache);
|
|
39
93
|
return {
|
|
40
94
|
signature,
|
|
41
95
|
layout
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { getGraphLayout } from './get-graph-layout.js';
|
|
2
|
+
const layoutCacheBySignature = new Map();
|
|
3
|
+
const maxLayoutCacheEntries = 6;
|
|
4
|
+
const farScaleThreshold = 0.22;
|
|
5
|
+
const midScaleThreshold = 0.78;
|
|
6
|
+
const viewportPaddingFactor = 0.18;
|
|
7
|
+
const maxNearEdgePerNode = 24;
|
|
8
|
+
const maxMidEdgePerNode = 12;
|
|
9
|
+
const maxFarEdgePerCluster = 8;
|
|
10
|
+
const inViewport = (item, input, padding) => {
|
|
11
|
+
const radius = item.radius ?? 24;
|
|
12
|
+
const paddedX = input.x - padding;
|
|
13
|
+
const paddedY = input.y - padding;
|
|
14
|
+
const paddedWidth = input.width + padding * 2;
|
|
15
|
+
const paddedHeight = input.height + padding * 2;
|
|
16
|
+
return (item.x + radius >= paddedX &&
|
|
17
|
+
item.x - radius <= paddedX + paddedWidth &&
|
|
18
|
+
item.y + radius >= paddedY &&
|
|
19
|
+
item.y - radius <= paddedY + paddedHeight);
|
|
20
|
+
};
|
|
21
|
+
const nodeDistanceToCenter = (item, input) => {
|
|
22
|
+
const cx = input.x + input.width / 2;
|
|
23
|
+
const cy = input.y + input.height / 2;
|
|
24
|
+
return Math.hypot(item.x - cx, item.y - cy);
|
|
25
|
+
};
|
|
26
|
+
const rankNodeRelevance = (node, input, degrees) => {
|
|
27
|
+
const degree = degrees.get(node.id) ?? 0;
|
|
28
|
+
const centerDistance = nodeDistanceToCenter(node, input);
|
|
29
|
+
const viewportRadius = Math.max(input.width, input.height) / 2;
|
|
30
|
+
const centerScore = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
|
|
31
|
+
const degreeScore = Math.log1p(Math.max(0, degree));
|
|
32
|
+
const tagScore = Math.min(node.tags.length, 6) * 0.18;
|
|
33
|
+
return degreeScore * 1.1 + centerScore * 1.3 + tagScore;
|
|
34
|
+
};
|
|
35
|
+
const rankGroupRelevance = (group, input) => {
|
|
36
|
+
const centerDistance = nodeDistanceToCenter(group, input);
|
|
37
|
+
const viewportRadius = Math.max(input.width, input.height) / 2;
|
|
38
|
+
const centerScore = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
|
|
39
|
+
const massScore = Math.log1p(group.nodeIds.length + group.childGroupIds.length * 2);
|
|
40
|
+
const edgeScore = Math.log1p(group.externalEdges.length + group.internalEdges.length);
|
|
41
|
+
return centerScore * 1.3 + massScore + edgeScore * 0.5;
|
|
42
|
+
};
|
|
43
|
+
const edgePriorityScore = (priority) => {
|
|
44
|
+
if (priority === 'critical')
|
|
45
|
+
return 4;
|
|
46
|
+
if (priority === 'high')
|
|
47
|
+
return 3;
|
|
48
|
+
if (priority === 'normal')
|
|
49
|
+
return 2;
|
|
50
|
+
return 1;
|
|
51
|
+
};
|
|
52
|
+
const edgeRank = (edge) => edge.weight * 1.35 + edgePriorityScore(edge.priority);
|
|
53
|
+
const createLayoutCache = (signature, nodes, edges, groups) => {
|
|
54
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
55
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
56
|
+
const degrees = new Map();
|
|
57
|
+
const adjacencyByNodeId = new Map();
|
|
58
|
+
const rankedAdjacencyByNodeId = new Map();
|
|
59
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
60
|
+
const edge = edges[index];
|
|
61
|
+
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edge.weight);
|
|
62
|
+
const sourceAdjacency = adjacencyByNodeId.get(edge.source) ?? [];
|
|
63
|
+
sourceAdjacency.push(index);
|
|
64
|
+
adjacencyByNodeId.set(edge.source, sourceAdjacency);
|
|
65
|
+
if (!edge.target) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edge.weight);
|
|
69
|
+
const targetAdjacency = adjacencyByNodeId.get(edge.target) ?? [];
|
|
70
|
+
targetAdjacency.push(index);
|
|
71
|
+
adjacencyByNodeId.set(edge.target, targetAdjacency);
|
|
72
|
+
}
|
|
73
|
+
for (const [nodeId, adjacency] of adjacencyByNodeId.entries()) {
|
|
74
|
+
rankedAdjacencyByNodeId.set(nodeId, [...adjacency].sort((leftIndex, rightIndex) => edgeRank(edges[rightIndex]) - edgeRank(edges[leftIndex])));
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
signature,
|
|
78
|
+
degrees,
|
|
79
|
+
nodeById,
|
|
80
|
+
groupById,
|
|
81
|
+
adjacencyByNodeId,
|
|
82
|
+
rankedAdjacencyByNodeId
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const getOrCreateLayoutCache = (signature, nodes, edges, groups) => {
|
|
86
|
+
const cached = layoutCacheBySignature.get(signature);
|
|
87
|
+
if (cached) {
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
const next = createLayoutCache(signature, nodes, edges, groups);
|
|
91
|
+
layoutCacheBySignature.set(signature, next);
|
|
92
|
+
while (layoutCacheBySignature.size > maxLayoutCacheEntries) {
|
|
93
|
+
const oldest = layoutCacheBySignature.keys().next().value;
|
|
94
|
+
if (!oldest)
|
|
95
|
+
break;
|
|
96
|
+
layoutCacheBySignature.delete(oldest);
|
|
97
|
+
}
|
|
98
|
+
return next;
|
|
99
|
+
};
|
|
100
|
+
const selectTopByRelevance = (items, relevanceById, budget) => [...items]
|
|
101
|
+
.sort((left, right) => {
|
|
102
|
+
const relevanceDelta = (relevanceById.get(right.id) ?? 0) - (relevanceById.get(left.id) ?? 0);
|
|
103
|
+
if (relevanceDelta !== 0)
|
|
104
|
+
return relevanceDelta;
|
|
105
|
+
return left.id.localeCompare(right.id);
|
|
106
|
+
})
|
|
107
|
+
.slice(0, Math.max(1, budget));
|
|
108
|
+
const selectNearNodes = (nodes, cache, input) => {
|
|
109
|
+
const padding = Math.max(input.width, input.height) * viewportPaddingFactor;
|
|
110
|
+
const candidateNodes = nodes.length <= input.nodeBudget
|
|
111
|
+
? nodes
|
|
112
|
+
: nodes.filter((node) => inViewport(node, input, padding));
|
|
113
|
+
const relevance = new Map();
|
|
114
|
+
for (let index = 0; index < candidateNodes.length; index += 1) {
|
|
115
|
+
const node = candidateNodes[index];
|
|
116
|
+
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees));
|
|
117
|
+
}
|
|
118
|
+
const selected = selectTopByRelevance(candidateNodes, relevance, input.nodeBudget);
|
|
119
|
+
return selected.map((node) => [
|
|
120
|
+
node.id,
|
|
121
|
+
node.title,
|
|
122
|
+
node.x,
|
|
123
|
+
node.y,
|
|
124
|
+
node.group,
|
|
125
|
+
node.segment,
|
|
126
|
+
'node',
|
|
127
|
+
relevance.get(node.id) ?? 0
|
|
128
|
+
]);
|
|
129
|
+
};
|
|
130
|
+
const selectMidNodes = (nodes, cache, input) => {
|
|
131
|
+
const padding = Math.max(input.width, input.height) * (viewportPaddingFactor + 0.08);
|
|
132
|
+
const candidateNodes = nodes.length <= input.nodeBudget
|
|
133
|
+
? nodes
|
|
134
|
+
: nodes.filter((node) => inViewport(node, input, padding));
|
|
135
|
+
const relevance = new Map();
|
|
136
|
+
for (let index = 0; index < candidateNodes.length; index += 1) {
|
|
137
|
+
const node = candidateNodes[index];
|
|
138
|
+
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees) * 1.1);
|
|
139
|
+
}
|
|
140
|
+
const selected = selectTopByRelevance(candidateNodes, relevance, input.nodeBudget);
|
|
141
|
+
return selected.map((node) => [
|
|
142
|
+
node.id,
|
|
143
|
+
node.title,
|
|
144
|
+
node.x,
|
|
145
|
+
node.y,
|
|
146
|
+
node.group,
|
|
147
|
+
node.segment,
|
|
148
|
+
'node',
|
|
149
|
+
relevance.get(node.id) ?? 0
|
|
150
|
+
]);
|
|
151
|
+
};
|
|
152
|
+
const selectFarClusters = (nodes, input, nodeBudget) => {
|
|
153
|
+
const roots = createSegmentClusters(nodes);
|
|
154
|
+
const relevance = new Map();
|
|
155
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
156
|
+
const group = roots[index];
|
|
157
|
+
relevance.set(group.id, rankGroupRelevance(group, input));
|
|
158
|
+
}
|
|
159
|
+
const selected = selectTopByRelevance(roots, relevance, nodeBudget);
|
|
160
|
+
return selected.map((group) => [
|
|
161
|
+
`cluster:${group.id}`,
|
|
162
|
+
group.title,
|
|
163
|
+
group.x,
|
|
164
|
+
group.y,
|
|
165
|
+
group.group,
|
|
166
|
+
group.segment,
|
|
167
|
+
'cluster',
|
|
168
|
+
relevance.get(group.id) ?? 0
|
|
169
|
+
]);
|
|
170
|
+
};
|
|
171
|
+
const createSegmentClusters = (nodes) => {
|
|
172
|
+
const nodesBySegment = new Map();
|
|
173
|
+
nodes.forEach((node) => {
|
|
174
|
+
const bucket = nodesBySegment.get(node.segment) ?? [];
|
|
175
|
+
bucket.push(node);
|
|
176
|
+
nodesBySegment.set(node.segment, bucket);
|
|
177
|
+
});
|
|
178
|
+
return Array.from(nodesBySegment.entries()).map(([segment, segmentNodes]) => {
|
|
179
|
+
const center = segmentNodes.reduce((state, node) => ({
|
|
180
|
+
x: state.x + node.x,
|
|
181
|
+
y: state.y + node.y
|
|
182
|
+
}), { x: 0, y: 0 });
|
|
183
|
+
const x = center.x / Math.max(segmentNodes.length, 1);
|
|
184
|
+
const y = center.y / Math.max(segmentNodes.length, 1);
|
|
185
|
+
const radius = segmentNodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 80);
|
|
186
|
+
return {
|
|
187
|
+
id: `segment:${segment}`,
|
|
188
|
+
level: 0,
|
|
189
|
+
parentId: null,
|
|
190
|
+
title: segment,
|
|
191
|
+
segment,
|
|
192
|
+
group: segmentNodes[0]?.group ?? segment,
|
|
193
|
+
x,
|
|
194
|
+
y,
|
|
195
|
+
radius: Math.max(radius + 96, 160),
|
|
196
|
+
nodeIds: segmentNodes.map((node) => node.id),
|
|
197
|
+
childGroupIds: [],
|
|
198
|
+
internalEdges: [],
|
|
199
|
+
externalEdges: []
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const collectEdgesForNodes = (allEdges, cache, nodeRows, edgeBudget, maxEdgesPerNode) => {
|
|
204
|
+
const isClusterMode = nodeRows.length > 0 && nodeRows[0]?.[6] === 'cluster';
|
|
205
|
+
if (isClusterMode) {
|
|
206
|
+
const clusterSegments = new Set(nodeRows.map((row) => row[5]));
|
|
207
|
+
const clusterEdges = new Map();
|
|
208
|
+
for (let index = 0; index < allEdges.length; index += 1) {
|
|
209
|
+
const edge = allEdges[index];
|
|
210
|
+
if (!edge.target)
|
|
211
|
+
continue;
|
|
212
|
+
const sourceNode = cache.nodeById.get(edge.source);
|
|
213
|
+
const targetNode = cache.nodeById.get(edge.target);
|
|
214
|
+
if (!sourceNode || !targetNode)
|
|
215
|
+
continue;
|
|
216
|
+
const sourceCluster = sourceNode.segment;
|
|
217
|
+
const targetCluster = targetNode.segment;
|
|
218
|
+
if (!clusterSegments.has(sourceCluster) || !clusterSegments.has(targetCluster) || sourceCluster === targetCluster)
|
|
219
|
+
continue;
|
|
220
|
+
const key = sourceCluster < targetCluster ? `${sourceCluster}|${targetCluster}` : `${targetCluster}|${sourceCluster}`;
|
|
221
|
+
const current = clusterEdges.get(key);
|
|
222
|
+
if (!current || edgeRank(edge) > edgeRank(current)) {
|
|
223
|
+
clusterEdges.set(key, edge);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...clusterEdges.values()]
|
|
227
|
+
.sort((left, right) => edgeRank(right) - edgeRank(left))
|
|
228
|
+
.slice(0, Math.max(1, edgeBudget))
|
|
229
|
+
.map((edge) => [
|
|
230
|
+
`cluster:segment:${cache.nodeById.get(edge.source)?.segment ?? ''}`,
|
|
231
|
+
`cluster:segment:${cache.nodeById.get(edge.target ?? '')?.segment ?? ''}`,
|
|
232
|
+
edge.weight,
|
|
233
|
+
edge.priority
|
|
234
|
+
])
|
|
235
|
+
.filter((edge) => edge[0] !== edge[1] && edge[0] !== 'cluster:segment:' && edge[1] !== 'cluster:segment:');
|
|
236
|
+
}
|
|
237
|
+
const nodeIds = new Set(nodeRows.map((row) => row[0]));
|
|
238
|
+
const collected = new Map();
|
|
239
|
+
for (const nodeId of nodeIds) {
|
|
240
|
+
const adjacency = cache.rankedAdjacencyByNodeId.get(nodeId);
|
|
241
|
+
if (!adjacency || adjacency.length === 0) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let perNodeCount = 0;
|
|
245
|
+
for (let index = 0; index < adjacency.length; index += 1) {
|
|
246
|
+
if (perNodeCount >= maxEdgesPerNode) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
const edge = allEdges[adjacency[index]];
|
|
250
|
+
if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const key = edge.source < edge.target ? `${edge.source}|${edge.target}` : `${edge.target}|${edge.source}`;
|
|
254
|
+
const current = collected.get(key);
|
|
255
|
+
if (!current || edgeRank(edge) > edgeRank(current)) {
|
|
256
|
+
collected.set(key, edge);
|
|
257
|
+
}
|
|
258
|
+
perNodeCount += 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return [...collected.values()]
|
|
262
|
+
.sort((left, right) => edgeRank(right) - edgeRank(left))
|
|
263
|
+
.slice(0, Math.max(1, edgeBudget))
|
|
264
|
+
.map((edge) => [edge.source, edge.target ?? '', edge.weight, edge.priority])
|
|
265
|
+
.filter((edge) => edge[1].length > 0);
|
|
266
|
+
};
|
|
267
|
+
const normalizeBudget = (value, fallback, min, max) => {
|
|
268
|
+
if (!Number.isFinite(value)) {
|
|
269
|
+
return fallback;
|
|
270
|
+
}
|
|
271
|
+
const rounded = Math.round(value);
|
|
272
|
+
if (rounded < min)
|
|
273
|
+
return min;
|
|
274
|
+
if (rounded > max)
|
|
275
|
+
return max;
|
|
276
|
+
return rounded;
|
|
277
|
+
};
|
|
278
|
+
export const getGraphStreamChunk = async (vaultPath, input) => {
|
|
279
|
+
const nodeBudget = normalizeBudget(input.nodeBudget, 1800, 80, 12_000);
|
|
280
|
+
const edgeBudget = normalizeBudget(input.edgeBudget, 5000, 120, 60_000);
|
|
281
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
282
|
+
agentId: input.agentId,
|
|
283
|
+
context: input.context
|
|
284
|
+
});
|
|
285
|
+
const groups = layout.groups ?? [];
|
|
286
|
+
const cache = getOrCreateLayoutCache(signature, layout.nodes, layout.edges, groups);
|
|
287
|
+
if (layout.nodes.length === 0) {
|
|
288
|
+
return {
|
|
289
|
+
signature,
|
|
290
|
+
mode: 'near',
|
|
291
|
+
nodes: [],
|
|
292
|
+
edges: [],
|
|
293
|
+
totals: {
|
|
294
|
+
nodes: 0,
|
|
295
|
+
edges: 0
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const mode = input.scale < farScaleThreshold
|
|
300
|
+
? 'far'
|
|
301
|
+
: input.scale < midScaleThreshold
|
|
302
|
+
? 'mid'
|
|
303
|
+
: 'near';
|
|
304
|
+
const nodes = mode === 'far'
|
|
305
|
+
? selectFarClusters(layout.nodes, input, nodeBudget)
|
|
306
|
+
: mode === 'mid'
|
|
307
|
+
? selectMidNodes(layout.nodes, cache, {
|
|
308
|
+
...input,
|
|
309
|
+
nodeBudget
|
|
310
|
+
})
|
|
311
|
+
: selectNearNodes(layout.nodes, cache, {
|
|
312
|
+
...input,
|
|
313
|
+
nodeBudget
|
|
314
|
+
});
|
|
315
|
+
const edges = collectEdgesForNodes(layout.edges, cache, nodes, edgeBudget, mode === 'near' ? maxNearEdgePerNode : mode === 'mid' ? maxMidEdgePerNode : maxFarEdgePerCluster);
|
|
316
|
+
return {
|
|
317
|
+
signature,
|
|
318
|
+
mode,
|
|
319
|
+
nodes,
|
|
320
|
+
edges,
|
|
321
|
+
totals: {
|
|
322
|
+
nodes: layout.nodes.length,
|
|
323
|
+
edges: layout.edges.length
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|