@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.160
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 +241 -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 +622 -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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inferExplicitVisualGraphContext } from './graph-contexts.js';
|
|
2
|
+
const hierarchyGroupNodeLimit = 1000;
|
|
1
3
|
const groupLabels = {
|
|
2
4
|
'00-maps': 'maps',
|
|
3
5
|
'10-agent-memory': 'agent-memory',
|
|
@@ -20,6 +22,7 @@ const segmentAngles = {
|
|
|
20
22
|
Evaluation: 2.08,
|
|
21
23
|
Security: 2.82
|
|
22
24
|
};
|
|
25
|
+
const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
23
26
|
const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
|
|
24
27
|
const jitter = (value, range) => {
|
|
25
28
|
const normalized = Math.abs(hashText(value) % 1000) / 1000;
|
|
@@ -62,6 +65,48 @@ const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
|
62
65
|
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
63
66
|
return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
|
|
64
67
|
};
|
|
68
|
+
const hubScore = (node) => {
|
|
69
|
+
const title = node.title.trim().toLowerCase();
|
|
70
|
+
if (title === 'memory hub')
|
|
71
|
+
return 5;
|
|
72
|
+
if (title === 'knowledge root')
|
|
73
|
+
return 4;
|
|
74
|
+
if (/\bmoc\b/i.test(node.title))
|
|
75
|
+
return 3;
|
|
76
|
+
return hubTitlePattern.test(node.title) ? 2 : 0;
|
|
77
|
+
};
|
|
78
|
+
const selectPrimaryHubId = (nodes, degrees) => {
|
|
79
|
+
const ranked = [...nodes]
|
|
80
|
+
.filter((node) => hubScore(node) > 0)
|
|
81
|
+
.sort((left, right) => {
|
|
82
|
+
const scoreDelta = hubScore(right) - hubScore(left);
|
|
83
|
+
if (scoreDelta !== 0)
|
|
84
|
+
return scoreDelta;
|
|
85
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
86
|
+
if (degreeDelta !== 0)
|
|
87
|
+
return degreeDelta;
|
|
88
|
+
return left.title.localeCompare(right.title);
|
|
89
|
+
});
|
|
90
|
+
return ranked[0]?.id ?? null;
|
|
91
|
+
};
|
|
92
|
+
const selectHighestDegreeNodeId = (nodes, degrees) => [...nodes].sort((left, right) => {
|
|
93
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
94
|
+
return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
|
|
95
|
+
})[0]?.id ?? null;
|
|
96
|
+
const centerLayoutByNode = (nodes, nodeId) => {
|
|
97
|
+
if (!nodeId) {
|
|
98
|
+
return nodes;
|
|
99
|
+
}
|
|
100
|
+
const anchor = nodes.find((node) => node.id === nodeId);
|
|
101
|
+
if (!anchor) {
|
|
102
|
+
return nodes;
|
|
103
|
+
}
|
|
104
|
+
return nodes.map((node) => ({
|
|
105
|
+
...node,
|
|
106
|
+
x: node.x - anchor.x,
|
|
107
|
+
y: node.y - anchor.y
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
65
110
|
const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
|
|
66
111
|
const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
|
|
67
112
|
const collectComponent = (adjacency, startId, visited) => {
|
|
@@ -96,8 +141,16 @@ const selectSegmentSeeds = (nodes, edges, degrees) => {
|
|
|
96
141
|
const assignSegments = (nodes, edges, degrees) => {
|
|
97
142
|
const adjacency = createAdjacency(nodes, edges);
|
|
98
143
|
const seeds = selectSegmentSeeds(nodes, edges, degrees);
|
|
99
|
-
const assignments = new Map(
|
|
144
|
+
const assignments = new Map(nodes.flatMap((node) => {
|
|
145
|
+
const visualContext = inferExplicitVisualGraphContext(node);
|
|
146
|
+
return visualContext ? [[node.id, visualContext.title]] : [];
|
|
147
|
+
}));
|
|
100
148
|
const queue = seeds.map((seed) => seed.id);
|
|
149
|
+
seeds.forEach((seed) => {
|
|
150
|
+
if (!assignments.has(seed.id)) {
|
|
151
|
+
assignments.set(seed.id, segmentName(seed));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
101
154
|
for (let index = 0; index < queue.length; index += 1) {
|
|
102
155
|
const id = queue[index];
|
|
103
156
|
const segment = assignments.get(id);
|
|
@@ -128,27 +181,245 @@ const groupNodesBySegment = (nodes, segments) => {
|
|
|
128
181
|
return new Map(groups);
|
|
129
182
|
};
|
|
130
183
|
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
131
|
-
const
|
|
184
|
+
const compareByStarOrder = (levels, degrees, segmentIndexByName, segments) => (left, right) => {
|
|
185
|
+
const levelDelta = (levels.get(left.id) ?? 1) - (levels.get(right.id) ?? 1);
|
|
186
|
+
if (levelDelta !== 0)
|
|
187
|
+
return levelDelta;
|
|
188
|
+
const leftSegment = segments.get(left.id) ?? groupLabel(groupKey(left));
|
|
189
|
+
const rightSegment = segments.get(right.id) ?? groupLabel(groupKey(right));
|
|
190
|
+
const segmentDelta = (segmentIndexByName.get(leftSegment) ?? 0) - (segmentIndexByName.get(rightSegment) ?? 0);
|
|
191
|
+
if (segmentDelta !== 0)
|
|
192
|
+
return segmentDelta;
|
|
193
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
194
|
+
return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
|
|
195
|
+
};
|
|
196
|
+
const petalRadiusForSegmentSize = (size) => {
|
|
197
|
+
const safeSize = Math.max(size, 1);
|
|
198
|
+
return Math.max(260, Math.sqrt(safeSize) * 96);
|
|
199
|
+
};
|
|
200
|
+
const selectSegmentHub = (nodes, degrees, primaryHubId) => {
|
|
201
|
+
const primary = nodes.find((node) => node.id === primaryHubId);
|
|
202
|
+
if (primary) {
|
|
203
|
+
return primary;
|
|
204
|
+
}
|
|
205
|
+
return [...nodes].sort((left, right) => {
|
|
206
|
+
const hubDelta = hubScore(right) - hubScore(left);
|
|
207
|
+
if (hubDelta !== 0)
|
|
208
|
+
return hubDelta;
|
|
209
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
210
|
+
if (degreeDelta !== 0)
|
|
211
|
+
return degreeDelta;
|
|
212
|
+
return left.title.localeCompare(right.title);
|
|
213
|
+
})[0] ?? null;
|
|
214
|
+
};
|
|
215
|
+
const segmentCenterRadius = (segments) => {
|
|
216
|
+
if (segments.length <= 1) {
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
const circumference = segments.reduce((total, [, nodes]) => total + petalRadiusForSegmentSize(nodes.length) * 2 + 180, 0);
|
|
220
|
+
return Math.max(520, circumference / (Math.PI * 2));
|
|
221
|
+
};
|
|
222
|
+
const createCauliflowerSegmentNodes = (segments, degrees, rootHubId, segmentGroups) => ([segment, nodes], segmentIndex) => {
|
|
132
223
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
224
|
+
const segmentHub = selectSegmentHub(sortedNodes, degrees, rootHubId);
|
|
225
|
+
const angle = segmentAngle(segment, segmentIndex, segmentGroups.length);
|
|
226
|
+
const globalRadius = segmentCenterRadius(segmentGroups);
|
|
227
|
+
const petalRadius = petalRadiusForSegmentSize(sortedNodes.length);
|
|
228
|
+
const isPrimarySegment = Boolean(segmentHub && segmentHub.id === rootHubId);
|
|
229
|
+
const centerX = isPrimarySegment || globalRadius === 0 ? 0 : Math.cos(angle) * globalRadius;
|
|
230
|
+
const centerY = isPrimarySegment || globalRadius === 0 ? 0 : Math.sin(angle) * (globalRadius * 0.86);
|
|
231
|
+
const nonHubNodes = sortedNodes.filter((node) => node.id !== segmentHub?.id);
|
|
232
|
+
const hubNode = segmentHub
|
|
233
|
+
? [{
|
|
234
|
+
...segmentHub,
|
|
235
|
+
group: groupLabel(groupKey(segmentHub)),
|
|
236
|
+
segment: segments.get(segmentHub.id) ?? segment,
|
|
237
|
+
x: centerX,
|
|
238
|
+
y: centerY
|
|
239
|
+
}]
|
|
240
|
+
: [];
|
|
241
|
+
const petalNodes = nonHubNodes.map((node, index) => {
|
|
242
|
+
const localAngle = index * 2.399963 + jitter(node.title, 0.5);
|
|
243
|
+
const radialLayer = Math.sqrt(index + 1) / Math.sqrt(Math.max(nonHubNodes.length, 1));
|
|
244
|
+
const localRadius = 150 + radialLayer * petalRadius + jitter(node.id, 34);
|
|
245
|
+
const degreePull = Math.min(degrees.get(node.id) ?? 0, 16) * 8;
|
|
246
|
+
const radius = Math.max(126, localRadius - degreePull);
|
|
142
247
|
return {
|
|
143
248
|
...node,
|
|
144
249
|
group: groupLabel(groupKey(node)),
|
|
145
250
|
segment: segments.get(node.id) ?? segment,
|
|
146
|
-
x: centerX + Math.cos(localAngle) *
|
|
147
|
-
y: centerY + Math.sin(localAngle) *
|
|
251
|
+
x: centerX + Math.cos(localAngle) * radius + jitter(node.title, 20),
|
|
252
|
+
y: centerY + Math.sin(localAngle) * radius * 0.84 + jitter(node.path, 20)
|
|
148
253
|
};
|
|
149
254
|
});
|
|
255
|
+
return [...hubNode, ...petalNodes];
|
|
256
|
+
};
|
|
257
|
+
const createVisualEdge = (source, target, weight, priority) => ({
|
|
258
|
+
source: source.id,
|
|
259
|
+
target: target.id,
|
|
260
|
+
targetTitle: target.title,
|
|
261
|
+
weight,
|
|
262
|
+
priority
|
|
263
|
+
});
|
|
264
|
+
const createCauliflowerVisualEdges = (segmentGroups, degrees, rootHubId) => {
|
|
265
|
+
const nodeById = new Map(segmentGroups.flatMap(([, nodes]) => nodes.map((node) => [node.id, node])));
|
|
266
|
+
const rootHub = rootHubId ? nodeById.get(rootHubId) ?? null : null;
|
|
267
|
+
const edges = new Map();
|
|
268
|
+
const addEdge = (edge) => {
|
|
269
|
+
if (!edge.target || edge.source === edge.target) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
edges.set(`${edge.source}|${edge.target}`, edge);
|
|
273
|
+
};
|
|
274
|
+
segmentGroups.forEach(([, nodes]) => {
|
|
275
|
+
const segmentHub = selectSegmentHub(nodes, degrees, rootHubId);
|
|
276
|
+
const parent = segmentHub ?? rootHub;
|
|
277
|
+
if (!parent) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (rootHub && parent.id !== rootHub.id) {
|
|
281
|
+
addEdge(createVisualEdge(rootHub, parent, 6, 'high'));
|
|
282
|
+
}
|
|
283
|
+
nodes.forEach((node) => {
|
|
284
|
+
if (node.id === parent.id || node.id === rootHub?.id) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
addEdge(createVisualEdge(parent, node, 1, 'low'));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
return Array.from(edges.values());
|
|
150
291
|
};
|
|
151
292
|
const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
|
|
293
|
+
const layoutBounds = (nodes) => {
|
|
294
|
+
if (nodes.length === 0) {
|
|
295
|
+
return { x: 0, y: 0, radius: 1 };
|
|
296
|
+
}
|
|
297
|
+
const bounds = nodes.reduce((current, node) => ({
|
|
298
|
+
minX: Math.min(current.minX, node.x),
|
|
299
|
+
maxX: Math.max(current.maxX, node.x),
|
|
300
|
+
minY: Math.min(current.minY, node.y),
|
|
301
|
+
maxY: Math.max(current.maxY, node.y)
|
|
302
|
+
}), {
|
|
303
|
+
minX: Number.POSITIVE_INFINITY,
|
|
304
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
305
|
+
minY: Number.POSITIVE_INFINITY,
|
|
306
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
307
|
+
});
|
|
308
|
+
const x = (bounds.minX + bounds.maxX) / 2;
|
|
309
|
+
const y = (bounds.minY + bounds.maxY) / 2;
|
|
310
|
+
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
|
|
311
|
+
return { x, y, radius: Math.max(radius + 72, 120) };
|
|
312
|
+
};
|
|
313
|
+
const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
|
|
314
|
+
const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
|
|
315
|
+
const groupTitle = (segment, level, index, nodes) => nodes.length === 1
|
|
316
|
+
? nodes[0]?.title ?? segment
|
|
317
|
+
: `${segment} ${level + 1}.${index + 1}`;
|
|
318
|
+
const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
319
|
+
const sortedNodes = [...nodes].sort((left, right) => {
|
|
320
|
+
const segmentDelta = left.segment.localeCompare(right.segment);
|
|
321
|
+
if (segmentDelta !== 0)
|
|
322
|
+
return segmentDelta;
|
|
323
|
+
const groupDelta = left.group.localeCompare(right.group);
|
|
324
|
+
if (groupDelta !== 0)
|
|
325
|
+
return groupDelta;
|
|
326
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
327
|
+
if (degreeDelta !== 0)
|
|
328
|
+
return degreeDelta;
|
|
329
|
+
return left.title.localeCompare(right.title);
|
|
330
|
+
});
|
|
331
|
+
const groupCountTarget = Math.min(groupNodeLimit, sortedNodes.length);
|
|
332
|
+
const chunkSize = sortedNodes.length <= groupNodeLimit * groupNodeLimit
|
|
333
|
+
? Math.max(1, Math.ceil(sortedNodes.length / groupCountTarget))
|
|
334
|
+
: groupNodeLimit;
|
|
335
|
+
const chunks = [];
|
|
336
|
+
for (let index = 0; index < sortedNodes.length; index += chunkSize) {
|
|
337
|
+
chunks.push(sortedNodes.slice(index, index + chunkSize));
|
|
338
|
+
}
|
|
339
|
+
return chunks;
|
|
340
|
+
};
|
|
341
|
+
const groupEdges = (edges, nodeIds) => ({
|
|
342
|
+
internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
|
|
343
|
+
externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
|
|
344
|
+
});
|
|
345
|
+
const groupBounds = (groups) => {
|
|
346
|
+
if (groups.length === 0) {
|
|
347
|
+
return { x: 0, y: 0, radius: 1 };
|
|
348
|
+
}
|
|
349
|
+
const nodes = groups.map((group) => ({
|
|
350
|
+
x: group.x,
|
|
351
|
+
y: group.y,
|
|
352
|
+
radius: group.radius
|
|
353
|
+
}));
|
|
354
|
+
const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
|
|
355
|
+
const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
|
|
356
|
+
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
|
|
357
|
+
return { x, y, radius: Math.max(radius + 120, 180) };
|
|
358
|
+
};
|
|
359
|
+
const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
|
|
360
|
+
const createParentGroups = (groups, edges, level, groupNodeLimit) => {
|
|
361
|
+
if (groups.length <= groupNodeLimit) {
|
|
362
|
+
return groups;
|
|
363
|
+
}
|
|
364
|
+
const parentGroups = [];
|
|
365
|
+
for (let index = 0; index < groups.length; index += groupNodeLimit) {
|
|
366
|
+
const chunk = groups.slice(index, index + groupNodeLimit);
|
|
367
|
+
const nodeIds = new Set(descendantNodeIds(chunk));
|
|
368
|
+
const bounds = groupBounds(chunk);
|
|
369
|
+
const segment = chunk[0]?.segment ?? 'root';
|
|
370
|
+
const group = chunk[0]?.group ?? 'root';
|
|
371
|
+
const groupIndex = index / groupNodeLimit;
|
|
372
|
+
const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
373
|
+
const edgeGroups = groupEdges(edges, nodeIds);
|
|
374
|
+
parentGroups.push({
|
|
375
|
+
id,
|
|
376
|
+
level,
|
|
377
|
+
parentId: null,
|
|
378
|
+
title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
|
|
379
|
+
segment,
|
|
380
|
+
group,
|
|
381
|
+
x: bounds.x,
|
|
382
|
+
y: bounds.y,
|
|
383
|
+
radius: bounds.radius,
|
|
384
|
+
nodeIds: [],
|
|
385
|
+
childGroupIds: chunk.map((child) => child.id),
|
|
386
|
+
internalEdges: edgeGroups.internalEdges,
|
|
387
|
+
externalEdges: edgeGroups.externalEdges
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const relinkedChildren = groups.map((group) => {
|
|
391
|
+
const parent = parentGroups.find((candidate) => candidate.childGroupIds.includes(group.id));
|
|
392
|
+
return parent ? { ...group, parentId: parent.id } : group;
|
|
393
|
+
});
|
|
394
|
+
return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
|
|
395
|
+
};
|
|
396
|
+
export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
397
|
+
if (nodes.length <= groupNodeLimit) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
|
|
401
|
+
const nodeIds = new Set(chunk.map((node) => node.id));
|
|
402
|
+
const bounds = layoutBounds(chunk);
|
|
403
|
+
const segment = chunk[0]?.segment ?? 'root';
|
|
404
|
+
const group = chunk[0]?.group ?? 'root';
|
|
405
|
+
const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
406
|
+
return {
|
|
407
|
+
id,
|
|
408
|
+
level: 0,
|
|
409
|
+
parentId: null,
|
|
410
|
+
title: groupTitle(segment, 0, index, chunk),
|
|
411
|
+
segment,
|
|
412
|
+
group,
|
|
413
|
+
x: bounds.x,
|
|
414
|
+
y: bounds.y,
|
|
415
|
+
radius: bounds.radius,
|
|
416
|
+
nodeIds: chunk.map((node) => node.id),
|
|
417
|
+
childGroupIds: [],
|
|
418
|
+
...groupEdges(edges, nodeIds)
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
|
|
422
|
+
};
|
|
152
423
|
const resolveCollisionPair = (left, right, minDistance) => {
|
|
153
424
|
const dx = right.x - left.x;
|
|
154
425
|
const dy = right.y - left.y;
|
|
@@ -157,8 +428,9 @@ const resolveCollisionPair = (left, right, minDistance) => {
|
|
|
157
428
|
return;
|
|
158
429
|
}
|
|
159
430
|
const push = (minDistance - distance) / 2;
|
|
160
|
-
const
|
|
161
|
-
const
|
|
431
|
+
const fallbackAngle = Math.PI * 2 * (Math.abs(hashText(`${left.id}:${right.id}`) % 1000) / 1000);
|
|
432
|
+
const ux = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.cos(fallbackAngle) : dx / distance;
|
|
433
|
+
const uy = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.sin(fallbackAngle) : dy / distance;
|
|
162
434
|
left.x -= ux * push;
|
|
163
435
|
left.y -= uy * push;
|
|
164
436
|
right.x += ux * push;
|
|
@@ -240,15 +512,114 @@ const relaxCollisions = (nodes, minDistance = 132, rounds = 32) => {
|
|
|
240
512
|
}
|
|
241
513
|
return layoutNodes;
|
|
242
514
|
};
|
|
515
|
+
const assignStarLevels = (nodes, edges, hubId) => {
|
|
516
|
+
if (!hubId) {
|
|
517
|
+
return new Map(nodes.map((node) => [node.id, 1]));
|
|
518
|
+
}
|
|
519
|
+
const adjacency = createAdjacency(nodes, edges);
|
|
520
|
+
const levels = new Map([[hubId, 0]]);
|
|
521
|
+
const queue = [hubId];
|
|
522
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
523
|
+
const id = queue[index];
|
|
524
|
+
const nextLevel = (levels.get(id) ?? 0) + 1;
|
|
525
|
+
(adjacency.get(id) ?? []).forEach((nextId) => {
|
|
526
|
+
if (!levels.has(nextId)) {
|
|
527
|
+
levels.set(nextId, nextLevel);
|
|
528
|
+
queue.push(nextId);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
nodes.forEach((node) => {
|
|
533
|
+
if (!levels.has(node.id)) {
|
|
534
|
+
levels.set(node.id, 2);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return levels;
|
|
538
|
+
};
|
|
539
|
+
const createStarNodes = (nodes, segments, degrees, hubId, levels) => {
|
|
540
|
+
const segmentNames = Array.from(new Set(nodes.map((node) => segments.get(node.id) ?? groupLabel(groupKey(node)))))
|
|
541
|
+
.sort((left, right) => segmentAngle(left, 0, 1) - segmentAngle(right, 0, 1) || left.localeCompare(right));
|
|
542
|
+
const segmentIndexByName = new Map(segmentNames.map((segment, index) => [segment, index]));
|
|
543
|
+
const sortedNodes = [...nodes].sort(compareByStarOrder(levels, degrees, segmentIndexByName, segments));
|
|
544
|
+
const nodesByLevel = sortedNodes.reduce((state, node) => {
|
|
545
|
+
const level = node.id === hubId ? 0 : Math.max(1, levels.get(node.id) ?? 1);
|
|
546
|
+
const levelNodes = state.get(level) ?? [];
|
|
547
|
+
levelNodes.push(node);
|
|
548
|
+
state.set(level, levelNodes);
|
|
549
|
+
return state;
|
|
550
|
+
}, new Map());
|
|
551
|
+
return Array.from(nodesByLevel.entries())
|
|
552
|
+
.sort(([left], [right]) => left - right)
|
|
553
|
+
.flatMap(([level, levelNodes]) => {
|
|
554
|
+
if (level === 0) {
|
|
555
|
+
return levelNodes.map((node) => ({
|
|
556
|
+
...node,
|
|
557
|
+
group: groupLabel(groupKey(node)),
|
|
558
|
+
segment: segments.get(node.id) ?? groupLabel(groupKey(node)),
|
|
559
|
+
x: 0,
|
|
560
|
+
y: 0
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
const levelNodesBySegment = segmentNames
|
|
564
|
+
.map((segment) => ({
|
|
565
|
+
segment,
|
|
566
|
+
nodes: levelNodes.filter((node) => (segments.get(node.id) ?? groupLabel(groupKey(node))) === segment)
|
|
567
|
+
}))
|
|
568
|
+
.filter((group) => group.nodes.length > 0);
|
|
569
|
+
const totalNodes = levelNodesBySegment.reduce((total, group) => total + group.nodes.length, 0);
|
|
570
|
+
const baseRadius = Math.max(360 + (level - 1) * 460, (levelNodes.length * 156) / (Math.PI * 2));
|
|
571
|
+
let arcCursor = -Math.PI / 2;
|
|
572
|
+
return levelNodesBySegment.flatMap((group) => {
|
|
573
|
+
const arcSize = (Math.PI * 2 * group.nodes.length) / Math.max(totalNodes, 1);
|
|
574
|
+
const arcPadding = Math.min(0.22, arcSize * 0.18);
|
|
575
|
+
const arcStart = arcCursor + arcPadding;
|
|
576
|
+
const arcEnd = arcCursor + arcSize - arcPadding;
|
|
577
|
+
const usableArc = Math.max(0.001, arcEnd - arcStart);
|
|
578
|
+
const segmentRadius = Math.max(baseRadius, (group.nodes.length * 156) / usableArc);
|
|
579
|
+
arcCursor += arcSize;
|
|
580
|
+
return group.nodes.map((node, index) => {
|
|
581
|
+
const lane = index % 3 - 1;
|
|
582
|
+
const angle = arcStart + usableArc * ((index + 0.5) / Math.max(group.nodes.length, 1)) + jitter(node.title, 0.035);
|
|
583
|
+
const radialJitter = jitter(node.id, 34);
|
|
584
|
+
return {
|
|
585
|
+
...node,
|
|
586
|
+
group: groupLabel(groupKey(node)),
|
|
587
|
+
segment: group.segment,
|
|
588
|
+
x: Math.cos(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.title, 16),
|
|
589
|
+
y: Math.sin(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.path, 16)
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
export const createStarGraphLayout = (graph) => {
|
|
596
|
+
const degrees = countDegrees(graph.edges);
|
|
597
|
+
const hubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
598
|
+
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
599
|
+
const levels = assignStarLevels(graph.nodes, graph.edges, hubId);
|
|
600
|
+
const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels), 156, 22);
|
|
601
|
+
const centeredNodes = centerLayoutByNode(nodes, hubId);
|
|
602
|
+
const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
|
|
603
|
+
return {
|
|
604
|
+
nodes: centeredNodes,
|
|
605
|
+
edges: graph.edges,
|
|
606
|
+
...(groups.length > 0 ? { groups } : {})
|
|
607
|
+
};
|
|
608
|
+
};
|
|
243
609
|
export const createCauliflowerGraphLayout = (graph) => {
|
|
244
610
|
const degrees = countDegrees(graph.edges);
|
|
245
611
|
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
246
612
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
247
613
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
248
|
-
const
|
|
614
|
+
const rootHubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
615
|
+
const nodes = relaxCollisions(segmentGroups.flatMap(createCauliflowerSegmentNodes(segments, degrees, rootHubId, segmentGroups)), 156, 28);
|
|
616
|
+
const centeredNodes = centerLayoutByNode(nodes, rootHubId);
|
|
617
|
+
const visualEdges = createCauliflowerVisualEdges(segmentGroups, degrees, rootHubId);
|
|
618
|
+
const groups = createGraphLayoutHierarchy(centeredNodes, visualEdges, degrees);
|
|
249
619
|
return {
|
|
250
|
-
nodes,
|
|
251
|
-
edges:
|
|
620
|
+
nodes: centeredNodes,
|
|
621
|
+
edges: visualEdges,
|
|
622
|
+
...(groups.length > 0 ? { groups } : {})
|
|
252
623
|
};
|
|
253
624
|
};
|
|
254
625
|
export const getMinimumLayoutDistance = (nodes) => nodes.reduce((minimumDistance, leftNode, leftIndex) => nodes.slice(leftIndex + 1).reduce((innerMinimum, rightNode) => Math.min(innerMinimum, distanceBetween(leftNode, rightNode)), minimumDistance), Number.POSITIVE_INFINITY);
|
package/dist/domain/markdown.js
CHANGED
|
@@ -6,6 +6,7 @@ const frontmatterPattern = /^---\n([\s\S]*?)\n---\n?/;
|
|
|
6
6
|
const wikiLinkPattern = /\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g;
|
|
7
7
|
const tagPattern = /(^|\s)#([A-Za-z0-9][A-Za-z0-9_-]*)/g;
|
|
8
8
|
const headingPattern = /^#\s+(.+)$/m;
|
|
9
|
+
const contextLinksHeadingPattern = /^(#{1,6})\s+(?:context\s+links?|links?\s+de\s+contexto)\b/i;
|
|
9
10
|
const priorityRanks = {
|
|
10
11
|
low: 0,
|
|
11
12
|
normal: 1,
|
|
@@ -18,6 +19,7 @@ const priorityBoosts = {
|
|
|
18
19
|
high: 3,
|
|
19
20
|
critical: 6
|
|
20
21
|
};
|
|
22
|
+
export const graphLinkModelVersion = 4;
|
|
21
23
|
const priorityPatterns = [
|
|
22
24
|
['critical', /\b(?:priority|prioridade|importance|importancia|importância)\s*[:=]\s*(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
|
|
23
25
|
['critical', /#(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
|
|
@@ -100,6 +102,47 @@ export const extractWikiLinkWeights = (content) => {
|
|
|
100
102
|
}, new Map());
|
|
101
103
|
return Array.from(weights.values());
|
|
102
104
|
};
|
|
105
|
+
const compareGraphLinks = (left, right) => {
|
|
106
|
+
const priorityDelta = priorityRanks[right.priority] - priorityRanks[left.priority];
|
|
107
|
+
if (priorityDelta !== 0)
|
|
108
|
+
return priorityDelta;
|
|
109
|
+
const weightDelta = right.weight - left.weight;
|
|
110
|
+
if (weightDelta !== 0)
|
|
111
|
+
return weightDelta;
|
|
112
|
+
return left.title.localeCompare(right.title);
|
|
113
|
+
};
|
|
114
|
+
export const selectGraphWikiLinkWeights = (links) => {
|
|
115
|
+
return [...links].sort(compareGraphLinks);
|
|
116
|
+
};
|
|
117
|
+
const contextLinkLines = (content) => {
|
|
118
|
+
const lines = visibleMarkdownLines(content);
|
|
119
|
+
const selected = [];
|
|
120
|
+
let insideContextLinks = false;
|
|
121
|
+
let headingDepth = 0;
|
|
122
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
123
|
+
const line = lines[index];
|
|
124
|
+
const trimmed = line.content.trim();
|
|
125
|
+
if (line.fenced) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const heading = trimmed.match(/^(#{1,6})\s+/);
|
|
129
|
+
const contextHeading = trimmed.match(contextLinksHeadingPattern);
|
|
130
|
+
if (contextHeading) {
|
|
131
|
+
insideContextLinks = true;
|
|
132
|
+
headingDepth = contextHeading[1].length;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (insideContextLinks && heading && heading[1].length <= headingDepth) {
|
|
136
|
+
insideContextLinks = false;
|
|
137
|
+
}
|
|
138
|
+
if (insideContextLinks) {
|
|
139
|
+
selected.push(line.content);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return selected;
|
|
143
|
+
};
|
|
144
|
+
export const hasContextLinksSection = (content) => visibleMarkdownLines(content).some((line) => !line.fenced && contextLinksHeadingPattern.test(line.content.trim()));
|
|
145
|
+
export const extractContextLinkWeights = (content) => selectGraphWikiLinkWeights(extractWikiLinkWeights(contextLinkLines(content).join('\n')));
|
|
103
146
|
const extractTitle = (filePath, content, frontmatter) => {
|
|
104
147
|
if (frontmatter.title) {
|
|
105
148
|
return normalizeTitle(frontmatter.title);
|
|
@@ -184,6 +227,7 @@ export const parseMarkdownDocument = (input) => {
|
|
|
184
227
|
content: input.content,
|
|
185
228
|
tags: extractTags(input.content),
|
|
186
229
|
links: extractWikiLinks(input.content),
|
|
230
|
+
contextLinks: extractContextLinkWeights(input.content).map((link) => link.title),
|
|
187
231
|
frontmatter,
|
|
188
232
|
createdAt: input.createdAt.toISOString(),
|
|
189
233
|
updatedAt: input.updatedAt.toISOString()
|
|
@@ -191,16 +235,16 @@ export const parseMarkdownDocument = (input) => {
|
|
|
191
235
|
};
|
|
192
236
|
export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
|
|
193
237
|
const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
238
|
+
const graphLinkWeights = selectGraphWikiLinkWeights(extractContextLinkWeights(document.content).filter((link) => link.title.toLowerCase() !== document.title.toLowerCase()));
|
|
239
|
+
const linkWeights = new Map(graphLinkWeights.map((link) => [link.title.toLowerCase(), link]));
|
|
240
|
+
const links = graphLinkWeights
|
|
241
|
+
.map((link) => ({
|
|
197
242
|
fromDocumentId: document.id,
|
|
198
|
-
toTitle,
|
|
199
|
-
toDocumentId: titleToDocumentId.get(
|
|
200
|
-
weight: linkWeights.get(
|
|
201
|
-
priority: linkWeights.get(
|
|
202
|
-
}))
|
|
203
|
-
.filter((link) => link.toDocumentId !== document.id);
|
|
243
|
+
toTitle: link.title,
|
|
244
|
+
toDocumentId: titleToDocumentId.get(link.title.toLowerCase()) ?? null,
|
|
245
|
+
weight: linkWeights.get(link.title.toLowerCase())?.weight ?? 1,
|
|
246
|
+
priority: linkWeights.get(link.title.toLowerCase())?.priority ?? 'normal'
|
|
247
|
+
}));
|
|
204
248
|
return {
|
|
205
249
|
document,
|
|
206
250
|
chunks,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const middleOutIndices = (size, pivotIndex) => {
|
|
2
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const clampedPivot = Math.max(0, Math.min(Math.floor(pivotIndex), size - 1));
|
|
6
|
+
const indices = [clampedPivot];
|
|
7
|
+
for (let offset = 1; indices.length < size; offset += 1) {
|
|
8
|
+
const left = clampedPivot - offset;
|
|
9
|
+
const right = clampedPivot + offset;
|
|
10
|
+
if (left >= 0) {
|
|
11
|
+
indices.push(left);
|
|
12
|
+
}
|
|
13
|
+
if (right < size) {
|
|
14
|
+
indices.push(right);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return indices;
|
|
18
|
+
};
|