@andespindola/brainlink 0.1.0-beta.104 → 0.1.0-beta.106

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
@@ -604,7 +604,7 @@ The graph UI shows:
604
604
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
605
605
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
606
606
  - 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
607
- - 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; for massive graphs the first expansion starts much deeper in zoom, low-size child levels use slower easing, and expansion is additionally gated by focus readiness (screen-space isolation of the focused parent) so child levels open only when that subgraph is truly centered and separated in view
607
+ - graph LOD progression: hierarchical rendering now follows one recursive graph-of-graphs standard whenever a graph has more than one hierarchy level; each level expands through intermediate subgraph sizes (instead of jumping directly to leaves), starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy-like and stress-50k-like structures share the same layered behavior at different depths; layered clusters also receive perspective depth projection (Z-depth) so expansion reads as a true depth field instead of a flat 2D switch; for massive graphs the first expansion starts deeper in zoom and is additionally gated by focus readiness (screen-space isolation of the focused parent) so child levels open only when that subgraph is truly centered and separated in view
608
608
 
609
609
  The server indexes before starting by default. Use `--no-index` to skip that step:
610
610
 
@@ -31,6 +31,10 @@ const massiveEcosystemClusterScaleThreshold = 4.2
31
31
  const ecosystemSubgraphScaleThreshold = 0.18
32
32
  const ecosystemMicroScaleThreshold = 0.08
33
33
  const ecosystemFocusedParentLimit = 2
34
+ const ecosystemDepthNear = 36
35
+ const ecosystemDepthFar = 560
36
+ const ecosystemDepthPerspective = 1400
37
+ const ecosystemDepthOpacityFloor = 0.42
34
38
  const zoomRecoveryGuardMs = 4200
35
39
  const zoomCapTargetViewportShare = 0.72
36
40
  const meshEdgeScaleThreshold = 0.09
@@ -596,7 +600,7 @@ const recomputeVisibility = () => {
596
600
  y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
597
601
  }
598
602
  : { x: 0, y: 0 }
599
- const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
603
+ const ecosystemGraph = nodes.length > 1
600
604
  ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
601
605
  : {
602
606
  clusters: [],
@@ -768,12 +772,29 @@ const ecosystemLayoutSpacingForSize = size => {
768
772
  return 7
769
773
  }
770
774
 
775
+ const buildIntermediateEcosystemSizes = (fromSize, toSize) => {
776
+ if (fromSize <= toSize + 1) {
777
+ return []
778
+ }
779
+ const intermediate = []
780
+ let current = fromSize
781
+ while (current > toSize + 1) {
782
+ const stepped = Math.max(toSize + 1, Math.ceil(current / 3))
783
+ if (stepped >= current) {
784
+ break
785
+ }
786
+ intermediate.push(stepped)
787
+ current = stepped
788
+ }
789
+ return intermediate
790
+ }
791
+
771
792
  const buildEcosystemLevelSizes = nodeCount => {
772
793
  if (nodeCount <= 0) return []
773
- const sizes = []
794
+ const primarySizes = []
774
795
  let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
775
796
  while (currentSize >= 1) {
776
- sizes.push(currentSize)
797
+ primarySizes.push(currentSize)
777
798
  if (currentSize === 1) {
778
799
  break
779
800
  }
@@ -783,7 +804,28 @@ const buildEcosystemLevelSizes = nodeCount => {
783
804
  }
784
805
  currentSize = nextSize
785
806
  }
786
- return sizes
807
+ const expandedSizes = []
808
+ for (let index = 0; index < primarySizes.length; index += 1) {
809
+ const size = primarySizes[index]
810
+ if (expandedSizes.length === 0 || expandedSizes[expandedSizes.length - 1] !== size) {
811
+ expandedSizes.push(size)
812
+ }
813
+ const nextSize = primarySizes[index + 1]
814
+ if (!Number.isFinite(nextSize)) {
815
+ continue
816
+ }
817
+ const intermediate = buildIntermediateEcosystemSizes(size, nextSize)
818
+ for (let intermediateIndex = 0; intermediateIndex < intermediate.length; intermediateIndex += 1) {
819
+ const candidate = intermediate[intermediateIndex]
820
+ if (expandedSizes[expandedSizes.length - 1] !== candidate) {
821
+ expandedSizes.push(candidate)
822
+ }
823
+ }
824
+ }
825
+ if (expandedSizes[expandedSizes.length - 1] !== 1) {
826
+ expandedSizes.push(1)
827
+ }
828
+ return expandedSizes
787
829
  }
788
830
 
789
831
  const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
@@ -794,12 +836,12 @@ const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
794
836
  const maxScale = isMassive
795
837
  ? massiveEcosystemClusterScaleThreshold
796
838
  : ecosystemClusterScaleThreshold
797
- const startScale = isMassive ? 0.82 : 0.18
839
+ const startScale = isMassive ? 1.12 : 0.24
798
840
  const transitionCount = levelSizes.length - 1
799
841
  const usableScale = Math.max(0.08, maxScale - startScale)
800
842
  const step = usableScale / transitionCount
801
- const stride = isMassive ? 0.9 : 0.78
802
- const overlap = isMassive ? 1.28 : 1.75
843
+ const stride = isMassive ? 0.93 : 0.82
844
+ const overlap = isMassive ? 1.22 : 1.62
803
845
  const levels = []
804
846
  for (let index = 0; index < transitionCount; index += 1) {
805
847
  const start = startScale + step * index * stride
@@ -1115,6 +1157,81 @@ const selectHierarchicalEcosystemClusters = viewport => {
1115
1157
  return [...hubClusters, ...visibleClusters]
1116
1158
  }
1117
1159
 
1160
+ const ecosystemLevelIndexBySize = () => {
1161
+ const indexBySize = new Map()
1162
+ for (let index = 0; index < state.ecosystemLevelSizes.length; index += 1) {
1163
+ indexBySize.set(state.ecosystemLevelSizes[index], index)
1164
+ }
1165
+ return indexBySize
1166
+ }
1167
+
1168
+ const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1169
+ if (cluster.isHub) {
1170
+ return ecosystemDepthNear
1171
+ }
1172
+ const maxLevelIndex = Math.max(state.ecosystemLevelSizes.length - 1, 0)
1173
+ const levelIndex = levelIndexMap.get(cluster.size) ?? 0
1174
+ const reverseIndex = Math.max(0, maxLevelIndex - levelIndex)
1175
+ const normalized = maxLevelIndex === 0 ? 0 : reverseIndex / maxLevelIndex
1176
+ return ecosystemDepthNear + normalized * (ecosystemDepthFar - ecosystemDepthNear)
1177
+ }
1178
+
1179
+ const projectEcosystemPoint = (x, y, depth, anchor) => {
1180
+ const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + Math.max(0, depth))
1181
+ return {
1182
+ x: anchor.x + (x - anchor.x) * factor,
1183
+ y: anchor.y + (y - anchor.y) * factor,
1184
+ factor
1185
+ }
1186
+ }
1187
+
1188
+ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1189
+ const levelIndexMap = ecosystemLevelIndexBySize()
1190
+ const projectedClusters = []
1191
+ const clusterById = new Map()
1192
+
1193
+ for (let index = 0; index < clusters.length; index += 1) {
1194
+ const cluster = clusters[index]
1195
+ const depth = ecosystemDepthForCluster(cluster, levelIndexMap)
1196
+ const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
1197
+ const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
1198
+ const depthOpacity = Math.max(
1199
+ ecosystemDepthOpacityFloor,
1200
+ Math.min(1, 0.58 + projected.factor * 0.62)
1201
+ )
1202
+ const projectedCluster = {
1203
+ ...cluster,
1204
+ x: projected.x,
1205
+ y: projected.y,
1206
+ lodOpacity: baseOpacity * depthOpacity,
1207
+ depth,
1208
+ depthScale: projected.factor
1209
+ }
1210
+ projectedClusters.push(projectedCluster)
1211
+ clusterById.set(projectedCluster.id, projectedCluster)
1212
+ }
1213
+
1214
+ const projectedEdges = edges
1215
+ .map((edge) => {
1216
+ const sourceCluster = clusterById.get(edge.sourceCluster.id)
1217
+ const targetCluster = clusterById.get(edge.targetCluster.id)
1218
+ if (!sourceCluster || !targetCluster) {
1219
+ return null
1220
+ }
1221
+ return {
1222
+ ...edge,
1223
+ sourceCluster,
1224
+ targetCluster
1225
+ }
1226
+ })
1227
+ .filter(Boolean)
1228
+
1229
+ return {
1230
+ clusters: projectedClusters,
1231
+ edges: projectedEdges
1232
+ }
1233
+ }
1234
+
1118
1235
  const ecosystemSiblingEdgesForClusters = (clusters, existingEdges) => {
1119
1236
  const byParent = new Map()
1120
1237
  for (let index = 0; index < clusters.length; index += 1) {
@@ -2813,7 +2930,9 @@ const clusterRadiusPx = cluster => {
2813
2930
  const size = Math.max(1, Math.min(ecosystemLevelNodeCap, cluster.size || cluster.count || 1))
2814
2931
  const sizeBias = 0.56 + Math.log10(size + 1) * 0.28
2815
2932
  const densityBias = Math.log10((cluster.count || 1) + 1) * 0.12
2816
- return Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
2933
+ const radius = Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
2934
+ const depthScale = Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
2935
+ return Math.max(0.56, Math.min(3.2, radius * depthScale))
2817
2936
  }
2818
2937
  return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2819
2938
  }
@@ -2985,14 +3104,17 @@ const computeRenderVisibility = () => {
2985
3104
  ? massiveEcosystemClusterScaleThreshold
2986
3105
  : ecosystemClusterScaleThreshold
2987
3106
  if (
2988
- state.visibleNodes.length > ecosystemActivationNodeThreshold &&
3107
+ state.ecosystemExpansionLevels.length > 0 &&
2989
3108
  state.transform.scale <= ecosystemScaleThreshold &&
2990
3109
  state.ecosystemClusters.length > 0
2991
3110
  ) {
2992
3111
  const clusters = selectHierarchicalEcosystemClusters(viewport)
2993
3112
  .sort((left, right) => right.count - left.count)
2994
- state.renderClusters = clusters
2995
- state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
3113
+ const edges = ecosystemEdgesForClusters(clusters)
3114
+ const projectionAnchor = ecosystemFocusPoint()
3115
+ const projected = applyEcosystemDepthProjection(clusters, edges, projectionAnchor)
3116
+ state.renderClusters = projected.clusters
3117
+ state.renderClusterEdges = projected.edges
2996
3118
  state.renderNodes = []
2997
3119
  state.renderEdges = []
2998
3120
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.104",
3
+ "version": "0.1.0-beta.106",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",