@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.150

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 (43) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +24 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +135 -7
  5. package/dist/application/auto-migrate-configured-vault.js +37 -0
  6. package/dist/application/build-context.js +64 -3
  7. package/dist/application/dedupe-notes.js +226 -0
  8. package/dist/application/frontend/client-css.js +111 -47
  9. package/dist/application/frontend/client-html.js +42 -26
  10. package/dist/application/frontend/client-js.js +788 -554
  11. package/dist/application/frontend/client-render-worker-js.js +569 -0
  12. package/dist/application/frontend/client-worker-js.js +66 -0
  13. package/dist/application/get-graph-layout.js +38 -5
  14. package/dist/application/get-graph-stream-chunk.js +289 -0
  15. package/dist/application/get-graph-view.js +243 -0
  16. package/dist/application/import-legacy-sqlite.js +296 -0
  17. package/dist/application/index-vault.js +249 -21
  18. package/dist/application/offline-pack-backup.js +44 -0
  19. package/dist/application/server/routes.js +187 -5
  20. package/dist/application/start-server.js +75 -4
  21. package/dist/application/watch-vault.js +23 -2
  22. package/dist/cli/commands/agent-commands.js +7 -0
  23. package/dist/cli/commands/write-commands.js +842 -8
  24. package/dist/cli/runtime.js +10 -2
  25. package/dist/domain/context.js +54 -11
  26. package/dist/domain/graph-layout.js +275 -3
  27. package/dist/domain/markdown.js +29 -9
  28. package/dist/domain/middle-out.js +18 -0
  29. package/dist/infrastructure/config.js +117 -4
  30. package/dist/infrastructure/file-index.js +70 -3
  31. package/dist/infrastructure/file-system-vault.js +15 -0
  32. package/dist/infrastructure/index-state.js +58 -0
  33. package/dist/infrastructure/private-pack-codec.js +71 -10
  34. package/dist/infrastructure/search-packs.js +286 -15
  35. package/dist/infrastructure/vault-migration-state.js +69 -0
  36. package/dist/infrastructure/volatile-memory.js +100 -0
  37. package/dist/mcp/runtime.js +20 -0
  38. package/dist/mcp/server.js +28 -10
  39. package/dist/mcp/tools.js +110 -0
  40. package/docs/AGENT_USAGE.md +87 -3
  41. package/docs/ARCHITECTURE.md +6 -0
  42. package/docs/QUICKSTART.md +7 -0
  43. package/package.json +7 -2
@@ -0,0 +1,289 @@
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 viewportNodes = nodes.filter((node) => inViewport(node, input, padding));
111
+ const relevance = new Map();
112
+ for (let index = 0; index < viewportNodes.length; index += 1) {
113
+ const node = viewportNodes[index];
114
+ relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees));
115
+ }
116
+ const selected = selectTopByRelevance(viewportNodes, relevance, input.nodeBudget);
117
+ return selected.map((node) => [
118
+ node.id,
119
+ node.title,
120
+ node.x,
121
+ node.y,
122
+ node.group,
123
+ node.segment,
124
+ 'node',
125
+ relevance.get(node.id) ?? 0
126
+ ]);
127
+ };
128
+ const selectMidNodes = (nodes, cache, input) => {
129
+ const padding = Math.max(input.width, input.height) * (viewportPaddingFactor + 0.08);
130
+ const viewportNodes = nodes.filter((node) => inViewport(node, input, padding));
131
+ const relevance = new Map();
132
+ for (let index = 0; index < viewportNodes.length; index += 1) {
133
+ const node = viewportNodes[index];
134
+ relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees) * 1.1);
135
+ }
136
+ const selected = selectTopByRelevance(viewportNodes, relevance, input.nodeBudget);
137
+ return selected.map((node) => [
138
+ node.id,
139
+ node.title,
140
+ node.x,
141
+ node.y,
142
+ node.group,
143
+ node.segment,
144
+ 'node',
145
+ relevance.get(node.id) ?? 0
146
+ ]);
147
+ };
148
+ const selectFarClusters = (groups, input, nodeBudget) => {
149
+ const roots = groups.filter((group) => group.parentId === null);
150
+ const padding = Math.max(input.width, input.height) * 0.12;
151
+ const candidates = roots.filter((group) => inViewport(group, input, padding));
152
+ const relevance = new Map();
153
+ for (let index = 0; index < candidates.length; index += 1) {
154
+ const group = candidates[index];
155
+ relevance.set(group.id, rankGroupRelevance(group, input));
156
+ }
157
+ const selected = selectTopByRelevance(candidates, relevance, nodeBudget);
158
+ return selected.map((group) => [
159
+ `cluster:${group.id}`,
160
+ group.title,
161
+ group.x,
162
+ group.y,
163
+ group.group,
164
+ group.segment,
165
+ 'cluster',
166
+ relevance.get(group.id) ?? 0
167
+ ]);
168
+ };
169
+ const collectEdgesForNodes = (allEdges, cache, nodeRows, edgeBudget, maxEdgesPerNode) => {
170
+ const isClusterMode = nodeRows.length > 0 && nodeRows[0]?.[6] === 'cluster';
171
+ if (isClusterMode) {
172
+ const clusterIds = new Set(nodeRows.map((row) => row[0].replace(/^cluster:/, '')));
173
+ const clusterEdges = new Map();
174
+ for (let index = 0; index < allEdges.length; index += 1) {
175
+ const edge = allEdges[index];
176
+ if (!edge.target)
177
+ continue;
178
+ const sourceNode = cache.nodeById.get(edge.source);
179
+ const targetNode = cache.nodeById.get(edge.target);
180
+ if (!sourceNode || !targetNode)
181
+ continue;
182
+ const sourceCluster = sourceNode.group;
183
+ const targetCluster = targetNode.group;
184
+ if (!clusterIds.has(sourceCluster) || !clusterIds.has(targetCluster) || sourceCluster === targetCluster)
185
+ continue;
186
+ const key = sourceCluster < targetCluster ? `${sourceCluster}|${targetCluster}` : `${targetCluster}|${sourceCluster}`;
187
+ const current = clusterEdges.get(key);
188
+ if (!current || edgeRank(edge) > edgeRank(current)) {
189
+ clusterEdges.set(key, edge);
190
+ }
191
+ }
192
+ return [...clusterEdges.values()]
193
+ .sort((left, right) => edgeRank(right) - edgeRank(left))
194
+ .slice(0, Math.max(1, edgeBudget))
195
+ .map((edge) => [
196
+ `cluster:${cache.nodeById.get(edge.source)?.group ?? ''}`,
197
+ `cluster:${cache.nodeById.get(edge.target ?? '')?.group ?? ''}`,
198
+ edge.weight,
199
+ edge.priority
200
+ ])
201
+ .filter((edge) => edge[0] !== edge[1] && edge[0] !== 'cluster:' && edge[1] !== 'cluster:');
202
+ }
203
+ const nodeIds = new Set(nodeRows.map((row) => row[0]));
204
+ const collected = new Map();
205
+ for (const nodeId of nodeIds) {
206
+ const adjacency = cache.rankedAdjacencyByNodeId.get(nodeId);
207
+ if (!adjacency || adjacency.length === 0) {
208
+ continue;
209
+ }
210
+ let perNodeCount = 0;
211
+ for (let index = 0; index < adjacency.length; index += 1) {
212
+ if (perNodeCount >= maxEdgesPerNode) {
213
+ break;
214
+ }
215
+ const edge = allEdges[adjacency[index]];
216
+ if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
217
+ continue;
218
+ }
219
+ const key = edge.source < edge.target ? `${edge.source}|${edge.target}` : `${edge.target}|${edge.source}`;
220
+ const current = collected.get(key);
221
+ if (!current || edgeRank(edge) > edgeRank(current)) {
222
+ collected.set(key, edge);
223
+ }
224
+ perNodeCount += 1;
225
+ }
226
+ }
227
+ return [...collected.values()]
228
+ .sort((left, right) => edgeRank(right) - edgeRank(left))
229
+ .slice(0, Math.max(1, edgeBudget))
230
+ .map((edge) => [edge.source, edge.target ?? '', edge.weight, edge.priority])
231
+ .filter((edge) => edge[1].length > 0);
232
+ };
233
+ const normalizeBudget = (value, fallback, min, max) => {
234
+ if (!Number.isFinite(value)) {
235
+ return fallback;
236
+ }
237
+ const rounded = Math.round(value);
238
+ if (rounded < min)
239
+ return min;
240
+ if (rounded > max)
241
+ return max;
242
+ return rounded;
243
+ };
244
+ export const getGraphStreamChunk = async (vaultPath, input) => {
245
+ const nodeBudget = normalizeBudget(input.nodeBudget, 1800, 80, 12_000);
246
+ const edgeBudget = normalizeBudget(input.edgeBudget, 5000, 120, 60_000);
247
+ const { signature, layout } = await getGraphLayout(vaultPath, input.agentId);
248
+ const groups = layout.groups ?? [];
249
+ const cache = getOrCreateLayoutCache(signature, layout.nodes, layout.edges, groups);
250
+ if (layout.nodes.length === 0) {
251
+ return {
252
+ signature,
253
+ mode: 'near',
254
+ nodes: [],
255
+ edges: [],
256
+ totals: {
257
+ nodes: 0,
258
+ edges: 0
259
+ }
260
+ };
261
+ }
262
+ const mode = input.scale < farScaleThreshold
263
+ ? 'far'
264
+ : input.scale < midScaleThreshold
265
+ ? 'mid'
266
+ : 'near';
267
+ const nodes = mode === 'far' && groups.length > 0
268
+ ? selectFarClusters(groups, input, nodeBudget)
269
+ : mode === 'mid'
270
+ ? selectMidNodes(layout.nodes, cache, {
271
+ ...input,
272
+ nodeBudget
273
+ })
274
+ : selectNearNodes(layout.nodes, cache, {
275
+ ...input,
276
+ nodeBudget
277
+ });
278
+ const edges = collectEdgesForNodes(layout.edges, cache, nodes, edgeBudget, mode === 'near' ? maxNearEdgePerNode : mode === 'mid' ? maxMidEdgePerNode : maxFarEdgePerCluster);
279
+ return {
280
+ signature,
281
+ mode,
282
+ nodes,
283
+ edges,
284
+ totals: {
285
+ nodes: layout.nodes.length,
286
+ edges: layout.edges.length
287
+ }
288
+ };
289
+ };
@@ -0,0 +1,243 @@
1
+ import { getGraphLayout } from './get-graph-layout.js';
2
+ const macroScale = 0.24;
3
+ const microCoverage = 0.72;
4
+ const nodeLimit = 1000;
5
+ const edgeLimit = 1400;
6
+ const groupEdgeLimit = 900;
7
+ const inViewport = (item, input) => {
8
+ const radius = item.radius ?? 48;
9
+ return (item.x + radius >= input.x &&
10
+ item.x - radius <= input.x + input.width &&
11
+ item.y + radius >= input.y &&
12
+ item.y - radius <= input.y + input.height);
13
+ };
14
+ const groupCoverage = (group, input) => {
15
+ const viewportRadius = Math.max(input.width, input.height) / 2;
16
+ const centerX = input.x + input.width / 2;
17
+ const centerY = input.y + input.height / 2;
18
+ const centerDistance = Math.hypot(group.x - centerX, group.y - centerY);
19
+ const fitCoverage = Math.min(1, childGraphRenderRadius(group) / Math.max(viewportRadius, 1));
20
+ const centerCoverage = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
21
+ return fitCoverage * 0.72 + centerCoverage * 0.28;
22
+ };
23
+ const childGraphRenderRadius = (group) => {
24
+ const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1);
25
+ return Math.max(420, Math.min(1800, Math.sqrt(childCount) * 24));
26
+ };
27
+ const groupRenderRadius = (group) => {
28
+ const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1);
29
+ return 10 + Math.min(Math.log2(childCount + 1) * 4.2, 22);
30
+ };
31
+ const arrangeGraphLevelGroups = (groups) => {
32
+ if (groups.length <= 1) {
33
+ return groups.map((group) => ({ ...group, radius: groupRenderRadius(group) }));
34
+ }
35
+ const centerGroup = groups
36
+ .map((group) => ({
37
+ group,
38
+ score: Math.max(group.nodeIds.length, group.childGroupIds.length, 1) + group.externalEdges.length
39
+ }))
40
+ .sort((left, right) => right.score - left.score || left.group.title.localeCompare(right.group.title))[0]?.group;
41
+ const outerGroups = groups
42
+ .filter((group) => group.id !== centerGroup?.id)
43
+ .sort((left, right) => left.segment.localeCompare(right.segment) || left.title.localeCompare(right.title));
44
+ const baseRadius = Math.max(520, Math.min(2200, Math.sqrt(groups.length) * 135));
45
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5));
46
+ const arranged = centerGroup
47
+ ? [{ ...centerGroup, x: 0, y: 0, radius: groupRenderRadius(centerGroup) }]
48
+ : [];
49
+ outerGroups.forEach((group, index) => {
50
+ const ringRadius = baseRadius * Math.sqrt((index + 1) / Math.max(outerGroups.length, 1));
51
+ const angle = index * goldenAngle;
52
+ arranged.push({
53
+ ...group,
54
+ x: Math.cos(angle) * ringRadius,
55
+ y: Math.sin(angle) * ringRadius,
56
+ radius: groupRenderRadius(group)
57
+ });
58
+ });
59
+ return arranged;
60
+ };
61
+ const distanceToViewportCenter = (item, input) => {
62
+ const centerX = input.x + input.width / 2;
63
+ const centerY = input.y + input.height / 2;
64
+ return Math.hypot(item.x - centerX, item.y - centerY);
65
+ };
66
+ const selectViewportItemsWithFill = (items, input, limit = nodeLimit) => {
67
+ const visible = items.filter((item) => inViewport(item, input));
68
+ if (visible.length >= limit) {
69
+ return visible.slice(0, limit);
70
+ }
71
+ const selectedIds = new Set(visible.map((item) => item.id));
72
+ const fill = items
73
+ .filter((item) => !selectedIds.has(item.id))
74
+ .sort((left, right) => distanceToViewportCenter(left, input) - distanceToViewportCenter(right, input) || left.id.localeCompare(right.id))
75
+ .slice(0, Math.max(0, limit - visible.length));
76
+ return visible.concat(fill);
77
+ };
78
+ const groupNode = (group) => [
79
+ `group:${group.id}`,
80
+ group.title,
81
+ group.x,
82
+ group.y,
83
+ group.group,
84
+ group.segment,
85
+ 'group'
86
+ ];
87
+ const realNode = (node) => [
88
+ node.id,
89
+ node.title,
90
+ node.x,
91
+ node.y,
92
+ node.group,
93
+ node.segment,
94
+ 'node'
95
+ ];
96
+ const descendants = (group, groupById) => group.nodeIds.length > 0
97
+ ? group.nodeIds
98
+ : group.childGroupIds.flatMap((childId) => {
99
+ const child = groupById.get(childId);
100
+ return child ? descendants(child, groupById) : [];
101
+ });
102
+ const aggregateGroupEdges = (groups, edges, groupById) => {
103
+ const groupNodeByNodeId = new Map();
104
+ groups.forEach((group) => {
105
+ descendants(group, groupById).forEach((nodeId) => groupNodeByNodeId.set(nodeId, `group:${group.id}`));
106
+ });
107
+ const selected = new Map();
108
+ edges.forEach((edge) => {
109
+ if (!edge.target)
110
+ return;
111
+ const source = groupNodeByNodeId.get(edge.source);
112
+ const target = groupNodeByNodeId.get(edge.target);
113
+ if (!source || !target || source === target)
114
+ return;
115
+ const key = source < target ? `${source}|${target}` : `${target}|${source}`;
116
+ const current = selected.get(key);
117
+ if (current && current[2] >= edge.weight)
118
+ return;
119
+ selected.set(key, [source, target, edge.weight, edge.priority]);
120
+ });
121
+ const degreeCounts = new Map();
122
+ return Array.from(selected.values())
123
+ .sort((left, right) => right[2] - left[2] || left[0].localeCompare(right[0]) || left[1].localeCompare(right[1]))
124
+ .filter((edge) => {
125
+ const sourceCount = degreeCounts.get(edge[0]) ?? 0;
126
+ const targetCount = degreeCounts.get(edge[1]) ?? 0;
127
+ if (sourceCount >= 3 || targetCount >= 3) {
128
+ return false;
129
+ }
130
+ degreeCounts.set(edge[0], sourceCount + 1);
131
+ degreeCounts.set(edge[1], targetCount + 1);
132
+ return true;
133
+ })
134
+ .slice(0, groupEdgeLimit);
135
+ };
136
+ const realEdges = (edges, nodeIds) => edges
137
+ .filter((edge) => Boolean(edge.target && nodeIds.has(edge.source) && nodeIds.has(edge.target)))
138
+ .slice(0, edgeLimit)
139
+ .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
140
+ const degreeMap = (edges) => edges.reduce((degrees, edge) => {
141
+ degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edge.weight);
142
+ if (edge.target) {
143
+ degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edge.weight);
144
+ }
145
+ return degrees;
146
+ }, new Map());
147
+ const arrangeChildGraphNodes = (nodes, group, degrees) => {
148
+ if (nodes.length <= 1) {
149
+ return nodes.map((node) => ({ ...node, x: group.x, y: group.y }));
150
+ }
151
+ const centerNode = nodes
152
+ .map((node) => ({
153
+ node,
154
+ score: (degrees.get(node.id) ?? 0) + node.tags.length
155
+ }))
156
+ .sort((left, right) => right.score - left.score || left.node.title.localeCompare(right.node.title))[0]?.node;
157
+ const outerNodes = nodes
158
+ .filter((node) => node.id !== centerNode?.id)
159
+ .sort((left, right) => {
160
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
161
+ if (degreeDelta !== 0)
162
+ return degreeDelta;
163
+ return left.title.localeCompare(right.title);
164
+ });
165
+ const targetRadius = childGraphRenderRadius(group);
166
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5));
167
+ const arranged = centerNode
168
+ ? [{ ...centerNode, x: group.x, y: group.y }]
169
+ : [];
170
+ outerNodes.forEach((node, index) => {
171
+ const ringRadius = targetRadius * Math.sqrt((index + 1) / Math.max(outerNodes.length, 1));
172
+ const angle = index * goldenAngle;
173
+ arranged.push({
174
+ ...node,
175
+ x: group.x + Math.cos(angle) * ringRadius,
176
+ y: group.y + Math.sin(angle) * ringRadius
177
+ });
178
+ });
179
+ return arranged;
180
+ };
181
+ const limitEdges = (edges) => edges.slice(0, edgeLimit);
182
+ export const getGraphView = async (vaultPath, input) => {
183
+ const { signature, layout } = await getGraphLayout(vaultPath, input.agentId);
184
+ const groups = layout.groups ?? [];
185
+ const degrees = degreeMap(layout.edges);
186
+ const groupById = new Map(groups.map((group) => [group.id, group]));
187
+ if (groups.length === 0) {
188
+ const nodes = layout.nodes.filter((node) => inViewport(node, input)).slice(0, nodeLimit);
189
+ const nodeIds = new Set(nodes.map((node) => node.id));
190
+ const viewNodes = nodes.map(realNode);
191
+ return {
192
+ signature,
193
+ mode: 'flat',
194
+ nodes: viewNodes,
195
+ edges: limitEdges(realEdges(layout.edges, nodeIds)),
196
+ totals: {
197
+ nodes: layout.nodes.length,
198
+ edges: layout.edges.length
199
+ }
200
+ };
201
+ }
202
+ const rootGroups = arrangeGraphLevelGroups(groups.filter((group) => group.parentId === null));
203
+ const leafGroups = arrangeGraphLevelGroups(groups.filter((group) => group.nodeIds.length > 0));
204
+ const visibleGroups = selectViewportItemsWithFill(rootGroups, input);
205
+ const focused = leafGroups
206
+ .filter((group) => group.nodeIds.length > 0 && inViewport(group, input))
207
+ .map((group) => ({ group, coverage: groupCoverage(group, input) }))
208
+ .sort((left, right) => right.coverage - left.coverage)[0];
209
+ if (focused && input.scale >= macroScale && focused.coverage >= microCoverage) {
210
+ const nodeIds = new Set(focused.group.nodeIds);
211
+ const arrangedNodes = arrangeChildGraphNodes(layout.nodes.filter((node) => nodeIds.has(node.id)), focused.group, degrees);
212
+ const nodesInViewport = arrangedNodes
213
+ .filter((node) => inViewport(node, input))
214
+ .slice(0, nodeLimit);
215
+ const nodes = nodesInViewport.length > 0
216
+ ? nodesInViewport
217
+ : arrangedNodes.slice(0, nodeLimit);
218
+ const visibleNodeIds = new Set(nodes.map((node) => node.id));
219
+ const viewNodes = nodes.map(realNode);
220
+ return {
221
+ signature,
222
+ mode: 'micro',
223
+ nodes: viewNodes,
224
+ edges: limitEdges(realEdges(layout.edges, visibleNodeIds)),
225
+ totals: {
226
+ nodes: layout.nodes.length,
227
+ edges: layout.edges.length
228
+ }
229
+ };
230
+ }
231
+ const groupsToRender = visibleGroups.slice(0, nodeLimit);
232
+ const viewNodes = groupsToRender.map(groupNode);
233
+ return {
234
+ signature,
235
+ mode: 'macro',
236
+ nodes: viewNodes,
237
+ edges: limitEdges(aggregateGroupEdges(groupsToRender, layout.edges, groupById)),
238
+ totals: {
239
+ nodes: layout.nodes.length,
240
+ edges: layout.edges.length
241
+ }
242
+ };
243
+ };