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

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
@@ -601,10 +601,12 @@ The graph UI shows:
601
601
  - floating graph totals (notes, links, tags) below the Brainlink title
602
602
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
603
603
  - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
604
+ - hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
605
+ - hierarchical edge projection now caches hub membership and node-to-cluster resolution per frame to keep large recursive subgraph rendering smooth during continuous zoom and pan
604
606
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
605
607
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
606
608
  - 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: 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
609
+ - 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 stronger perspective depth projection (Z-depth) with vertical camera tilt/parallax 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
610
 
609
611
  The server indexes before starting by default. Use `--no-index` to skip that step:
610
612
 
@@ -31,10 +31,12 @@ 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
+ const ecosystemDepthNear = 80
35
+ const ecosystemDepthFar = 2600
36
+ const ecosystemDepthPerspective = 620
37
+ const ecosystemDepthTiltY = 0.24
38
+ const ecosystemDepthMinScale = 0.24
39
+ const ecosystemDepthOpacityFloor = 0.2
38
40
  const zoomRecoveryGuardMs = 4200
39
41
  const zoomCapTargetViewportShare = 0.72
40
42
  const meshEdgeScaleThreshold = 0.09
@@ -89,6 +91,8 @@ const state = {
89
91
  ecosystemClustersBySize: new Map(),
90
92
  ecosystemNodeClusterBySize: new Map(),
91
93
  ecosystemLevelSizes: [],
94
+ ecosystemLevelIndexBySize: new Map(),
95
+ ecosystemHubNodeIds: new Set(),
92
96
  ecosystemExpansionLevels: [],
93
97
  ecosystemBaseSize: ecosystemLevelNodeCap,
94
98
  ecosystemHubCluster: null,
@@ -615,6 +619,11 @@ const recomputeVisibility = () => {
615
619
  state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
616
620
  state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
617
621
  state.ecosystemLevelSizes = ecosystemGraph.levelSizes
622
+ state.ecosystemLevelIndexBySize = ecosystemGraph.levelSizes.reduce((map, size, index) => {
623
+ map.set(size, index)
624
+ return map
625
+ }, new Map())
626
+ state.ecosystemHubNodeIds = new Set(ecosystemGraph.hubCluster?.nodeIds ?? [])
618
627
  state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
619
628
  state.ecosystemBaseSize = ecosystemGraph.baseSize
620
629
  state.ecosystemHubCluster = ecosystemGraph.hubCluster
@@ -1103,10 +1112,18 @@ const expandFocusedClusters = (parentClusters, focusPoint, childSize, progress,
1103
1112
  ecosystemFocusedParentLimit
1104
1113
  ))
1105
1114
  const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
1106
- const visibleChildClusters = childClusters
1107
- .filter(cluster => expandedParentIds.has(cluster.parentId))
1108
- .map(cluster => spreadChildClusterFromParent(cluster, childSize, progress, spread))
1109
- .filter(cluster => isClusterInViewport(cluster, viewport))
1115
+ const visibleChildClusters = []
1116
+ for (let index = 0; index < childClusters.length; index += 1) {
1117
+ const cluster = childClusters[index]
1118
+ if (!expandedParentIds.has(cluster.parentId)) {
1119
+ continue
1120
+ }
1121
+ const spreadCluster = spreadChildClusterFromParent(cluster, childSize, progress, spread)
1122
+ if (!isClusterInViewport(spreadCluster, viewport)) {
1123
+ continue
1124
+ }
1125
+ visibleChildClusters.push(spreadCluster)
1126
+ }
1110
1127
 
1111
1128
  return {
1112
1129
  expandedParentIds,
@@ -1135,11 +1152,21 @@ const selectHierarchicalEcosystemClusters = viewport => {
1135
1152
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
1136
1153
  const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
1137
1154
  const visibleClusters = [...visibleBaseClusters]
1155
+ const clustersBySize = new Map()
1156
+ for (let index = 0; index < visibleBaseClusters.length; index += 1) {
1157
+ const cluster = visibleBaseClusters[index]
1158
+ const levelClusters = clustersBySize.get(cluster.size)
1159
+ if (levelClusters) {
1160
+ levelClusters.push(cluster)
1161
+ } else {
1162
+ clustersBySize.set(cluster.size, [cluster])
1163
+ }
1164
+ }
1138
1165
  const focusPoint = ecosystemFocusPoint()
1139
1166
 
1140
1167
  for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
1141
1168
  const level = state.ecosystemExpansionLevels[index]
1142
- const parentClusters = visibleClusters.filter(cluster => cluster.size === level.parentSize)
1169
+ const parentClusters = clustersBySize.get(level.parentSize) ?? []
1143
1170
  if (parentClusters.length === 0) {
1144
1171
  continue
1145
1172
  }
@@ -1152,18 +1179,20 @@ const selectHierarchicalEcosystemClusters = viewport => {
1152
1179
  const spread = semanticZoomSpread(progress, level.childSize)
1153
1180
  const expansion = expandFocusedClusters(parentClusters, focusPoint, level.childSize, progress, spread, viewport)
1154
1181
  visibleClusters.push(...expansion.childClusters)
1182
+ if (expansion.childClusters.length > 0) {
1183
+ const levelClusters = clustersBySize.get(level.childSize)
1184
+ if (levelClusters) {
1185
+ levelClusters.push(...expansion.childClusters)
1186
+ } else {
1187
+ clustersBySize.set(level.childSize, [...expansion.childClusters])
1188
+ }
1189
+ }
1155
1190
  }
1156
1191
 
1157
1192
  return [...hubClusters, ...visibleClusters]
1158
1193
  }
1159
1194
 
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
- }
1195
+ const ecosystemLevelIndexBySize = () => state.ecosystemLevelIndexBySize
1167
1196
 
1168
1197
  const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1169
1198
  if (cluster.isHub) {
@@ -1177,10 +1206,12 @@ const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1177
1206
  }
1178
1207
 
1179
1208
  const projectEcosystemPoint = (x, y, depth, anchor) => {
1180
- const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + Math.max(0, depth))
1209
+ const safeDepth = Math.max(0, depth)
1210
+ const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + safeDepth)
1211
+ const verticalTilt = safeDepth * ecosystemDepthTiltY
1181
1212
  return {
1182
1213
  x: anchor.x + (x - anchor.x) * factor,
1183
- y: anchor.y + (y - anchor.y) * factor,
1214
+ y: anchor.y + (y - anchor.y) * factor - verticalTilt,
1184
1215
  factor
1185
1216
  }
1186
1217
  }
@@ -1195,9 +1226,10 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1195
1226
  const depth = ecosystemDepthForCluster(cluster, levelIndexMap)
1196
1227
  const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
1197
1228
  const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
1229
+ const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
1198
1230
  const depthOpacity = Math.max(
1199
1231
  ecosystemDepthOpacityFloor,
1200
- Math.min(1, 0.58 + projected.factor * 0.62)
1232
+ Math.min(1, depthScale * 1.08)
1201
1233
  )
1202
1234
  const projectedCluster = {
1203
1235
  ...cluster,
@@ -1205,26 +1237,26 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1205
1237
  y: projected.y,
1206
1238
  lodOpacity: baseOpacity * depthOpacity,
1207
1239
  depth,
1208
- depthScale: projected.factor
1240
+ depthScale
1209
1241
  }
1210
1242
  projectedClusters.push(projectedCluster)
1211
1243
  clusterById.set(projectedCluster.id, projectedCluster)
1212
1244
  }
1213
1245
 
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
- }
1246
+ const projectedEdges = []
1247
+ for (let index = 0; index < edges.length; index += 1) {
1248
+ const edge = edges[index]
1249
+ const sourceCluster = clusterById.get(edge.sourceCluster.id)
1250
+ const targetCluster = clusterById.get(edge.targetCluster.id)
1251
+ if (!sourceCluster || !targetCluster) {
1252
+ continue
1253
+ }
1254
+ projectedEdges.push({
1255
+ ...edge,
1256
+ sourceCluster,
1257
+ targetCluster
1226
1258
  })
1227
- .filter(Boolean)
1259
+ }
1228
1260
 
1229
1261
  return {
1230
1262
  clusters: projectedClusters,
@@ -1284,28 +1316,37 @@ const ecosystemEdgesForClusters = clusters => {
1284
1316
  const clusterById = new Map(edgeClusters.map(cluster => [cluster.id, cluster]))
1285
1317
  const clusterIds = new Set(clusterById.keys())
1286
1318
  const levelsBySize = []
1319
+ const seenSizes = new Set()
1287
1320
  for (let index = 0; index < edgeClusters.length; index += 1) {
1288
1321
  const cluster = edgeClusters[index]
1289
1322
  if (!cluster.size || cluster.isHub) continue
1290
- if (!levelsBySize.some(level => level.size === cluster.size)) {
1291
- levelsBySize.push({
1292
- size: cluster.size,
1293
- lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
1294
- })
1295
- }
1323
+ if (seenSizes.has(cluster.size)) continue
1324
+ seenSizes.add(cluster.size)
1325
+ levelsBySize.push({
1326
+ size: cluster.size,
1327
+ lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
1328
+ })
1296
1329
  }
1297
1330
  levelsBySize.sort((left, right) => left.size - right.size)
1331
+ const resolvedNodeClusterById = new Map()
1298
1332
  const resolveClusterForNode = nodeId => {
1299
- if (state.ecosystemHubCluster?.nodeIds.includes(nodeId) && clusterIds.has(state.ecosystemHubCluster.id)) {
1333
+ if (resolvedNodeClusterById.has(nodeId)) {
1334
+ return resolvedNodeClusterById.get(nodeId)
1335
+ }
1336
+ if (state.ecosystemHubNodeIds.has(nodeId) && state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)) {
1337
+ resolvedNodeClusterById.set(nodeId, state.ecosystemHubCluster)
1300
1338
  return state.ecosystemHubCluster
1301
1339
  }
1302
1340
  for (let index = 0; index < levelsBySize.length; index += 1) {
1303
1341
  const lookup = levelsBySize[index].lookup
1304
1342
  const cluster = lookup.get(nodeId)
1305
1343
  if (cluster && clusterIds.has(cluster.id)) {
1306
- return clusterById.get(cluster.id) ?? cluster
1344
+ const resolvedCluster = clusterById.get(cluster.id) ?? cluster
1345
+ resolvedNodeClusterById.set(nodeId, resolvedCluster)
1346
+ return resolvedCluster
1307
1347
  }
1308
1348
  }
1349
+ resolvedNodeClusterById.set(nodeId, null)
1309
1350
  return null
1310
1351
  }
1311
1352
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.106",
3
+ "version": "0.1.0-beta.108",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",