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

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,10 +17,11 @@ 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: [],
24
+ nodeById: new Map(),
24
25
  edges: [],
25
26
  visibleNodes: [],
26
27
  visibleEdges: [],
@@ -555,6 +556,72 @@ const sampleVisibleNodes = (limit = renderNodeBudget, sourceNodes = state.visibl
555
556
  return nodes
556
557
  }
557
558
 
559
+ const enrichSampleWithNeighbors = (nodes) => {
560
+ if (nodes.length === 0) {
561
+ return {
562
+ nodes,
563
+ edges: []
564
+ }
565
+ }
566
+
567
+ const maxNodes = Math.min(renderNodeBudget, nodes.length + 200)
568
+ const expanded = [...nodes]
569
+ const ids = new Set(expanded.map((node) => node.id))
570
+
571
+ for (let index = 0; index < nodes.length && expanded.length < maxNodes; index += 1) {
572
+ const node = nodes[index]
573
+ const candidates = [...(state.visibleEdgeByNode.get(node.id) ?? [])]
574
+ .filter((edge) => edge.target)
575
+ .sort((left, right) => edgeWeight(right) - edgeWeight(left))
576
+ .slice(0, 3)
577
+
578
+ for (let candidateIndex = 0; candidateIndex < candidates.length && expanded.length < maxNodes; candidateIndex += 1) {
579
+ const edge = candidates[candidateIndex]
580
+ const otherId = edge.source === node.id ? edge.target : edge.source
581
+
582
+ if (!otherId || ids.has(otherId)) {
583
+ continue
584
+ }
585
+
586
+ const otherNode = state.nodeById.get(otherId)
587
+ if (!otherNode) {
588
+ continue
589
+ }
590
+
591
+ ids.add(otherId)
592
+ expanded.push(otherNode)
593
+ }
594
+ }
595
+
596
+ const edges = collectVisibleEdgesForNodes(ids)
597
+
598
+ return {
599
+ nodes: expanded,
600
+ edges
601
+ }
602
+ }
603
+
604
+ const ensureHubNodesInRenderedSet = (nodes) => {
605
+ if (nodes.length === 0) {
606
+ return nodes
607
+ }
608
+
609
+ const maxNodes = Math.max(renderNodeBudget, nodes.length)
610
+ const ids = new Set(nodes.map((node) => node.id))
611
+ const hubs = rankedHubNodes()
612
+ const merged = [...nodes]
613
+
614
+ for (let index = 0; index < hubs.length && merged.length < maxNodes; index += 1) {
615
+ const hub = hubs[index]
616
+ if (!ids.has(hub.id)) {
617
+ merged.push(hub)
618
+ ids.add(hub.id)
619
+ }
620
+ }
621
+
622
+ return merged
623
+ }
624
+
558
625
  const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
559
626
  const isFiniteNumber = value => Number.isFinite(value)
560
627
  const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
@@ -1053,9 +1120,19 @@ const computeRenderVisibility = () => {
1053
1120
  ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), sourceNodes)
1054
1121
  : sourceNodes.slice(0, Math.min(sourceNodes.length, renderNodeBudget))
1055
1122
  const sampledIds = new Set(sampled.map((node) => node.id))
1123
+ let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1124
+ let sampledNodes = ensureHubNodesInRenderedSet(sampled)
1125
+
1126
+ if (state.transform.scale >= 0.035 && sampledEdges.length === 0) {
1127
+ const enriched = enrichSampleWithNeighbors(sampledNodes)
1128
+ sampledNodes = ensureHubNodesInRenderedSet(enriched.nodes)
1129
+ const sampledWithHubsIds = new Set(sampledNodes.map((node) => node.id))
1130
+ sampledEdges = collectVisibleEdgesForNodes(sampledWithHubsIds)
1131
+ }
1132
+
1056
1133
  state.renderClusters = []
1057
- state.renderNodes = sampled
1058
- state.renderEdges = state.transform.scale >= 0.1 ? collectVisibleEdgesForNodes(sampledIds) : []
1134
+ state.renderNodes = sampledNodes
1135
+ state.renderEdges = sampledEdges
1059
1136
  return
1060
1137
  }
1061
1138
 
@@ -1104,10 +1181,11 @@ const computeRenderVisibility = () => {
1104
1181
  return
1105
1182
  }
1106
1183
 
1107
- const nodeIds = new Set(nodes.map((node) => node.id))
1184
+ const normalizedNodes = ensureHubNodesInRenderedSet(nodes)
1185
+ const nodeIds = new Set(normalizedNodes.map((node) => node.id))
1108
1186
  const edges = collectVisibleEdgesForNodes(nodeIds)
1109
1187
 
1110
- state.renderNodes = nodes
1188
+ state.renderNodes = normalizedNodes
1111
1189
  state.renderEdges = edges
1112
1190
 
1113
1191
  if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
@@ -1212,11 +1290,13 @@ const render = now => {
1212
1290
  state.offscreenFrameCount = 0
1213
1291
  }
1214
1292
  const minimumEdgeScale =
1215
- state.nodes.length > massiveGraphNodeThreshold
1216
- ? 0.1
1217
- : state.nodes.length > largeGraphNodeThreshold
1218
- ? 0.16
1219
- : 0
1293
+ state.renderNodes.length > 1300
1294
+ ? 0.12
1295
+ : state.renderNodes.length > 900
1296
+ ? 0.085
1297
+ : state.renderNodes.length > 500
1298
+ ? 0.05
1299
+ : 0
1220
1300
  const drawEdges =
1221
1301
  state.renderClusters.length === 0 &&
1222
1302
  state.transform.scale >= minimumEdgeScale
@@ -1399,17 +1479,19 @@ const selectNodeById = id => {
1399
1479
  }
1400
1480
 
1401
1481
  const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
1482
+ if (source === 'wheel') {
1483
+ state.lastManualZoomAt = performance.now()
1484
+ }
1402
1485
  const nextScale = clampScale(state.transform.scale * factor)
1403
- if (nextScale === state.transform.scale) return
1486
+ if (nextScale === state.transform.scale) {
1487
+ return
1488
+ }
1404
1489
  const worldX = (screenX - state.transform.x) / state.transform.scale
1405
1490
  const worldY = (screenY - state.transform.y) / state.transform.scale
1406
1491
  state.transform.scale = clampScale(nextScale)
1407
1492
  state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
1408
1493
  state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
1409
1494
  state.offscreenFrameCount = 0
1410
- if (source === 'wheel') {
1411
- state.lastManualZoomAt = performance.now()
1412
- }
1413
1495
  markRenderDirty()
1414
1496
  }
1415
1497
 
@@ -1627,6 +1709,7 @@ const loadGraph = async (options = { reset: false }) => {
1627
1709
  state.graphSignature = signature
1628
1710
  state.graph = graph
1629
1711
  state.nodes = layout.nodes
1712
+ state.nodeById = new Map(state.nodes.map((node) => [node.id, node]))
1630
1713
  state.edges = layout.edges
1631
1714
  state.nodeDegrees = state.edges.reduce((degrees, edge) => {
1632
1715
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edgeWeight(edge))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.50",
3
+ "version": "0.1.0-beta.52",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",