@andespindola/brainlink 0.1.0-beta.51 → 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.
@@ -17,7 +17,7 @@ const worldCoordinateLimit = 5_000_000
17
17
  const transformCoordinateLimit = 20_000_000
18
18
  const hoverHitTestIntervalMs = 64
19
19
  const overviewClusterMaxCount = 1400
20
- const zoomRecoveryGuardMs = 560
20
+ const zoomRecoveryGuardMs = 1500
21
21
  const state = {
22
22
  graph: { nodes: [], edges: [] },
23
23
  nodes: [],
@@ -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
 
@@ -601,6 +603,27 @@ const enrichSampleWithNeighbors = (nodes) => {
601
603
  }
602
604
  }
603
605
 
606
+ const ensureHubNodesInRenderedSet = (nodes) => {
607
+ if (nodes.length === 0) {
608
+ return nodes
609
+ }
610
+
611
+ const maxNodes = Math.max(renderNodeBudget, nodes.length)
612
+ const ids = new Set(nodes.map((node) => node.id))
613
+ const hubs = rankedHubNodes()
614
+ const merged = [...nodes]
615
+
616
+ for (let index = 0; index < hubs.length && merged.length < maxNodes; index += 1) {
617
+ const hub = hubs[index]
618
+ if (!ids.has(hub.id)) {
619
+ merged.push(hub)
620
+ ids.add(hub.id)
621
+ }
622
+ }
623
+
624
+ return merged
625
+ }
626
+
604
627
  const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
605
628
  const isFiniteNumber = value => Number.isFinite(value)
606
629
  const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
@@ -658,7 +681,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
658
681
  return { min: 0.0008, max: 0.24 }
659
682
  }
660
683
 
661
- const fitView = (options = { useFiltered: true, macro: false }) => {
684
+ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
662
685
  const rect = canvas.getBoundingClientRect()
663
686
  const width = Math.max(rect.width, 320)
664
687
  const height = Math.max(rect.height, 320)
@@ -695,8 +718,12 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
695
718
  : nodes.length > massiveGraphNodeThreshold
696
719
  ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
697
720
  : baselineScale
698
- const centerX = (bounds.minX + bounds.maxX) / 2
699
- 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
700
727
 
701
728
  state.transform = {
702
729
  x: clampTransformCoordinate(width / 2 - centerX * scale),
@@ -708,7 +735,7 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
708
735
  markRenderDirty()
709
736
  }
710
737
 
711
- const resetView = () => fitView({ useFiltered: false, macro: true })
738
+ const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
712
739
 
713
740
  const createLayout = graph => {
714
741
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
@@ -1100,12 +1127,13 @@ const computeRenderVisibility = () => {
1100
1127
  : sourceNodes.slice(0, Math.min(sourceNodes.length, renderNodeBudget))
1101
1128
  const sampledIds = new Set(sampled.map((node) => node.id))
1102
1129
  let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1103
- let sampledNodes = sampled
1130
+ let sampledNodes = ensureHubNodesInRenderedSet(sampled)
1104
1131
 
1105
1132
  if (state.transform.scale >= 0.035 && sampledEdges.length === 0) {
1106
- const enriched = enrichSampleWithNeighbors(sampled)
1107
- sampledNodes = enriched.nodes
1108
- sampledEdges = enriched.edges
1133
+ const enriched = enrichSampleWithNeighbors(sampledNodes)
1134
+ sampledNodes = ensureHubNodesInRenderedSet(enriched.nodes)
1135
+ const sampledWithHubsIds = new Set(sampledNodes.map((node) => node.id))
1136
+ sampledEdges = collectVisibleEdgesForNodes(sampledWithHubsIds)
1109
1137
  }
1110
1138
 
1111
1139
  state.renderClusters = []
@@ -1159,10 +1187,11 @@ const computeRenderVisibility = () => {
1159
1187
  return
1160
1188
  }
1161
1189
 
1162
- const nodeIds = new Set(nodes.map((node) => node.id))
1190
+ const normalizedNodes = ensureHubNodesInRenderedSet(nodes)
1191
+ const nodeIds = new Set(normalizedNodes.map((node) => node.id))
1163
1192
  const edges = collectVisibleEdgesForNodes(nodeIds)
1164
1193
 
1165
- state.renderNodes = nodes
1194
+ state.renderNodes = normalizedNodes
1166
1195
  state.renderEdges = edges
1167
1196
 
1168
1197
  if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
@@ -1456,17 +1485,19 @@ const selectNodeById = id => {
1456
1485
  }
1457
1486
 
1458
1487
  const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
1488
+ if (source === 'wheel') {
1489
+ state.lastManualZoomAt = performance.now()
1490
+ }
1459
1491
  const nextScale = clampScale(state.transform.scale * factor)
1460
- if (nextScale === state.transform.scale) return
1492
+ if (nextScale === state.transform.scale) {
1493
+ return
1494
+ }
1461
1495
  const worldX = (screenX - state.transform.x) / state.transform.scale
1462
1496
  const worldY = (screenY - state.transform.y) / state.transform.scale
1463
1497
  state.transform.scale = clampScale(nextScale)
1464
1498
  state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
1465
1499
  state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
1466
1500
  state.offscreenFrameCount = 0
1467
- if (source === 'wheel') {
1468
- state.lastManualZoomAt = performance.now()
1469
- }
1470
1501
  markRenderDirty()
1471
1502
  }
1472
1503
 
@@ -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.51",
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",