@andespindola/brainlink 0.1.0-beta.96 → 0.1.0-beta.97

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
@@ -602,7 +602,7 @@ The graph UI shows:
602
602
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
603
603
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
604
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: graphs up to 1000 notes render directly; larger graphs use a compact memory-hub-centered mesh where each 1000-note point already carries a faint connected 500-note micro-structure, then zoom-in keeps focused child clusters latent at the parent position before fading and separating them through 250-note, 125-note, 60-note, 30-note, 15-note and 8-note subgraphs with aggregated real links plus local sibling mesh links, and in massive graphs keeps this subgraph mode active much longer with finer wheel steps so deep zoom does not abruptly switch to a broad sampled node cloud
605
+ - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use one recursive model where each visible level targets up to 999 non-hub nodes, starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (again up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy and stress-50k follow the same structure at different depths
606
606
 
607
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
608
608
 
@@ -21,9 +21,8 @@ const viewportPaddingPx = 280
21
21
  const worldCoordinateLimit = 5_000_000
22
22
  const transformCoordinateLimit = 20_000_000
23
23
  const hoverHitTestIntervalMs = 64
24
- const ecosystemGroupSize = 1000
24
+ const ecosystemLevelNodeCap = 999
25
25
  const ecosystemActivationNodeThreshold = 1000
26
- const ecosystemGroupSizes = [1000, 500, 250, 125, 60, 30, 15, 8]
27
26
  const ecosystemClusterEdgeLimit = 520
28
27
  const ecosystemHubEdgeLimit = 120
29
28
  const ecosystemSiblingEdgeLimit = 180
@@ -32,15 +31,6 @@ const massiveEcosystemClusterScaleThreshold = 4.2
32
31
  const ecosystemSubgraphScaleThreshold = 0.18
33
32
  const ecosystemMicroScaleThreshold = 0.08
34
33
  const ecosystemFocusedParentLimit = 2
35
- const ecosystemExpansionLevels = [
36
- { parentSize: 1000, childSize: 500, start: 0.04, end: 0.62 },
37
- { parentSize: 500, childSize: 250, start: 0.16, end: 0.72 },
38
- { parentSize: 250, childSize: 125, start: 0.3, end: 0.78 },
39
- { parentSize: 125, childSize: 60, start: 0.46, end: 0.86 },
40
- { parentSize: 60, childSize: 30, start: 0.64, end: 1.06 },
41
- { parentSize: 30, childSize: 15, start: 0.84, end: 1.38 },
42
- { parentSize: 15, childSize: 8, start: 1.08, end: 1.82 }
43
- ]
44
34
  const zoomRecoveryGuardMs = 4200
45
35
  const zoomCapTargetViewportShare = 0.72
46
36
  const meshEdgeScaleThreshold = 0.09
@@ -87,6 +77,9 @@ const state = {
87
77
  ecosystemClusters: [],
88
78
  ecosystemClustersBySize: new Map(),
89
79
  ecosystemNodeClusterBySize: new Map(),
80
+ ecosystemLevelSizes: [],
81
+ ecosystemExpansionLevels: [],
82
+ ecosystemBaseSize: ecosystemLevelNodeCap,
90
83
  ecosystemHubCluster: null,
91
84
  macroCenter: { x: 0, y: 0 },
92
85
  macroRepresentative: null,
@@ -597,10 +590,21 @@ const recomputeVisibility = () => {
597
590
  : { x: 0, y: 0 }
598
591
  const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
599
592
  ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
600
- : { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
593
+ : {
594
+ clusters: [],
595
+ clustersBySize: new Map(),
596
+ nodeClusterBySize: new Map(),
597
+ levelSizes: [],
598
+ expansionLevels: [],
599
+ baseSize: ecosystemLevelNodeCap,
600
+ hubCluster: null
601
+ }
601
602
  state.ecosystemClusters = ecosystemGraph.clusters
602
603
  state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
603
604
  state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
605
+ state.ecosystemLevelSizes = ecosystemGraph.levelSizes
606
+ state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
607
+ state.ecosystemBaseSize = ecosystemGraph.baseSize
604
608
  state.ecosystemHubCluster = ecosystemGraph.hubCluster
605
609
  state.macroRepresentative = resolveMacroRepresentative(nodes)
606
610
  markRenderDirty()
@@ -747,14 +751,56 @@ const selectEcosystemRepresentative = nodes => {
747
751
  }
748
752
 
749
753
  const ecosystemLayoutSpacingForSize = size => {
750
- if (size >= 1000) return 260
751
- if (size >= 500) return 92
752
- if (size >= 250) return 52
753
- if (size >= 125) return 28
754
- if (size >= 60) return 16
755
- if (size >= 30) return 11
756
- if (size >= 15) return 7
757
- return 5
754
+ if (size >= ecosystemLevelNodeCap) return 260
755
+ if (size >= 320) return 110
756
+ if (size >= 120) return 64
757
+ if (size >= 48) return 34
758
+ if (size >= 18) return 18
759
+ if (size >= 8) return 11
760
+ return 7
761
+ }
762
+
763
+ const buildEcosystemLevelSizes = nodeCount => {
764
+ if (nodeCount <= 0) return []
765
+ const sizes = []
766
+ let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
767
+ while (currentSize >= 1) {
768
+ sizes.push(currentSize)
769
+ if (currentSize === 1) {
770
+ break
771
+ }
772
+ const nextSize = Math.max(1, Math.ceil(currentSize / ecosystemLevelNodeCap))
773
+ if (nextSize === currentSize) {
774
+ break
775
+ }
776
+ currentSize = nextSize
777
+ }
778
+ return sizes
779
+ }
780
+
781
+ const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
782
+ if (levelSizes.length <= 1) {
783
+ return []
784
+ }
785
+ const maxScale = nodeCount > massiveGraphNodeThreshold
786
+ ? massiveEcosystemClusterScaleThreshold
787
+ : ecosystemClusterScaleThreshold
788
+ const startScale = 0.04
789
+ const transitionCount = levelSizes.length - 1
790
+ const usableScale = Math.max(0.08, maxScale - startScale)
791
+ const step = usableScale / transitionCount
792
+ const levels = []
793
+ for (let index = 0; index < transitionCount; index += 1) {
794
+ const start = startScale + step * index * 0.72
795
+ const end = Math.min(maxScale, start + step * 1.85)
796
+ levels.push({
797
+ parentSize: levelSizes[index],
798
+ childSize: levelSizes[index + 1],
799
+ start,
800
+ end
801
+ })
802
+ }
803
+ return levels
758
804
  }
759
805
 
760
806
  const ecosystemCompactPoint = (index, total, center, spacing) => {
@@ -837,19 +883,30 @@ const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
837
883
 
838
884
  const buildEcosystemGraph = (nodes, center, hub) => {
839
885
  if (nodes.length === 0) {
840
- return { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
886
+ return {
887
+ clusters: [],
888
+ clustersBySize: new Map(),
889
+ nodeClusterBySize: new Map(),
890
+ levelSizes: [],
891
+ expansionLevels: [],
892
+ baseSize: ecosystemLevelNodeCap,
893
+ hubCluster: null
894
+ }
841
895
  }
842
896
 
843
897
  const hubCluster = buildEcosystemHubCluster(hub, center)
844
898
  const sortedNodes = nodes
845
899
  .filter(node => node.id !== hub?.id)
846
900
  .sort(compareNodesForEcosystem)
901
+ const levelSizes = buildEcosystemLevelSizes(sortedNodes.length)
902
+ const expansionLevels = buildEcosystemExpansionLevels(levelSizes, nodes.length)
903
+ const baseSize = levelSizes[0] ?? ecosystemLevelNodeCap
847
904
  const clustersBySize = new Map()
848
905
  const nodeClusterBySize = new Map()
849
906
  let parentLookup = null
850
907
 
851
- for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
852
- const size = ecosystemGroupSizes[index]
908
+ for (let index = 0; index < levelSizes.length; index += 1) {
909
+ const size = levelSizes[index]
853
910
  const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
854
911
  clustersBySize.set(size, level.clusters)
855
912
  nodeClusterBySize.set(size, level.clusterByNodeId)
@@ -857,9 +914,12 @@ const buildEcosystemGraph = (nodes, center, hub) => {
857
914
  }
858
915
 
859
916
  return {
860
- clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
917
+ clusters: clustersBySize.get(baseSize) ?? [],
861
918
  clustersBySize,
862
919
  nodeClusterBySize,
920
+ levelSizes,
921
+ expansionLevels,
922
+ baseSize,
863
923
  hubCluster
864
924
  }
865
925
  }
@@ -903,7 +963,7 @@ const zoomProgress = (scale, start, end) =>
903
963
 
904
964
  const semanticZoomSpread = (progress, childSize) => {
905
965
  const curve = Math.pow(progress, 4.2)
906
- if (childSize >= 500) {
966
+ if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
907
967
  return 0.12 + curve * 0.88
908
968
  }
909
969
  return curve
@@ -911,7 +971,7 @@ const semanticZoomSpread = (progress, childSize) => {
911
971
 
912
972
  const opacityForProgress = (progress, childSize) => {
913
973
  const eased = Math.pow(progress, 2.1)
914
- if (childSize >= 500) {
974
+ if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
915
975
  return 0.22 + eased * 0.78
916
976
  }
917
977
  return eased
@@ -953,13 +1013,13 @@ const spreadChildClusterFromParent = (cluster, childSize, progress, spread) => {
953
1013
  }
954
1014
 
955
1015
  const selectHierarchicalEcosystemClusters = viewport => {
956
- const baseClusters = state.ecosystemClustersBySize.get(ecosystemGroupSize) ?? state.ecosystemClusters
1016
+ const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
957
1017
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
958
1018
  const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
959
1019
  const visibleClusters = [...visibleBaseClusters]
960
1020
 
961
- for (let index = 0; index < ecosystemExpansionLevels.length; index += 1) {
962
- const level = ecosystemExpansionLevels[index]
1021
+ for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
1022
+ const level = state.ecosystemExpansionLevels[index]
963
1023
  const parentClusters = visibleClusters.filter(cluster => cluster.size === level.parentSize)
964
1024
  if (parentClusters.length === 0) {
965
1025
  continue
@@ -2550,22 +2610,10 @@ const clusterRadiusPx = cluster => {
2550
2610
  return 3.8
2551
2611
  }
2552
2612
  if (String(cluster.id).startsWith('ecosystem-')) {
2553
- const base = cluster.size >= 1000
2554
- ? 0.96
2555
- : cluster.size >= 500
2556
- ? 0.9
2557
- : cluster.size >= 250
2558
- ? 0.82
2559
- : cluster.size >= 125
2560
- ? 0.74
2561
- : cluster.size >= 60
2562
- ? 0.66
2563
- : cluster.size >= 30
2564
- ? 0.62
2565
- : cluster.size >= 15
2566
- ? 0.74
2567
- : 0.9
2568
- return Math.max(0.62, Math.min(2.2, base + Math.log10(cluster.count + 1) * 0.14))
2613
+ const size = Math.max(1, Math.min(ecosystemLevelNodeCap, cluster.size || cluster.count || 1))
2614
+ const sizeBias = 0.56 + Math.log10(size + 1) * 0.28
2615
+ const densityBias = Math.log10((cluster.count || 1) + 1) * 0.12
2616
+ return Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
2569
2617
  }
2570
2618
  return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2571
2619
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.96",
3
+ "version": "0.1.0-beta.97",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",