@andespindola/brainlink 0.1.0-beta.13 → 0.1.0-beta.130
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 +8 -5
- package/CHANGELOG.md +26 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +143 -20
- package/SECURITY.md +1 -1
- package/dist/application/analyze-vault.js +1 -15
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +93 -45
- package/dist/application/frontend/client-html.js +34 -25
- package/dist/application/frontend/client-js.js +2724 -182
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +39 -6
- package/dist/application/get-graph-node.js +3 -3
- package/dist/application/get-graph-summary.js +3 -3
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +253 -25
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +3 -3
- package/dist/application/search-knowledge.js +6 -6
- package/dist/application/server/routes.js +105 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +842 -8
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +181 -3
- package/dist/domain/markdown.js +29 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +38 -0
- package/dist/infrastructure/file-index.js +358 -0
- 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 +313 -17
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/server.js +21 -1
- package/dist/mcp/tools.js +96 -0
- package/docs/AGENT_USAGE.md +101 -18
- package/docs/ARCHITECTURE.md +22 -27
- package/docs/QUICKSTART.md +7 -0
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -267
- package/dist/infrastructure/sqlite/recovery.js +0 -163
- package/dist/infrastructure/sqlite/schema.js +0 -114
- package/dist/infrastructure/sqlite/search-reader.js +0 -188
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -38
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const createClientWorkerJs = () => `const normalize = value => String(value || '')
|
|
2
|
+
.normalize('NFKD')
|
|
3
|
+
.replace(/\\p{Diacritic}/gu, '')
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
|
|
6
|
+
let nodeIndex = []
|
|
7
|
+
|
|
8
|
+
const toNodeIndex = nodes =>
|
|
9
|
+
(Array.isArray(nodes) ? nodes : [])
|
|
10
|
+
.map(node => {
|
|
11
|
+
const id = typeof node.id === 'string' ? node.id : ''
|
|
12
|
+
if (!id) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
const title = normalize(node.title)
|
|
16
|
+
const path = normalize(node.path)
|
|
17
|
+
const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
text: [title, path, ...tags].join(' ')
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
|
|
25
|
+
const scoreText = (text, query) => {
|
|
26
|
+
if (!query) return 0
|
|
27
|
+
if (!text.includes(query)) return 0
|
|
28
|
+
if (text.startsWith(query)) return 4
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filterIds = (query, limit) => {
|
|
33
|
+
const normalizedQuery = normalize(query).trim()
|
|
34
|
+
if (!normalizedQuery) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
const rows = []
|
|
38
|
+
for (let index = 0; index < nodeIndex.length; index += 1) {
|
|
39
|
+
const row = nodeIndex[index]
|
|
40
|
+
const score = scoreText(row.text, normalizedQuery)
|
|
41
|
+
if (score > 0) {
|
|
42
|
+
rows.push({ id: row.id, score })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
46
|
+
return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = event => {
|
|
50
|
+
const payload = event.data
|
|
51
|
+
if (!payload || typeof payload !== 'object') {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (payload.type === 'load-nodes') {
|
|
55
|
+
nodeIndex = toNodeIndex(payload.nodes)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (payload.type === 'filter') {
|
|
59
|
+
const token = payload.token
|
|
60
|
+
const ids = filterIds(payload.query, payload.limit)
|
|
61
|
+
self.postMessage({ type: 'filter-result', token, ids })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.postMessage({ type: 'ready' })
|
|
66
|
+
`;
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
|
|
5
|
+
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
5
6
|
import { getGraphSummary } from './get-graph-summary.js';
|
|
7
|
+
const graphLayoutVersion = 2;
|
|
6
8
|
const graphLayoutCache = new Map();
|
|
9
|
+
const graphLayoutStoragePath = (vaultPath, agentId) => join(vaultPath, '.brainlink', `graph-layout-${agentId?.replace(/[^a-zA-Z0-9_-]/g, '_') ?? 'all'}.json`);
|
|
10
|
+
const readPersistedLayout = async (vaultPath, databaseSignature, agentId) => {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath, agentId), 'utf8'));
|
|
13
|
+
return parsed.databaseSignature === databaseSignature && parsed.layoutVersion === graphLayoutVersion ? parsed : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const writePersistedLayout = async (vaultPath, agentId, cached) => {
|
|
20
|
+
const target = graphLayoutStoragePath(vaultPath, agentId);
|
|
21
|
+
const temp = `${target}.tmp`;
|
|
22
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
23
|
+
await writeFile(temp, `${JSON.stringify(cached)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
24
|
+
await rename(temp, target);
|
|
25
|
+
};
|
|
7
26
|
const readDatabaseSignature = async (vaultPath) => {
|
|
8
27
|
try {
|
|
9
|
-
const info = await stat(
|
|
28
|
+
const info = await stat(indexStoragePath(vaultPath));
|
|
10
29
|
return `${Math.floor(info.mtimeMs)}:${info.size}`;
|
|
11
30
|
}
|
|
12
31
|
catch {
|
|
@@ -26,16 +45,30 @@ export const getGraphLayout = async (vaultPath, agentId) => {
|
|
|
26
45
|
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
27
46
|
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
28
47
|
const cached = graphLayoutCache.get(cacheKey);
|
|
29
|
-
if (cached?.databaseSignature === databaseSignature) {
|
|
48
|
+
if (cached?.databaseSignature === databaseSignature && cached.layoutVersion === graphLayoutVersion) {
|
|
30
49
|
return {
|
|
31
50
|
signature: cached.signature,
|
|
32
51
|
layout: cached.layout
|
|
33
52
|
};
|
|
34
53
|
}
|
|
54
|
+
const persisted = await readPersistedLayout(vaultPath, databaseSignature, agentId);
|
|
55
|
+
if (persisted) {
|
|
56
|
+
graphLayoutCache.set(cacheKey, persisted);
|
|
57
|
+
return {
|
|
58
|
+
signature: persisted.signature,
|
|
59
|
+
layout: persisted.layout
|
|
60
|
+
};
|
|
61
|
+
}
|
|
35
62
|
const graph = await getGraphSummary(vaultPath, agentId);
|
|
36
63
|
const signature = createGraphSignature(graph);
|
|
37
|
-
const
|
|
38
|
-
|
|
64
|
+
const rawLayout = createCauliflowerGraphLayout(graph);
|
|
65
|
+
const layout = {
|
|
66
|
+
...rawLayout,
|
|
67
|
+
nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
|
|
68
|
+
};
|
|
69
|
+
const nextCache = { layoutVersion: graphLayoutVersion, databaseSignature, signature, layout };
|
|
70
|
+
graphLayoutCache.set(cacheKey, nextCache);
|
|
71
|
+
await writePersistedLayout(vaultPath, agentId, nextCache);
|
|
39
72
|
return {
|
|
40
73
|
signature,
|
|
41
74
|
layout
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const getGraphNode = async (vaultPath, id, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.getGraphNode(id, agentId);
|
|
7
|
+
return await index.getGraphNode(id, agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const getGraphSummary = async (vaultPath, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.getGraphSummary(agentId);
|
|
7
|
+
return await index.getGraphSummary(agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -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
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const getGraph = async (vaultPath, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.getGraph(agentId);
|
|
7
|
+
return await index.getGraph(agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|