@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.161

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 (50) hide show
  1. package/AGENTS.md +9 -6
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +177 -20
  5. package/dist/application/add-note.js +13 -44
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +64 -3
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +258 -51
  11. package/dist/application/frontend/client-html.js +50 -27
  12. package/dist/application/frontend/client-js.js +1369 -605
  13. package/dist/application/frontend/client-render-worker-js.js +645 -0
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-contexts.js +33 -0
  16. package/dist/application/get-graph-layout.js +62 -8
  17. package/dist/application/get-graph-stream-chunk.js +326 -0
  18. package/dist/application/get-graph-view.js +246 -0
  19. package/dist/application/graph-view-state.js +66 -0
  20. package/dist/application/import-legacy-sqlite.js +266 -0
  21. package/dist/application/index-vault.js +262 -23
  22. package/dist/application/migrate-context-links.js +79 -0
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +63 -3
  25. package/dist/application/server/routes.js +247 -7
  26. package/dist/application/start-server.js +75 -4
  27. package/dist/application/watch-vault.js +23 -2
  28. package/dist/cli/commands/agent-commands.js +7 -0
  29. package/dist/cli/commands/write-commands.js +924 -14
  30. package/dist/cli/runtime.js +10 -2
  31. package/dist/domain/context.js +54 -11
  32. package/dist/domain/graph-contexts.js +180 -0
  33. package/dist/domain/graph-layout.js +389 -18
  34. package/dist/domain/markdown.js +53 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +121 -4
  37. package/dist/infrastructure/file-index.js +76 -6
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +286 -15
  42. package/dist/infrastructure/vault-migration-state.js +69 -0
  43. package/dist/infrastructure/volatile-memory.js +100 -0
  44. package/dist/mcp/runtime.js +20 -0
  45. package/dist/mcp/server.js +39 -11
  46. package/dist/mcp/tools.js +183 -7
  47. package/docs/AGENT_USAGE.md +96 -5
  48. package/docs/ARCHITECTURE.md +8 -0
  49. package/docs/QUICKSTART.md +7 -0
  50. 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(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
+ }));
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 createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
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 angle = segmentAngle(segment, segmentIndex, segmentCount);
134
- const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
135
- const centerX = Math.cos(angle) * baseRadius;
136
- const centerY = Math.sin(angle) * (baseRadius * 0.78);
137
- const petalSpread = 40 + Math.sqrt(sortedNodes.length) * 14;
138
- return sortedNodes.map((node, index) => {
139
- const localAngle = index * 2.399963 + jitter(node.title, 0.42);
140
- const localRadius = Math.sqrt(index + 1) * petalSpread;
141
- 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);
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) * localRadius - Math.cos(angle) * hubPull + jitter(node.id, 24),
147
- 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)
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 ux = dx / distance;
161
- 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;
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 nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
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: graph.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);
@@ -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,
@@ -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
+ };