@andespindola/brainlink 0.1.0-beta.83 → 0.1.0-beta.85

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,6 +84,7 @@ 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 connected ecosystem clusters of up to 1000 notes before expanding into individual notes, keeping vaults visually oriented with a smaller render scope.
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.
@@ -600,7 +601,8 @@ The graph UI shows:
600
601
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
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
- - 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
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
605
+ - graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes with aggregated real links, then progressively raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
604
606
 
605
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
606
608
 
@@ -21,14 +21,15 @@ 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 ecosystemClusterEdgeLimit = 520
26
+ const ecosystemClusterScaleThreshold = 0.08
25
27
  const zoomRecoveryGuardMs = 4200
26
28
  const zoomCapTargetViewportShare = 0.72
27
29
  const meshEdgeScaleThreshold = 0.09
28
30
  const meshEdgeMinBudget = 140
29
31
  const meshEdgeMaxBudget = 1400
30
32
  const layeredCoreScaleThreshold = 0.55
31
- const massiveOverviewClusterScaleThreshold = 0.035
32
33
  const dragNeighborhoodMaxAffected = 180
33
34
  const dragSettleRounds = 3
34
35
  const wheelZoomExponent = 0.0018
@@ -44,6 +45,7 @@ const state = {
44
45
  renderNodes: [],
45
46
  renderEdges: [],
46
47
  renderClusters: [],
48
+ renderClusterEdges: [],
47
49
  nodeDegrees: new Map(),
48
50
  selected: null,
49
51
  hovered: null,
@@ -65,7 +67,8 @@ const state = {
65
67
  lastViewportKey: '',
66
68
  visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
67
69
  visibleEdgeByNode: new Map(),
68
- overviewClusters: [],
70
+ ecosystemClusters: [],
71
+ ecosystemEdges: [],
69
72
  macroCenter: { x: 0, y: 0 },
70
73
  macroRepresentative: null,
71
74
  primaryHub: null,
@@ -537,6 +540,17 @@ const nearestHubNeighborDistance = (hub, nodes) => {
537
540
  return minimum
538
541
  }
539
542
 
543
+ const isDominantHub = (hub, nodeCount = state.visibleNodes.length) => {
544
+ if (!hub || nodeCount <= 0) {
545
+ return false
546
+ }
547
+
548
+ const degree = state.nodeDegrees.get(hub.id) ?? 0
549
+ const minimumDegree = Math.max(18, Math.sqrt(nodeCount) * 1.8)
550
+ const degreeRatio = degree / Math.max(nodeCount, 1)
551
+ return degree >= minimumDegree || degreeRatio >= 0.035
552
+ }
553
+
540
554
  const recomputeVisibility = () => {
541
555
  const nodes = filteredNodes()
542
556
  const ids = new Set(nodes.map(node => node.id))
@@ -551,15 +565,20 @@ const recomputeVisibility = () => {
551
565
  state.visibleEdges = limitedEdges
552
566
  state.visibleNodeSpatial = createSpatialIndex(nodes)
553
567
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
554
- state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
568
+ const ecosystemGraph = nodes.length > 1
569
+ ? buildEcosystemGraph(nodes, limitedEdges)
570
+ : { clusters: [], edges: [] }
571
+ state.ecosystemClusters = ecosystemGraph.clusters
572
+ state.ecosystemEdges = ecosystemGraph.edges
555
573
  const primaryHub = rankedHubNodes()[0] ?? null
556
574
  state.primaryHub = primaryHub
557
575
  state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
558
576
  const bounds = graphBounds(nodes)
577
+ const macroHub = isDominantHub(primaryHub, nodes.length) ? primaryHub : null
559
578
  state.macroCenter = bounds
560
579
  ? {
561
- x: primaryHub ? primaryHub.x : (bounds.minX + bounds.maxX) / 2,
562
- y: primaryHub ? primaryHub.y : (bounds.minY + bounds.maxY) / 2
580
+ x: macroHub ? macroHub.x : (bounds.minX + bounds.maxX) / 2,
581
+ y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
563
582
  }
564
583
  : { x: 0, y: 0 }
565
584
  state.macroRepresentative = resolveMacroRepresentative(nodes)
@@ -667,68 +686,131 @@ const createVisibleEdgeLookup = edges => {
667
686
  return lookup
668
687
  }
669
688
 
670
- const buildOverviewClusters = nodes => {
671
- if (nodes.length === 0) {
672
- return []
689
+ const ecosystemKeyForNode = node => {
690
+ if (typeof node.segment === 'string' && node.segment.trim()) {
691
+ return node.segment.trim()
673
692
  }
674
-
675
- const bounds = graphBounds(nodes)
676
- if (!bounds) {
677
- return []
693
+ if (typeof node.group === 'string' && node.group.trim()) {
694
+ return node.group.trim()
678
695
  }
696
+ const pathParts = String(node.path || '')
697
+ .split('/')
698
+ .filter(part => part.trim())
699
+ .slice(0, 2)
700
+ return pathParts.length > 0 ? pathParts.join('/') : 'root'
701
+ }
679
702
 
680
- const longest = Math.max(bounds.width, bounds.height, 1)
681
- const cellSize = Math.max(longest / 56, 900)
682
- const buckets = new Map()
703
+ const compareNodesForEcosystem = (left, right) => {
704
+ const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
705
+ if (keyComparison !== 0) return keyComparison
706
+ const leftDegree = state.nodeDegrees.get(left.id) ?? 0
707
+ const rightDegree = state.nodeDegrees.get(right.id) ?? 0
708
+ if (leftDegree !== rightDegree) return rightDegree - leftDegree
709
+ return String(left.title || left.id).localeCompare(String(right.title || right.id))
710
+ }
711
+
712
+ const selectEcosystemRepresentative = nodes => {
713
+ let representative = nodes[0] ?? null
714
+ let representativeScore = Number.NEGATIVE_INFINITY
683
715
 
684
716
  for (let index = 0; index < nodes.length; index += 1) {
685
717
  const node = nodes[index]
686
- const keyX = Math.floor((node.x - bounds.minX) / cellSize)
687
- const keyY = Math.floor((node.y - bounds.minY) / cellSize)
688
- const key = keyX + ':' + keyY
689
- const degree = state.nodeDegrees.get(node.id) ?? 0
690
- const current = buckets.get(key)
718
+ const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
719
+ if (score > representativeScore) {
720
+ representative = node
721
+ representativeScore = score
722
+ }
723
+ }
724
+
725
+ return representative
726
+ }
727
+
728
+ const buildEcosystemCluster = (nodes, index) => {
729
+ const count = Math.max(nodes.length, 1)
730
+ const sum = nodes.reduce((accumulator, node) => ({
731
+ x: accumulator.x + node.x,
732
+ y: accumulator.y + node.y
733
+ }), { x: 0, y: 0 })
734
+ const representative = selectEcosystemRepresentative(nodes)
735
+
736
+ return {
737
+ id: 'ecosystem-' + index,
738
+ x: sum.x / count,
739
+ y: sum.y / count,
740
+ count,
741
+ representative,
742
+ label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
743
+ }
744
+ }
745
+
746
+ const buildEcosystemGraph = (nodes, edges) => {
747
+ if (nodes.length === 0) {
748
+ return { clusters: [], edges: [] }
749
+ }
750
+
751
+ const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
752
+ const clusters = []
753
+ const clusterByNodeId = new Map()
754
+
755
+ for (let offset = 0; offset < sortedNodes.length; offset += ecosystemGroupSize) {
756
+ const clusterNodes = sortedNodes.slice(offset, offset + ecosystemGroupSize)
757
+ const cluster = buildEcosystemCluster(clusterNodes, clusters.length)
758
+ clusters.push(cluster)
759
+ for (let index = 0; index < clusterNodes.length; index += 1) {
760
+ clusterByNodeId.set(clusterNodes[index].id, cluster)
761
+ }
762
+ }
763
+
764
+ const edgeByClusterPair = new Map()
765
+ for (let index = 0; index < edges.length; index += 1) {
766
+ const edge = edges[index]
767
+ const sourceCluster = clusterByNodeId.get(edge.source)
768
+ const targetCluster = clusterByNodeId.get(edge.target)
769
+ if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
770
+ continue
771
+ }
772
+
773
+ const orderedIds = sourceCluster.id < targetCluster.id
774
+ ? [sourceCluster.id, targetCluster.id]
775
+ : [targetCluster.id, sourceCluster.id]
776
+ const key = orderedIds.join(':')
777
+ const current = edgeByClusterPair.get(key)
691
778
  if (current) {
692
- current.count += 1
693
- current.sumX += node.x
694
- current.sumY += node.y
695
- if (degree > current.degree) {
696
- current.representative = node
697
- current.degree = degree
698
- }
779
+ current.weight += edgeWeight(edge)
699
780
  continue
700
781
  }
701
782
 
702
- buckets.set(key, {
783
+ edgeByClusterPair.set(key, {
703
784
  id: key,
704
- count: 1,
705
- sumX: node.x,
706
- sumY: node.y,
707
- representative: node,
708
- degree
785
+ sourceCluster,
786
+ targetCluster,
787
+ weight: edgeWeight(edge)
709
788
  })
710
789
  }
711
790
 
712
- return Array.from(buckets.values())
713
- .sort((left, right) => right.count - left.count)
714
- .slice(0, overviewClusterMaxCount)
715
- .map((cluster) => ({
716
- id: cluster.id,
717
- x: cluster.sumX / Math.max(cluster.count, 1),
718
- y: cluster.sumY / Math.max(cluster.count, 1),
719
- count: cluster.count,
720
- representative: cluster.representative
721
- }))
791
+ const aggregatedEdges = Array.from(edgeByClusterPair.values())
792
+ .sort((left, right) => right.weight - left.weight)
793
+ .slice(0, ecosystemClusterEdgeLimit)
794
+
795
+ return { clusters, edges: aggregatedEdges }
722
796
  }
723
797
 
724
- const filterOverviewClustersByViewport = viewport =>
725
- state.overviewClusters.filter((cluster) =>
798
+ const filterEcosystemClustersByViewport = viewport =>
799
+ state.ecosystemClusters.filter((cluster) =>
726
800
  cluster.x >= viewport.minX &&
727
801
  cluster.x <= viewport.maxX &&
728
802
  cluster.y >= viewport.minY &&
729
803
  cluster.y <= viewport.maxY
730
804
  )
731
805
 
806
+ const ecosystemEdgesForClusters = clusters => {
807
+ const clusterIds = new Set(clusters.map(cluster => cluster.id))
808
+ return state.ecosystemEdges.filter(edge =>
809
+ clusterIds.has(edge.sourceCluster.id) &&
810
+ clusterIds.has(edge.targetCluster.id)
811
+ )
812
+ }
813
+
732
814
  const edgeBudgetForCurrentFrame = () => {
733
815
  const zoom = state.transform.scale
734
816
  if (zoom < 0.12) return 380
@@ -763,14 +845,6 @@ const nodeBudgetForScale = (scale) => {
763
845
  return renderNodeBudget
764
846
  }
765
847
 
766
- const massiveLowZoomNodeBudgetForScale = (scale) => {
767
- if (scale < 0.004) return 780
768
- if (scale < 0.01) return 860
769
- if (scale < 0.02) return 900
770
- if (scale < 0.035) return 900
771
- return renderNodeBudget
772
- }
773
-
774
848
  const layerFocusForScale = (scale) => {
775
849
  const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
776
850
  const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
@@ -1675,7 +1749,9 @@ const zoomCapByHubDistance = (distance) => {
1675
1749
 
1676
1750
  const currentZoomMax = () => {
1677
1751
  const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
1678
- const hubDistanceCap = zoomCapByHubDistance(state.hubNeighborDistance)
1752
+ const hubDistanceCap = isDominantHub(state.primaryHub, nodeCount)
1753
+ ? zoomCapByHubDistance(state.hubNeighborDistance)
1754
+ : zoomRange.max
1679
1755
  const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
1680
1756
  const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
1681
1757
  return Math.max(zoomRange.min * 2, capped)
@@ -1776,7 +1852,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
1776
1852
  ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
1777
1853
  : baselineScale
1778
1854
  const hubCenter =
1779
- options.preferHubCenter && state.primaryHub && nodes.some((node) => node.id === state.primaryHub.id)
1855
+ options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
1780
1856
  ? state.primaryHub
1781
1857
  : null
1782
1858
  const centerX = hubCenter ? hubCenter.x : (bounds.minX + bounds.maxX) / 2
@@ -2302,6 +2378,7 @@ const computeRenderVisibility = () => {
2302
2378
  }
2303
2379
  state.lastViewportKey = viewportKey
2304
2380
  state.renderVisibilityDirty = false
2381
+ state.renderClusterEdges = []
2305
2382
 
2306
2383
  const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
2307
2384
 
@@ -2325,12 +2402,27 @@ const computeRenderVisibility = () => {
2325
2402
  state.renderNodes = []
2326
2403
  }
2327
2404
  state.renderEdges = []
2405
+ state.renderClusterEdges = []
2406
+ return
2407
+ }
2408
+
2409
+ if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
2410
+ const viewportClusters = filterEcosystemClustersByViewport(viewport)
2411
+ .sort((left, right) => right.count - left.count)
2412
+ const clusters = viewportClusters.length > 0
2413
+ ? viewportClusters
2414
+ : state.ecosystemClusters
2415
+ state.renderClusters = clusters
2416
+ state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
2417
+ state.renderNodes = []
2418
+ state.renderEdges = []
2328
2419
  return
2329
2420
  }
2330
2421
 
2331
2422
  if (state.visibleNodes.length <= 2000) {
2332
2423
  state.renderNodes = state.visibleNodes
2333
2424
  state.renderClusters = []
2425
+ state.renderClusterEdges = []
2334
2426
  const ids = new Set(state.renderNodes.map((node) => node.id))
2335
2427
  state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
2336
2428
  return
@@ -2338,30 +2430,6 @@ const computeRenderVisibility = () => {
2338
2430
 
2339
2431
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
2340
2432
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2341
- if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
2342
- const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
2343
- const overviewClusters = filterOverviewClustersByViewport(viewport)
2344
- .sort((left, right) => right.count - left.count)
2345
- .slice(0, overviewLimit)
2346
- if (overviewClusters.length > 0) {
2347
- const overviewNodes = representativeNodesFromClusters(
2348
- overviewClusters,
2349
- overviewLimit
2350
- )
2351
- const anchoredNodes = includeHubPreviewNeighborhood(
2352
- overviewNodes,
2353
- Math.min(renderNodeBudget, overviewLimit)
2354
- )
2355
- const enriched = enrichSampleWithNeighbors(anchoredNodes)
2356
- const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
2357
- const previewIds = new Set(previewNodes.map((node) => node.id))
2358
- const previewEdges = collectVisibleEdgesForNodes(previewIds)
2359
- state.renderClusters = []
2360
- state.renderNodes = previewNodes
2361
- state.renderEdges = previewEdges
2362
- return
2363
- }
2364
- }
2365
2433
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
2366
2434
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
2367
2435
  const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
@@ -2407,6 +2475,7 @@ const computeRenderVisibility = () => {
2407
2475
  }
2408
2476
 
2409
2477
  state.renderClusters = []
2478
+ state.renderClusterEdges = []
2410
2479
  state.renderNodes = sampledNodes
2411
2480
  state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
2412
2481
  return
@@ -2416,6 +2485,7 @@ const computeRenderVisibility = () => {
2416
2485
  const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
2417
2486
  const sampledIds = new Set(sampled.map((node) => node.id))
2418
2487
  state.renderClusters = []
2488
+ state.renderClusterEdges = []
2419
2489
  state.renderNodes = sampled
2420
2490
  state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
2421
2491
  return
@@ -2425,11 +2495,13 @@ const computeRenderVisibility = () => {
2425
2495
  const clusters = clusterViewportNodes(viewportNodes)
2426
2496
  if (clusters.length > 0) {
2427
2497
  state.renderClusters = []
2498
+ state.renderClusterEdges = []
2428
2499
  state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
2429
2500
  state.renderEdges = []
2430
2501
  return
2431
2502
  }
2432
2503
  state.renderClusters = []
2504
+ state.renderClusterEdges = []
2433
2505
  const stride = viewportNodeStride()
2434
2506
  const picked = []
2435
2507
 
@@ -2453,6 +2525,7 @@ const computeRenderVisibility = () => {
2453
2525
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2454
2526
  state.renderNodes = fallbackNodes
2455
2527
  state.renderClusters = []
2528
+ state.renderClusterEdges = []
2456
2529
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2457
2530
  return
2458
2531
  }
@@ -2468,6 +2541,7 @@ const computeRenderVisibility = () => {
2468
2541
  const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
2469
2542
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2470
2543
  state.renderClusters = []
2544
+ state.renderClusterEdges = []
2471
2545
  state.renderNodes = fallbackNodes
2472
2546
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2473
2547
  }
@@ -2584,6 +2658,17 @@ const render = now => {
2584
2658
  ctx.translate(state.transform.x, state.transform.y)
2585
2659
  ctx.scale(state.transform.scale, state.transform.scale)
2586
2660
  const safeScale = Math.max(state.transform.scale, 0.0001)
2661
+ if (state.renderClusterEdges.length > 0) {
2662
+ ctx.beginPath()
2663
+ for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
2664
+ const edge = state.renderClusterEdges[index]
2665
+ ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
2666
+ ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
2667
+ }
2668
+ ctx.lineWidth = 1.2 / safeScale
2669
+ ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
2670
+ ctx.stroke()
2671
+ }
2587
2672
  state.renderClusters.forEach(cluster => {
2588
2673
  const isMacro = cluster.id === 'macro-galaxy'
2589
2674
  const radiusPx = isMacro
@@ -2735,27 +2820,8 @@ const selectNodeById = id => {
2735
2820
  }
2736
2821
 
2737
2822
  const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
2738
- const resolveZoomFactor = () => {
2739
- if (state.nodes.length <= massiveGraphNodeThreshold) {
2740
- return factor
2741
- }
2742
-
2743
- const scale = state.transform.scale
2744
- if (factor > 1) {
2745
- if (scale < 0.006) return Math.max(factor, 1.48)
2746
- if (scale < 0.02) return Math.max(factor, 1.34)
2747
- if (scale < 0.08) return Math.max(factor, 1.22)
2748
- return factor
2749
- }
2750
-
2751
- if (scale < 0.006) return Math.min(factor, 0.68)
2752
- if (scale < 0.02) return Math.min(factor, 0.78)
2753
- if (scale < 0.08) return Math.min(factor, 0.86)
2754
- return factor
2755
- }
2756
-
2757
2823
  state.lastManualZoomAt = performance.now()
2758
- const effectiveFactor = resolveZoomFactor()
2824
+ const effectiveFactor = factor
2759
2825
  const nextScale = clampScale(state.transform.scale * effectiveFactor)
2760
2826
  if (nextScale === state.transform.scale) {
2761
2827
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.83",
3
+ "version": "0.1.0-beta.85",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",