@andespindola/brainlink 0.1.0-beta.99 → 1.0.0
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 +6 -6
- package/CHANGELOG.md +14 -0
- package/README.md +186 -38
- package/dist/application/add-note.js +13 -44
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +119 -20
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/frontend/client-css.js +212 -42
- package/dist/application/frontend/client-html.js +42 -28
- package/dist/application/frontend/client-js.js +1294 -3222
- package/dist/application/frontend/client-render-worker-js.js +676 -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 +3 -33
- package/dist/application/index-vault.js +35 -22
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +197 -12
- package/dist/cli/commands/read-commands.js +39 -3
- package/dist/cli/commands/vault-commands.js +182 -0
- package/dist/cli/commands/write-commands.js +147 -12
- package/dist/cli/main.js +2 -0
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +1 -0
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +347 -21
- package/dist/domain/markdown.js +53 -9
- package/dist/infrastructure/config.js +105 -6
- package/dist/infrastructure/context-packs.js +122 -0
- package/dist/infrastructure/file-index.js +6 -3
- package/dist/infrastructure/index-state.js +2 -0
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/http-server.js +97 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +36 -13
- package/dist/mcp/tools.js +203 -14
- package/docs/AGENT_USAGE.md +50 -5
- package/docs/ARCHITECTURE.md +11 -0
- package/docs/QUICKSTART.md +3 -1
- package/docs/RELEASE.md +4 -3
- package/package.json +3 -1
|
@@ -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',
|
|
@@ -87,6 +89,10 @@ const selectPrimaryHubId = (nodes, degrees) => {
|
|
|
87
89
|
});
|
|
88
90
|
return ranked[0]?.id ?? null;
|
|
89
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;
|
|
90
96
|
const centerLayoutByNode = (nodes, nodeId) => {
|
|
91
97
|
if (!nodeId) {
|
|
92
98
|
return nodes;
|
|
@@ -135,8 +141,16 @@ const selectSegmentSeeds = (nodes, edges, degrees) => {
|
|
|
135
141
|
const assignSegments = (nodes, edges, degrees) => {
|
|
136
142
|
const adjacency = createAdjacency(nodes, edges);
|
|
137
143
|
const seeds = selectSegmentSeeds(nodes, edges, degrees);
|
|
138
|
-
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
|
+
}));
|
|
139
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
|
+
});
|
|
140
154
|
for (let index = 0; index < queue.length; index += 1) {
|
|
141
155
|
const id = queue[index];
|
|
142
156
|
const segment = assignments.get(id);
|
|
@@ -167,31 +181,245 @@ const groupNodesBySegment = (nodes, segments) => {
|
|
|
167
181
|
return new Map(groups);
|
|
168
182
|
};
|
|
169
183
|
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
170
|
-
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) => {
|
|
171
197
|
const safeSize = Math.max(size, 1);
|
|
172
|
-
return
|
|
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));
|
|
173
221
|
};
|
|
174
|
-
const
|
|
222
|
+
const createCauliflowerSegmentNodes = (segments, degrees, rootHubId, segmentGroups) => ([segment, nodes], segmentIndex) => {
|
|
175
223
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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);
|
|
185
247
|
return {
|
|
186
248
|
...node,
|
|
187
249
|
group: groupLabel(groupKey(node)),
|
|
188
250
|
segment: segments.get(node.id) ?? segment,
|
|
189
|
-
x: centerX + Math.cos(localAngle) *
|
|
190
|
-
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)
|
|
191
253
|
};
|
|
192
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());
|
|
193
291
|
};
|
|
194
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
|
+
};
|
|
195
423
|
const resolveCollisionPair = (left, right, minDistance) => {
|
|
196
424
|
const dx = right.x - left.x;
|
|
197
425
|
const dy = right.y - left.y;
|
|
@@ -200,8 +428,9 @@ const resolveCollisionPair = (left, right, minDistance) => {
|
|
|
200
428
|
return;
|
|
201
429
|
}
|
|
202
430
|
const push = (minDistance - distance) / 2;
|
|
203
|
-
const
|
|
204
|
-
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;
|
|
205
434
|
left.x -= ux * push;
|
|
206
435
|
left.y -= uy * push;
|
|
207
436
|
right.x += ux * push;
|
|
@@ -283,17 +512,114 @@ const relaxCollisions = (nodes, minDistance = 132, rounds = 32) => {
|
|
|
283
512
|
}
|
|
284
513
|
return layoutNodes;
|
|
285
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
|
+
};
|
|
286
609
|
export const createCauliflowerGraphLayout = (graph) => {
|
|
287
610
|
const degrees = countDegrees(graph.edges);
|
|
288
611
|
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
289
612
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
290
613
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const centeredNodes = centerLayoutByNode(nodes,
|
|
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);
|
|
294
619
|
return {
|
|
295
620
|
nodes: centeredNodes,
|
|
296
|
-
edges:
|
|
621
|
+
edges: visualEdges,
|
|
622
|
+
...(groups.length > 0 ? { groups } : {})
|
|
297
623
|
};
|
|
298
624
|
};
|
|
299
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,
|