@andespindola/brainlink 0.1.0-beta.84 → 0.1.0-beta.86

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.
package/README.md CHANGED
@@ -84,10 +84,11 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
84
84
  - Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
85
85
  - Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
86
86
  - WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
87
+ - Graph zoom-out renders hierarchical ecosystem subgraphs: macro clusters of up to 1000 notes expand near the user's focus into smaller graph meshes before individual notes are rendered.
87
88
  - Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
88
89
  - Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
89
90
  - Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
90
- - Zoomed-out graph LOD clusters dense regions and progressively expands the focused viewport as zoom increases, including very large vaults.
91
+ - Zoomed-out graph LOD renders nested subgraphs and progressively expands only the focused cluster as zoom increases, including very large vaults.
91
92
  - Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
92
93
  - Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
93
94
  - Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
@@ -601,7 +602,7 @@ The graph UI shows:
601
602
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
602
603
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
603
604
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
604
- - massive-graph LOD progression: very low zoom uses spatial overview sampling plus hub-neighborhood edge previews to preserve whole-vault shape and orientation, then progressively raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
605
+ - graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes, zoom-in expands only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
605
606
 
606
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
607
608
 
@@ -21,14 +21,19 @@ const viewportPaddingPx = 280
21
21
  const worldCoordinateLimit = 5_000_000
22
22
  const transformCoordinateLimit = 20_000_000
23
23
  const hoverHitTestIntervalMs = 64
24
- const overviewClusterMaxCount = 1400
24
+ const ecosystemGroupSize = 1000
25
+ const ecosystemGroupSizes = [1000, 250, 60]
26
+ const ecosystemClusterEdgeLimit = 520
27
+ const ecosystemClusterScaleThreshold = 0.32
28
+ const ecosystemSubgraphScaleThreshold = 0.18
29
+ const ecosystemMicroScaleThreshold = 0.08
30
+ const ecosystemFocusedParentLimit = 3
25
31
  const zoomRecoveryGuardMs = 4200
26
32
  const zoomCapTargetViewportShare = 0.72
27
33
  const meshEdgeScaleThreshold = 0.09
28
34
  const meshEdgeMinBudget = 140
29
35
  const meshEdgeMaxBudget = 1400
30
36
  const layeredCoreScaleThreshold = 0.55
31
- const massiveOverviewClusterScaleThreshold = 0.035
32
37
  const dragNeighborhoodMaxAffected = 180
33
38
  const dragSettleRounds = 3
34
39
  const wheelZoomExponent = 0.0018
@@ -44,6 +49,7 @@ const state = {
44
49
  renderNodes: [],
45
50
  renderEdges: [],
46
51
  renderClusters: [],
52
+ renderClusterEdges: [],
47
53
  nodeDegrees: new Map(),
48
54
  selected: null,
49
55
  hovered: null,
@@ -65,7 +71,8 @@ const state = {
65
71
  lastViewportKey: '',
66
72
  visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
67
73
  visibleEdgeByNode: new Map(),
68
- overviewClusters: [],
74
+ ecosystemClusters: [],
75
+ ecosystemClustersBySize: new Map(),
69
76
  macroCenter: { x: 0, y: 0 },
70
77
  macroRepresentative: null,
71
78
  primaryHub: null,
@@ -562,7 +569,11 @@ const recomputeVisibility = () => {
562
569
  state.visibleEdges = limitedEdges
563
570
  state.visibleNodeSpatial = createSpatialIndex(nodes)
564
571
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
565
- state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
572
+ const ecosystemGraph = nodes.length > 1
573
+ ? buildEcosystemGraph(nodes)
574
+ : { clusters: [], clustersBySize: new Map() }
575
+ state.ecosystemClusters = ecosystemGraph.clusters
576
+ state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
566
577
  const primaryHub = rankedHubNodes()[0] ?? null
567
578
  state.primaryHub = primaryHub
568
579
  state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
@@ -679,68 +690,219 @@ const createVisibleEdgeLookup = edges => {
679
690
  return lookup
680
691
  }
681
692
 
682
- const buildOverviewClusters = nodes => {
683
- if (nodes.length === 0) {
684
- return []
693
+ const ecosystemKeyForNode = node => {
694
+ if (typeof node.segment === 'string' && node.segment.trim()) {
695
+ return node.segment.trim()
685
696
  }
686
-
687
- const bounds = graphBounds(nodes)
688
- if (!bounds) {
689
- return []
697
+ if (typeof node.group === 'string' && node.group.trim()) {
698
+ return node.group.trim()
690
699
  }
700
+ const pathParts = String(node.path || '')
701
+ .split('/')
702
+ .filter(part => part.trim())
703
+ .slice(0, 2)
704
+ return pathParts.length > 0 ? pathParts.join('/') : 'root'
705
+ }
691
706
 
692
- const longest = Math.max(bounds.width, bounds.height, 1)
693
- const cellSize = Math.max(longest / 56, 900)
694
- const buckets = new Map()
707
+ const compareNodesForEcosystem = (left, right) => {
708
+ const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
709
+ if (keyComparison !== 0) return keyComparison
710
+ const leftDegree = state.nodeDegrees.get(left.id) ?? 0
711
+ const rightDegree = state.nodeDegrees.get(right.id) ?? 0
712
+ if (leftDegree !== rightDegree) return rightDegree - leftDegree
713
+ return String(left.title || left.id).localeCompare(String(right.title || right.id))
714
+ }
715
+
716
+ const selectEcosystemRepresentative = nodes => {
717
+ let representative = nodes[0] ?? null
718
+ let representativeScore = Number.NEGATIVE_INFINITY
695
719
 
696
720
  for (let index = 0; index < nodes.length; index += 1) {
697
721
  const node = nodes[index]
698
- const keyX = Math.floor((node.x - bounds.minX) / cellSize)
699
- const keyY = Math.floor((node.y - bounds.minY) / cellSize)
700
- const key = keyX + ':' + keyY
701
- const degree = state.nodeDegrees.get(node.id) ?? 0
702
- const current = buckets.get(key)
722
+ const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
723
+ if (score > representativeScore) {
724
+ representative = node
725
+ representativeScore = score
726
+ }
727
+ }
728
+
729
+ return representative
730
+ }
731
+
732
+ const buildEcosystemCluster = (nodes, index) => {
733
+ const count = Math.max(nodes.length, 1)
734
+ const sum = nodes.reduce((accumulator, node) => ({
735
+ x: accumulator.x + node.x,
736
+ y: accumulator.y + node.y
737
+ }), { x: 0, y: 0 })
738
+ const representative = selectEcosystemRepresentative(nodes)
739
+
740
+ return {
741
+ id: 'ecosystem-' + index,
742
+ x: sum.x / count,
743
+ y: sum.y / count,
744
+ count,
745
+ nodeIds: nodes.map(node => node.id),
746
+ representative,
747
+ label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
748
+ }
749
+ }
750
+
751
+ const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
752
+ const clusters = []
753
+ const clusterByNodeId = new Map()
754
+
755
+ for (let offset = 0; offset < sortedNodes.length; offset += size) {
756
+ const clusterNodes = sortedNodes.slice(offset, offset + size)
757
+ const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
758
+ const cluster = {
759
+ ...buildEcosystemCluster(clusterNodes, clusters.length),
760
+ id: 'ecosystem-' + size + '-' + clusters.length,
761
+ size,
762
+ parentId: parentCluster?.id ?? null
763
+ }
764
+ clusters.push(cluster)
765
+ for (let index = 0; index < clusterNodes.length; index += 1) {
766
+ clusterByNodeId.set(clusterNodes[index].id, cluster)
767
+ }
768
+ }
769
+
770
+ return { clusters, clusterByNodeId }
771
+ }
772
+
773
+ const buildEcosystemGraph = (nodes) => {
774
+ if (nodes.length === 0) {
775
+ return { clusters: [], clustersBySize: new Map() }
776
+ }
777
+
778
+ const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
779
+ const clustersBySize = new Map()
780
+ let parentLookup = null
781
+
782
+ for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
783
+ const size = ecosystemGroupSizes[index]
784
+ const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
785
+ clustersBySize.set(size, level.clusters)
786
+ parentLookup = level.clusterByNodeId
787
+ }
788
+
789
+ return {
790
+ clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
791
+ clustersBySize
792
+ }
793
+ }
794
+
795
+ const isClusterInViewport = (cluster, viewport) =>
796
+ cluster.x >= viewport.minX &&
797
+ cluster.x <= viewport.maxX &&
798
+ cluster.y >= viewport.minY &&
799
+ cluster.y <= viewport.maxY
800
+
801
+ const filterEcosystemClustersByViewport = (clusters, viewport) => {
802
+ const visible = clusters.filter(cluster => isClusterInViewport(cluster, viewport))
803
+ return visible.length > 0 ? visible : [...clusters]
804
+ }
805
+
806
+ const ecosystemFocusPoint = () => {
807
+ const now = performance.now()
808
+ if (now - state.lastZoomFocus.at <= 1800) {
809
+ return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
810
+ }
811
+ return viewportCenterWorldPoint()
812
+ }
813
+
814
+ const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
815
+ clusters
816
+ .map(cluster => ({
817
+ cluster,
818
+ distance: Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y)
819
+ }))
820
+ .sort((left, right) => left.distance - right.distance)
821
+ .slice(0, limit)
822
+ .map(item => item.cluster.id)
823
+
824
+ const ecosystemPlanForScale = scale => {
825
+ if (scale <= ecosystemMicroScaleThreshold) {
826
+ return { baseSize: 1000, childSize: null }
827
+ }
828
+ if (scale <= ecosystemSubgraphScaleThreshold) {
829
+ return { baseSize: 1000, childSize: 250 }
830
+ }
831
+ return { baseSize: 250, childSize: 60 }
832
+ }
833
+
834
+ const selectHierarchicalEcosystemClusters = viewport => {
835
+ const plan = ecosystemPlanForScale(state.transform.scale)
836
+ const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
837
+ const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
838
+
839
+ if (!plan.childSize) {
840
+ return visibleBaseClusters
841
+ }
842
+
843
+ const focusPoint = ecosystemFocusPoint()
844
+ const expandedParentIds = new Set(nearestEcosystemParentIds(
845
+ visibleBaseClusters,
846
+ focusPoint,
847
+ ecosystemFocusedParentLimit
848
+ ))
849
+ const childClusters = state.ecosystemClustersBySize.get(plan.childSize) ?? []
850
+ const visibleChildClusters = childClusters.filter(cluster =>
851
+ expandedParentIds.has(cluster.parentId) &&
852
+ isClusterInViewport(cluster, viewport)
853
+ )
854
+
855
+ if (visibleChildClusters.length === 0) {
856
+ return visibleBaseClusters
857
+ }
858
+
859
+ return [
860
+ ...visibleBaseClusters.filter(cluster => !expandedParentIds.has(cluster.id)),
861
+ ...visibleChildClusters
862
+ ]
863
+ }
864
+
865
+ const ecosystemEdgesForClusters = clusters => {
866
+ const clusterByNodeId = new Map()
867
+ for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
868
+ const cluster = clusters[clusterIndex]
869
+ for (let nodeIndex = 0; nodeIndex < cluster.nodeIds.length; nodeIndex += 1) {
870
+ clusterByNodeId.set(cluster.nodeIds[nodeIndex], cluster)
871
+ }
872
+ }
873
+
874
+ const edgeByClusterPair = new Map()
875
+ for (let index = 0; index < state.visibleEdges.length; index += 1) {
876
+ const edge = state.visibleEdges[index]
877
+ const sourceCluster = clusterByNodeId.get(edge.source)
878
+ const targetCluster = clusterByNodeId.get(edge.target)
879
+ if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
880
+ continue
881
+ }
882
+
883
+ const orderedIds = sourceCluster.id < targetCluster.id
884
+ ? [sourceCluster.id, targetCluster.id]
885
+ : [targetCluster.id, sourceCluster.id]
886
+ const key = orderedIds.join(':')
887
+ const current = edgeByClusterPair.get(key)
703
888
  if (current) {
704
- current.count += 1
705
- current.sumX += node.x
706
- current.sumY += node.y
707
- if (degree > current.degree) {
708
- current.representative = node
709
- current.degree = degree
710
- }
889
+ current.weight += edgeWeight(edge)
711
890
  continue
712
891
  }
713
892
 
714
- buckets.set(key, {
893
+ edgeByClusterPair.set(key, {
715
894
  id: key,
716
- count: 1,
717
- sumX: node.x,
718
- sumY: node.y,
719
- representative: node,
720
- degree
895
+ sourceCluster,
896
+ targetCluster,
897
+ weight: edgeWeight(edge)
721
898
  })
722
899
  }
723
900
 
724
- return Array.from(buckets.values())
725
- .sort((left, right) => right.count - left.count)
726
- .slice(0, overviewClusterMaxCount)
727
- .map((cluster) => ({
728
- id: cluster.id,
729
- x: cluster.sumX / Math.max(cluster.count, 1),
730
- y: cluster.sumY / Math.max(cluster.count, 1),
731
- count: cluster.count,
732
- representative: cluster.representative
733
- }))
901
+ return Array.from(edgeByClusterPair.values())
902
+ .sort((left, right) => right.weight - left.weight)
903
+ .slice(0, ecosystemClusterEdgeLimit)
734
904
  }
735
905
 
736
- const filterOverviewClustersByViewport = viewport =>
737
- state.overviewClusters.filter((cluster) =>
738
- cluster.x >= viewport.minX &&
739
- cluster.x <= viewport.maxX &&
740
- cluster.y >= viewport.minY &&
741
- cluster.y <= viewport.maxY
742
- )
743
-
744
906
  const edgeBudgetForCurrentFrame = () => {
745
907
  const zoom = state.transform.scale
746
908
  if (zoom < 0.12) return 380
@@ -775,14 +937,6 @@ const nodeBudgetForScale = (scale) => {
775
937
  return renderNodeBudget
776
938
  }
777
939
 
778
- const massiveLowZoomNodeBudgetForScale = (scale) => {
779
- if (scale < 0.004) return 780
780
- if (scale < 0.01) return 860
781
- if (scale < 0.02) return 900
782
- if (scale < 0.035) return 900
783
- return renderNodeBudget
784
- }
785
-
786
940
  const layerFocusForScale = (scale) => {
787
941
  const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
788
942
  const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
@@ -2316,6 +2470,7 @@ const computeRenderVisibility = () => {
2316
2470
  }
2317
2471
  state.lastViewportKey = viewportKey
2318
2472
  state.renderVisibilityDirty = false
2473
+ state.renderClusterEdges = []
2319
2474
 
2320
2475
  const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
2321
2476
 
@@ -2339,12 +2494,24 @@ const computeRenderVisibility = () => {
2339
2494
  state.renderNodes = []
2340
2495
  }
2341
2496
  state.renderEdges = []
2497
+ state.renderClusterEdges = []
2498
+ return
2499
+ }
2500
+
2501
+ if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
2502
+ const clusters = selectHierarchicalEcosystemClusters(viewport)
2503
+ .sort((left, right) => right.count - left.count)
2504
+ state.renderClusters = clusters
2505
+ state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
2506
+ state.renderNodes = []
2507
+ state.renderEdges = []
2342
2508
  return
2343
2509
  }
2344
2510
 
2345
2511
  if (state.visibleNodes.length <= 2000) {
2346
2512
  state.renderNodes = state.visibleNodes
2347
2513
  state.renderClusters = []
2514
+ state.renderClusterEdges = []
2348
2515
  const ids = new Set(state.renderNodes.map((node) => node.id))
2349
2516
  state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
2350
2517
  return
@@ -2352,30 +2519,6 @@ const computeRenderVisibility = () => {
2352
2519
 
2353
2520
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
2354
2521
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2355
- if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
2356
- const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
2357
- const overviewClusters = filterOverviewClustersByViewport(viewport)
2358
- .sort((left, right) => right.count - left.count)
2359
- .slice(0, overviewLimit)
2360
- if (overviewClusters.length > 0) {
2361
- const overviewNodes = representativeNodesFromClusters(
2362
- overviewClusters,
2363
- overviewLimit
2364
- )
2365
- const anchoredNodes = includeHubPreviewNeighborhood(
2366
- overviewNodes,
2367
- Math.min(renderNodeBudget, overviewLimit)
2368
- )
2369
- const enriched = enrichSampleWithNeighbors(anchoredNodes)
2370
- const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
2371
- const previewIds = new Set(previewNodes.map((node) => node.id))
2372
- const previewEdges = collectVisibleEdgesForNodes(previewIds)
2373
- state.renderClusters = []
2374
- state.renderNodes = previewNodes
2375
- state.renderEdges = previewEdges
2376
- return
2377
- }
2378
- }
2379
2522
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
2380
2523
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
2381
2524
  const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
@@ -2421,6 +2564,7 @@ const computeRenderVisibility = () => {
2421
2564
  }
2422
2565
 
2423
2566
  state.renderClusters = []
2567
+ state.renderClusterEdges = []
2424
2568
  state.renderNodes = sampledNodes
2425
2569
  state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
2426
2570
  return
@@ -2430,6 +2574,7 @@ const computeRenderVisibility = () => {
2430
2574
  const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
2431
2575
  const sampledIds = new Set(sampled.map((node) => node.id))
2432
2576
  state.renderClusters = []
2577
+ state.renderClusterEdges = []
2433
2578
  state.renderNodes = sampled
2434
2579
  state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
2435
2580
  return
@@ -2439,11 +2584,13 @@ const computeRenderVisibility = () => {
2439
2584
  const clusters = clusterViewportNodes(viewportNodes)
2440
2585
  if (clusters.length > 0) {
2441
2586
  state.renderClusters = []
2587
+ state.renderClusterEdges = []
2442
2588
  state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
2443
2589
  state.renderEdges = []
2444
2590
  return
2445
2591
  }
2446
2592
  state.renderClusters = []
2593
+ state.renderClusterEdges = []
2447
2594
  const stride = viewportNodeStride()
2448
2595
  const picked = []
2449
2596
 
@@ -2467,6 +2614,7 @@ const computeRenderVisibility = () => {
2467
2614
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2468
2615
  state.renderNodes = fallbackNodes
2469
2616
  state.renderClusters = []
2617
+ state.renderClusterEdges = []
2470
2618
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2471
2619
  return
2472
2620
  }
@@ -2482,6 +2630,7 @@ const computeRenderVisibility = () => {
2482
2630
  const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
2483
2631
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2484
2632
  state.renderClusters = []
2633
+ state.renderClusterEdges = []
2485
2634
  state.renderNodes = fallbackNodes
2486
2635
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2487
2636
  }
@@ -2598,6 +2747,17 @@ const render = now => {
2598
2747
  ctx.translate(state.transform.x, state.transform.y)
2599
2748
  ctx.scale(state.transform.scale, state.transform.scale)
2600
2749
  const safeScale = Math.max(state.transform.scale, 0.0001)
2750
+ if (state.renderClusterEdges.length > 0) {
2751
+ ctx.beginPath()
2752
+ for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
2753
+ const edge = state.renderClusterEdges[index]
2754
+ ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
2755
+ ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
2756
+ }
2757
+ ctx.lineWidth = 1.2 / safeScale
2758
+ ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
2759
+ ctx.stroke()
2760
+ }
2601
2761
  state.renderClusters.forEach(cluster => {
2602
2762
  const isMacro = cluster.id === 'macro-galaxy'
2603
2763
  const radiusPx = isMacro
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.84",
3
+ "version": "0.1.0-beta.86",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",