@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.
Files changed (46) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +186 -38
  4. package/dist/application/add-note.js +13 -44
  5. package/dist/application/analyze-vault.js +1 -1
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +119 -20
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/frontend/client-css.js +212 -42
  10. package/dist/application/frontend/client-html.js +42 -28
  11. package/dist/application/frontend/client-js.js +1294 -3222
  12. package/dist/application/frontend/client-render-worker-js.js +676 -0
  13. package/dist/application/get-graph-contexts.js +33 -0
  14. package/dist/application/get-graph-layout.js +62 -8
  15. package/dist/application/get-graph-stream-chunk.js +326 -0
  16. package/dist/application/get-graph-view.js +246 -0
  17. package/dist/application/graph-view-state.js +66 -0
  18. package/dist/application/import-legacy-sqlite.js +3 -33
  19. package/dist/application/index-vault.js +35 -22
  20. package/dist/application/migrate-context-links.js +79 -0
  21. package/dist/application/search-graph-node-ids.js +63 -3
  22. package/dist/application/server/routes.js +197 -12
  23. package/dist/cli/commands/read-commands.js +39 -3
  24. package/dist/cli/commands/vault-commands.js +182 -0
  25. package/dist/cli/commands/write-commands.js +147 -12
  26. package/dist/cli/main.js +2 -0
  27. package/dist/cli/runtime.js +10 -2
  28. package/dist/domain/context.js +1 -0
  29. package/dist/domain/graph-contexts.js +180 -0
  30. package/dist/domain/graph-layout.js +347 -21
  31. package/dist/domain/markdown.js +53 -9
  32. package/dist/infrastructure/config.js +105 -6
  33. package/dist/infrastructure/context-packs.js +122 -0
  34. package/dist/infrastructure/file-index.js +6 -3
  35. package/dist/infrastructure/index-state.js +2 -0
  36. package/dist/infrastructure/vault-migration-state.js +69 -0
  37. package/dist/infrastructure/volatile-memory.js +100 -0
  38. package/dist/mcp/http-server.js +97 -0
  39. package/dist/mcp/runtime.js +20 -0
  40. package/dist/mcp/server.js +36 -13
  41. package/dist/mcp/tools.js +203 -14
  42. package/docs/AGENT_USAGE.md +50 -5
  43. package/docs/ARCHITECTURE.md +11 -0
  44. package/docs/QUICKSTART.md +3 -1
  45. package/docs/RELEASE.md +4 -3
  46. 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(seeds.map((seed) => [seed.id, segmentName(seed)]));
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 petalSpreadForSegmentSize = (size) => {
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 180 + Math.log2(safeSize + 1) * 6;
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 createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
222
+ const createCauliflowerSegmentNodes = (segments, degrees, rootHubId, segmentGroups) => ([segment, nodes], segmentIndex) => {
175
223
  const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
176
- const angle = segmentAngle(segment, segmentIndex, segmentCount);
177
- const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
178
- const centerX = Math.cos(angle) * baseRadius;
179
- const centerY = Math.sin(angle) * (baseRadius * 0.78);
180
- const petalSpread = petalSpreadForSegmentSize(sortedNodes.length);
181
- return sortedNodes.map((node, index) => {
182
- const localAngle = index * 2.399963 + jitter(node.title, 0.42);
183
- const localRadius = Math.sqrt(index + 1) * petalSpread;
184
- const hubPull = segmentCount === 1 ? 0 : Math.min(degrees.get(node.id) ?? 0, 12) * 12;
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) * localRadius - Math.cos(angle) * hubPull + jitter(node.id, 24),
190
- y: centerY + Math.sin(localAngle) * localRadius * 0.78 - Math.sin(angle) * hubPull + jitter(node.path, 24)
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 ux = dx / distance;
204
- const uy = dy / distance;
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 nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
292
- const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
293
- const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
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: graph.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);
@@ -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 linkWeights = new Map(extractWikiLinkWeights(document.content).map((link) => [link.title.toLowerCase(), link]));
195
- const links = document.links
196
- .map((toTitle) => ({
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(toTitle.toLowerCase()) ?? null,
200
- weight: linkWeights.get(toTitle.toLowerCase())?.weight ?? 1,
201
- priority: linkWeights.get(toTitle.toLowerCase())?.priority ?? 'normal'
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,