@andespindola/brainlink 0.1.0-beta.90 → 0.1.0-beta.92

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 of connected 1000-note points, zoom-in spreads only focused clusters into 250-note and 60-note subgraphs with aggregated real links plus local sibling mesh links, keeps parent points during expansion to avoid visual jumps, then progressively raises the focused node budget so local areas keep nearby notes and links visible
605
+ - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use a compact memory-hub-centered mesh of small connected 1000-note points, zoom-in delays expansion until the focused cluster is visually close, then continuously spreads and fades only focused clusters through 500-note, 250-note, 125-note and 60-note subgraphs with aggregated real links plus local sibling mesh links, keeps parent points during expansion to avoid visual jumps, then progressively raises the focused node budget so local areas keep nearby notes and links visible
606
606
 
607
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
608
608
 
@@ -23,14 +23,20 @@ const transformCoordinateLimit = 20_000_000
23
23
  const hoverHitTestIntervalMs = 64
24
24
  const ecosystemGroupSize = 1000
25
25
  const ecosystemActivationNodeThreshold = 1000
26
- const ecosystemGroupSizes = [1000, 250, 60]
26
+ const ecosystemGroupSizes = [1000, 500, 250, 125, 60]
27
27
  const ecosystemClusterEdgeLimit = 520
28
28
  const ecosystemHubEdgeLimit = 120
29
29
  const ecosystemSiblingEdgeLimit = 180
30
- const ecosystemClusterScaleThreshold = 0.32
30
+ const ecosystemClusterScaleThreshold = 0.78
31
31
  const ecosystemSubgraphScaleThreshold = 0.18
32
32
  const ecosystemMicroScaleThreshold = 0.08
33
- const ecosystemFocusedParentLimit = 3
33
+ const ecosystemFocusedParentLimit = 2
34
+ const ecosystemExpansionLevels = [
35
+ { parentSize: 1000, childSize: 500, start: 0.16, end: 0.34 },
36
+ { parentSize: 500, childSize: 250, start: 0.3, end: 0.5 },
37
+ { parentSize: 250, childSize: 125, start: 0.46, end: 0.66 },
38
+ { parentSize: 125, childSize: 60, start: 0.62, end: 0.78 }
39
+ ]
34
40
  const zoomRecoveryGuardMs = 4200
35
41
  const zoomCapTargetViewportShare = 0.72
36
42
  const meshEdgeScaleThreshold = 0.09
@@ -737,9 +743,11 @@ const selectEcosystemRepresentative = nodes => {
737
743
  }
738
744
 
739
745
  const ecosystemLayoutSpacingForSize = size => {
740
- if (size >= 1000) return 360
741
- if (size >= 250) return 92
742
- return 28
746
+ if (size >= 1000) return 260
747
+ if (size >= 500) return 92
748
+ if (size >= 250) return 52
749
+ if (size >= 125) return 28
750
+ return 16
743
751
  }
744
752
 
745
753
  const ecosystemCompactPoint = (index, total, center, spacing) => {
@@ -886,7 +894,9 @@ const smoothStep = value => {
886
894
  const zoomProgress = (scale, start, end) =>
887
895
  smoothStep((scale - start) / Math.max(end - start, 0.0001))
888
896
 
889
- const shouldReplaceParentCluster = spread => spread >= 0.96
897
+ const semanticZoomSpread = progress => Math.pow(progress, 2.6)
898
+
899
+ const opacityForSpread = spread => 0.03 + smoothStep(spread) * 0.97
890
900
 
891
901
  const expandFocusedClusters = (parentClusters, childSize, spread, viewport) => {
892
902
  const focusPoint = ecosystemFocusPoint()
@@ -909,13 +919,17 @@ const expandFocusedClusters = (parentClusters, childSize, spread, viewport) => {
909
919
 
910
920
  const spreadChildClusterFromParent = (cluster, spread) => {
911
921
  if (!Number.isFinite(cluster.parentX) || !Number.isFinite(cluster.parentY)) {
912
- return cluster
922
+ return {
923
+ ...cluster,
924
+ lodOpacity: opacityForSpread(spread)
925
+ }
913
926
  }
914
927
 
915
928
  return {
916
929
  ...cluster,
917
930
  x: cluster.parentX + (cluster.x - cluster.parentX) * spread,
918
- y: cluster.parentY + (cluster.y - cluster.parentY) * spread
931
+ y: cluster.parentY + (cluster.y - cluster.parentY) * spread,
932
+ lodOpacity: opacityForSpread(spread)
919
933
  }
920
934
  }
921
935
 
@@ -923,61 +937,24 @@ const selectHierarchicalEcosystemClusters = viewport => {
923
937
  const baseClusters = state.ecosystemClustersBySize.get(ecosystemGroupSize) ?? state.ecosystemClusters
924
938
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
925
939
  const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
940
+ const visibleClusters = [...visibleBaseClusters]
926
941
 
927
- if (state.transform.scale <= ecosystemMicroScaleThreshold) {
928
- return [...hubClusters, ...visibleBaseClusters]
929
- }
930
-
931
- const midExpansion = expandFocusedClusters(
932
- visibleBaseClusters,
933
- 250,
934
- state.transform.scale <= ecosystemSubgraphScaleThreshold
935
- ? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
936
- : 1,
937
- viewport
938
- )
939
- if (midExpansion.childClusters.length === 0) {
940
- return [...hubClusters, ...visibleBaseClusters]
941
- }
942
-
943
- const midSpread = state.transform.scale <= ecosystemSubgraphScaleThreshold
944
- ? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
945
- : 1
946
- const remainingBaseClusters = shouldReplaceParentCluster(midSpread)
947
- ? visibleBaseClusters.filter(cluster => !midExpansion.expandedParentIds.has(cluster.id))
948
- : visibleBaseClusters
949
- if (state.transform.scale <= ecosystemSubgraphScaleThreshold) {
950
- return [
951
- ...hubClusters,
952
- ...remainingBaseClusters,
953
- ...midExpansion.childClusters
954
- ]
955
- }
956
-
957
- const microExpansion = expandFocusedClusters(
958
- midExpansion.childClusters,
959
- 60,
960
- zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold),
961
- viewport
962
- )
963
- if (microExpansion.childClusters.length === 0) {
964
- return [
965
- ...hubClusters,
966
- ...remainingBaseClusters,
967
- ...midExpansion.childClusters
968
- ]
942
+ for (let index = 0; index < ecosystemExpansionLevels.length; index += 1) {
943
+ const level = ecosystemExpansionLevels[index]
944
+ const parentClusters = visibleClusters.filter(cluster => cluster.size === level.parentSize)
945
+ if (parentClusters.length === 0) {
946
+ continue
947
+ }
948
+ const progress = zoomProgress(state.transform.scale, level.start, level.end)
949
+ if (progress <= 0.025) {
950
+ continue
951
+ }
952
+ const spread = semanticZoomSpread(progress)
953
+ const expansion = expandFocusedClusters(parentClusters, level.childSize, spread, viewport)
954
+ visibleClusters.push(...expansion.childClusters)
969
955
  }
970
956
 
971
- const microSpread = zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold)
972
- const visibleMidClusters = shouldReplaceParentCluster(microSpread)
973
- ? midExpansion.childClusters.filter(cluster => !microExpansion.expandedParentIds.has(cluster.id))
974
- : midExpansion.childClusters
975
- return [
976
- ...hubClusters,
977
- ...remainingBaseClusters,
978
- ...visibleMidClusters,
979
- ...microExpansion.childClusters
980
- ]
957
+ return [...hubClusters, ...visibleClusters]
981
958
  }
982
959
 
983
960
  const ecosystemSiblingEdgesForClusters = (clusters, existingEdges) => {
@@ -2553,15 +2530,18 @@ const clusterRadiusPx = cluster => {
2553
2530
  return 10
2554
2531
  }
2555
2532
  if (cluster.isHub) {
2556
- return 5.2
2533
+ return 4.2
2557
2534
  }
2558
2535
  if (String(cluster.id).startsWith('ecosystem-')) {
2559
- const base = cluster.size >= 1000 ? 2.4 : cluster.size >= 250 ? 2.1 : 1.8
2560
- return Math.max(1.8, Math.min(4.2, base + Math.log10(cluster.count + 1) * 0.28))
2536
+ const base = cluster.size >= 1000 ? 1.2 : cluster.size >= 500 ? 1.1 : cluster.size >= 250 ? 1 : cluster.size >= 125 ? 0.92 : 0.86
2537
+ return Math.max(0.85, Math.min(2.45, base + Math.log10(cluster.count + 1) * 0.18))
2561
2538
  }
2562
2539
  return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2563
2540
  }
2564
2541
 
2542
+ const clusterOpacity = cluster =>
2543
+ Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
2544
+
2565
2545
  const worldViewportBounds = () => {
2566
2546
  const rect = canvas.getBoundingClientRect()
2567
2547
  const width = Math.max(rect.width, 320)
@@ -2977,23 +2957,32 @@ const render = now => {
2977
2957
  ctx.scale(state.transform.scale, state.transform.scale)
2978
2958
  const safeScale = Math.max(state.transform.scale, 0.0001)
2979
2959
  if (state.renderClusterEdges.length > 0) {
2980
- ctx.beginPath()
2981
2960
  for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
2982
2961
  const edge = state.renderClusterEdges[index]
2962
+ const edgeOpacity = Math.min(clusterOpacity(edge.sourceCluster), clusterOpacity(edge.targetCluster))
2963
+ if (edgeOpacity <= 0.01) {
2964
+ continue
2965
+ }
2966
+ ctx.beginPath()
2983
2967
  ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
2984
2968
  ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
2969
+ ctx.lineWidth = 1.2 / safeScale
2970
+ ctx.strokeStyle = 'rgba(153, 165, 181, ' + (edge.inferred ? 0.14 : 0.22) * edgeOpacity + ')'
2971
+ ctx.stroke()
2985
2972
  }
2986
- ctx.lineWidth = 1.2 / safeScale
2987
- ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
2988
- ctx.stroke()
2989
2973
  }
2990
2974
  state.renderClusters.forEach(cluster => {
2991
2975
  const isMacro = cluster.id === 'macro-galaxy'
2992
2976
  const isEcosystem = String(cluster.id).startsWith('ecosystem-')
2993
2977
  const isHub = Boolean(cluster.isHub)
2978
+ const opacity = clusterOpacity(cluster)
2979
+ if (opacity <= 0.01) {
2980
+ return
2981
+ }
2994
2982
  const radiusPx = clusterRadiusPx(cluster)
2995
2983
  const radius = radiusPx / safeScale
2996
2984
  const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
2985
+ ctx.globalAlpha = opacity
2997
2986
  if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
2998
2987
  ctx.beginPath()
2999
2988
  ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
@@ -3014,6 +3003,7 @@ const render = now => {
3014
3003
  ctx.textBaseline = 'top'
3015
3004
  ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
3016
3005
  }
3006
+ ctx.globalAlpha = 1
3017
3007
  // Keep cluster markers minimal and faster to draw on large graphs.
3018
3008
  })
3019
3009
  ctx.restore()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.90",
3
+ "version": "0.1.0-beta.92",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",