@andespindola/brainlink 0.1.0-beta.74 → 0.1.0-beta.76

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
@@ -594,7 +594,7 @@ The graph UI shows:
594
594
  - double-click on canvas zooms in at cursor position
595
595
  - floating graph totals (notes, links, tags) below the Brainlink title
596
596
  - large-graph rendering safeguards (edge draw caps, lower redraw rate, zoom-aware interaction)
597
- - massive-graph LOD progression: very low zoom uses spatial overview clusters to preserve whole-vault shape, then progressively reveals nodes and edges as zoom increases
597
+ - 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 reveals nodes and edges as zoom increases
598
598
 
599
599
  The server indexes before starting by default. Use `--no-index` to skip that step:
600
600
 
@@ -1110,6 +1110,64 @@ const enrichSampleWithNeighbors = (nodes) => {
1110
1110
  }
1111
1111
  }
1112
1112
 
1113
+ const includeHubPreviewNeighborhood = (nodes, limit) => {
1114
+ const hub = state.primaryHub
1115
+ if (!hub) {
1116
+ return nodes
1117
+ }
1118
+
1119
+ const maxNodes = Math.max(1, Math.min(renderNodeBudget, limit))
1120
+ const merged = [...nodes]
1121
+ const ids = new Set(merged.map((node) => node.id))
1122
+
1123
+ if (!ids.has(hub.id)) {
1124
+ if (merged.length < maxNodes) {
1125
+ merged.push(hub)
1126
+ ids.add(hub.id)
1127
+ } else {
1128
+ const replaceIndex = merged.findIndex((node) => node.id !== hub.id)
1129
+ if (replaceIndex >= 0) {
1130
+ ids.delete(merged[replaceIndex].id)
1131
+ merged[replaceIndex] = hub
1132
+ ids.add(hub.id)
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ const hubEdges = [...(state.visibleEdgeByNode.get(hub.id) ?? [])]
1138
+ .filter((edge) => edge.target && (edge.source === hub.id || edge.target === hub.id))
1139
+ .sort((left, right) => {
1140
+ const byWeight = edgeWeight(right) - edgeWeight(left)
1141
+ if (byWeight !== 0) return byWeight
1142
+
1143
+ const leftOtherId = left.source === hub.id ? left.target : left.source
1144
+ const rightOtherId = right.source === hub.id ? right.target : right.source
1145
+ const leftDegree = state.nodeDegrees.get(leftOtherId ?? '') ?? 0
1146
+ const rightDegree = state.nodeDegrees.get(rightOtherId ?? '') ?? 0
1147
+ if (leftDegree !== rightDegree) return rightDegree - leftDegree
1148
+
1149
+ return edgeIdentityKey(left).localeCompare(edgeIdentityKey(right))
1150
+ })
1151
+
1152
+ for (let index = 0; index < hubEdges.length && merged.length < maxNodes; index += 1) {
1153
+ const edge = hubEdges[index]
1154
+ const otherId = edge.source === hub.id ? edge.target : edge.source
1155
+ if (!otherId || ids.has(otherId)) {
1156
+ continue
1157
+ }
1158
+
1159
+ const otherNode = state.nodeById.get(otherId)
1160
+ if (!otherNode) {
1161
+ continue
1162
+ }
1163
+
1164
+ ids.add(otherId)
1165
+ merged.push(otherNode)
1166
+ }
1167
+
1168
+ return merged
1169
+ }
1170
+
1113
1171
  const ensureHubNodesInRenderedSet = (nodes) => {
1114
1172
  if (nodes.length === 0) {
1115
1173
  return nodes
@@ -1758,6 +1816,18 @@ const clusterViewportNodes = viewportNodes => {
1758
1816
  }))
1759
1817
  }
1760
1818
 
1819
+ const representativeNodesFromClusters = (clusters, limit) => {
1820
+ const representatives = clusters
1821
+ .map((cluster) => cluster.representative)
1822
+ .filter((node) => Boolean(node))
1823
+ const merged = mergeUniqueNodes(
1824
+ representatives,
1825
+ state.renderNodes ?? [],
1826
+ Math.max(1, limit)
1827
+ )
1828
+ return ensureHubNodesInRenderedSet(merged)
1829
+ }
1830
+
1761
1831
  const computeRenderVisibility = () => {
1762
1832
  if (!hasValidTransform()) {
1763
1833
  fitView({ useFiltered: true })
@@ -1816,8 +1886,11 @@ const computeRenderVisibility = () => {
1816
1886
  .sort((left, right) => right.count - left.count)
1817
1887
  .slice(0, Math.min(renderNodeBudget, clusterBudgetForScale(state.transform.scale)))
1818
1888
  if (overviewClusters.length > 0) {
1819
- state.renderClusters = overviewClusters
1820
- state.renderNodes = overviewClusters.map((cluster) => cluster.representative)
1889
+ state.renderClusters = []
1890
+ state.renderNodes = representativeNodesFromClusters(
1891
+ overviewClusters,
1892
+ Math.min(renderNodeBudget, clusterBudgetForScale(state.transform.scale))
1893
+ )
1821
1894
  state.renderEdges = []
1822
1895
  return
1823
1896
  }
@@ -1848,9 +1921,15 @@ const computeRenderVisibility = () => {
1848
1921
  sampledRaw,
1849
1922
  Math.min(sampleLimit, renderNodeBudget)
1850
1923
  )
1851
- const sampledIds = new Set(sampled.map((node) => node.id))
1852
- let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1853
1924
  let sampledNodes = ensureHubNodesInRenderedSet(sampled)
1925
+ if (state.transform.scale < 0.035) {
1926
+ sampledNodes = includeHubPreviewNeighborhood(
1927
+ sampledNodes,
1928
+ Math.min(renderNodeBudget, sampleLimit + 160)
1929
+ )
1930
+ }
1931
+ const sampledIds = new Set(sampledNodes.map((node) => node.id))
1932
+ let sampledEdges = collectVisibleEdgesForNodes(sampledIds)
1854
1933
 
1855
1934
  if (state.transform.scale >= 0.035 && sampledEdges.length === 0) {
1856
1935
  const enriched = enrichSampleWithNeighbors(sampledNodes)
@@ -1877,8 +1956,8 @@ const computeRenderVisibility = () => {
1877
1956
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1878
1957
  const clusters = clusterViewportNodes(viewportNodes)
1879
1958
  if (clusters.length > 0) {
1880
- state.renderClusters = clusters
1881
- state.renderNodes = clusters.map(cluster => cluster.representative)
1959
+ state.renderClusters = []
1960
+ state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
1882
1961
  state.renderEdges = []
1883
1962
  return
1884
1963
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.74",
3
+ "version": "0.1.0-beta.76",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",