@andespindola/brainlink 0.1.0-beta.52 → 0.1.0-beta.53

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.
@@ -52,6 +52,7 @@ const state = {
52
52
  overviewClusters: [],
53
53
  macroCenter: { x: 0, y: 0 },
54
54
  macroRepresentative: null,
55
+ primaryHub: null,
55
56
  filterWorker: null,
56
57
  filterReady: false,
57
58
  lastHoverHitAt: 0,
@@ -296,6 +297,7 @@ const recomputeVisibility = () => {
296
297
  }
297
298
  : { x: 0, y: 0 }
298
299
  state.macroRepresentative = resolveMacroRepresentative(nodes)
300
+ state.primaryHub = rankedHubNodes()[0] ?? null
299
301
  markRenderDirty()
300
302
  }
301
303
 
@@ -679,7 +681,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
679
681
  return { min: 0.0008, max: 0.24 }
680
682
  }
681
683
 
682
- const fitView = (options = { useFiltered: true, macro: false }) => {
684
+ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
683
685
  const rect = canvas.getBoundingClientRect()
684
686
  const width = Math.max(rect.width, 320)
685
687
  const height = Math.max(rect.height, 320)
@@ -716,8 +718,12 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
716
718
  : nodes.length > massiveGraphNodeThreshold
717
719
  ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
718
720
  : baselineScale
719
- const centerX = (bounds.minX + bounds.maxX) / 2
720
- const centerY = (bounds.minY + bounds.maxY) / 2
721
+ const hubCenter =
722
+ options.preferHubCenter && state.primaryHub && nodes.some((node) => node.id === state.primaryHub.id)
723
+ ? state.primaryHub
724
+ : null
725
+ const centerX = hubCenter ? hubCenter.x : (bounds.minX + bounds.maxX) / 2
726
+ const centerY = hubCenter ? hubCenter.y : (bounds.minY + bounds.maxY) / 2
721
727
 
722
728
  state.transform = {
723
729
  x: clampTransformCoordinate(width / 2 - centerX * scale),
@@ -729,7 +735,7 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
729
735
  markRenderDirty()
730
736
  }
731
737
 
732
- const resetView = () => fitView({ useFiltered: false, macro: true })
738
+ const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
733
739
 
734
740
  const createLayout = graph => {
735
741
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
@@ -20,6 +20,7 @@ const segmentAngles = {
20
20
  Evaluation: 2.08,
21
21
  Security: 2.82
22
22
  };
23
+ const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
23
24
  const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
24
25
  const jitter = (value, range) => {
25
26
  const normalized = Math.abs(hashText(value) % 1000) / 1000;
@@ -62,6 +63,44 @@ const byDegreeThenTitle = (degrees) => (left, right) => {
62
63
  const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
63
64
  return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
64
65
  };
66
+ const hubScore = (node) => {
67
+ const title = node.title.trim().toLowerCase();
68
+ if (title === 'memory hub')
69
+ return 5;
70
+ if (title === 'knowledge root')
71
+ return 4;
72
+ if (/\bmoc\b/i.test(node.title))
73
+ return 3;
74
+ return hubTitlePattern.test(node.title) ? 2 : 0;
75
+ };
76
+ const selectPrimaryHubId = (nodes, degrees) => {
77
+ const ranked = [...nodes]
78
+ .filter((node) => hubScore(node) > 0)
79
+ .sort((left, right) => {
80
+ const scoreDelta = hubScore(right) - hubScore(left);
81
+ if (scoreDelta !== 0)
82
+ return scoreDelta;
83
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
84
+ if (degreeDelta !== 0)
85
+ return degreeDelta;
86
+ return left.title.localeCompare(right.title);
87
+ });
88
+ return ranked[0]?.id ?? null;
89
+ };
90
+ const centerLayoutByNode = (nodes, nodeId) => {
91
+ if (!nodeId) {
92
+ return nodes;
93
+ }
94
+ const anchor = nodes.find((node) => node.id === nodeId);
95
+ if (!anchor) {
96
+ return nodes;
97
+ }
98
+ return nodes.map((node) => ({
99
+ ...node,
100
+ x: node.x - anchor.x,
101
+ y: node.y - anchor.y
102
+ }));
103
+ };
65
104
  const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
66
105
  const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
67
106
  const collectComponent = (adjacency, startId, visited) => {
@@ -246,8 +285,10 @@ export const createCauliflowerGraphLayout = (graph) => {
246
285
  const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
247
286
  .sort(([left], [right]) => left.localeCompare(right));
248
287
  const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
288
+ const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
289
+ const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
249
290
  return {
250
- nodes,
291
+ nodes: centeredNodes,
251
292
  edges: graph.edges
252
293
  };
253
294
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.52",
3
+ "version": "0.1.0-beta.53",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",