@andespindola/brainlink 0.1.0-beta.48 → 0.1.0-beta.49

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
@@ -82,8 +82,9 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
82
82
  - Local HTTP API.
83
83
  - Realtime graph UI with agent selector and colored knowledge groups.
84
84
  - Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
85
- - Large graph layout API automatically uses compact payload encoding and edge-cap transmission to reduce initial client load on very large vaults.
85
+ - Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
86
86
  - Zoomed-out graph LOD now clusters dense regions and progressively expands nodes as zoom increases.
87
+ - Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
87
88
  - Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
88
89
  - Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
89
90
 
@@ -2,13 +2,14 @@ export const createClientJs = () => `const canvas = document.getElementById('gra
2
2
  const ctx = canvas.getContext('2d')
3
3
  const largeGraphNodeThreshold = 4000
4
4
  const massiveGraphNodeThreshold = 20000
5
- const largeGraphEdgeRenderLimit = 16000
5
+ const largeGraphEdgeRenderLimit = 120000
6
6
  const renderNodeBudget = 900
7
7
  const renderEdgeBudget = 2400
8
8
  const clusterActivationNodeThreshold = 600
9
9
  const clusterZoomThreshold = 0.18
10
10
  const macroGalaxyZoomThreshold = 0.012
11
11
  const massiveAutoFitMacroScale = 0.006
12
+ const defaultMacroScale = 0.006
12
13
  const clusterCellPixelSize = 64
13
14
  const minNodePixelRadius = 2.3
14
15
  const viewportPaddingPx = 280
@@ -609,7 +610,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
609
610
  return { min: 0.0008, max: 0.24 }
610
611
  }
611
612
 
612
- const fitView = (options = { useFiltered: true }) => {
613
+ const fitView = (options = { useFiltered: true, macro: false }) => {
613
614
  const rect = canvas.getBoundingClientRect()
614
615
  const width = Math.max(rect.width, 320)
615
616
  const height = Math.max(rect.height, 320)
@@ -640,9 +641,12 @@ const fitView = (options = { useFiltered: true }) => {
640
641
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
641
642
  const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
642
643
  const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
643
- const scale = nodes.length > massiveGraphNodeThreshold
644
- ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
645
- : baselineScale
644
+ const macroScale = nodes.length > massiveGraphNodeThreshold ? massiveAutoFitMacroScale : defaultMacroScale
645
+ const scale = options.macro && nodes.length > 1
646
+ ? clampScale(Math.min(baselineScale, macroScale))
647
+ : nodes.length > massiveGraphNodeThreshold
648
+ ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
649
+ : baselineScale
646
650
  const centerX = (bounds.minX + bounds.maxX) / 2
647
651
  const centerY = (bounds.minY + bounds.maxY) / 2
648
652
 
@@ -656,7 +660,7 @@ const fitView = (options = { useFiltered: true }) => {
656
660
  markRenderDirty()
657
661
  }
658
662
 
659
- const resetView = () => fitView({ useFiltered: false })
663
+ const resetView = () => fitView({ useFiltered: false, macro: true })
660
664
 
661
665
  const createLayout = graph => {
662
666
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
@@ -1005,6 +1009,32 @@ const computeRenderVisibility = () => {
1005
1009
  state.lastViewportKey = viewportKey
1006
1010
  state.renderVisibilityDirty = false
1007
1011
 
1012
+ const shouldRenderMacroGalaxy =
1013
+ state.transform.scale <= macroGalaxyZoomThreshold && state.visibleNodes.length > 1
1014
+
1015
+ if (shouldRenderMacroGalaxy) {
1016
+ const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1017
+ const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1018
+ const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
1019
+ if (representative) {
1020
+ state.renderClusters = [
1021
+ {
1022
+ id: 'macro-galaxy',
1023
+ x: state.macroCenter.x,
1024
+ y: state.macroCenter.y,
1025
+ count: sourceNodes.length,
1026
+ representative
1027
+ }
1028
+ ]
1029
+ state.renderNodes = [representative]
1030
+ } else {
1031
+ state.renderClusters = []
1032
+ state.renderNodes = []
1033
+ }
1034
+ state.renderEdges = []
1035
+ return
1036
+ }
1037
+
1008
1038
  if (state.visibleNodes.length <= 2000) {
1009
1039
  state.renderNodes = state.visibleNodes
1010
1040
  state.renderClusters = []
@@ -1016,26 +1046,6 @@ const computeRenderVisibility = () => {
1016
1046
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
1017
1047
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1018
1048
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1019
- if (state.transform.scale <= macroGalaxyZoomThreshold) {
1020
- const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
1021
- if (representative) {
1022
- state.renderClusters = [
1023
- {
1024
- id: 'macro-galaxy',
1025
- x: state.macroCenter.x,
1026
- y: state.macroCenter.y,
1027
- count: sourceNodes.length,
1028
- representative
1029
- }
1030
- ]
1031
- state.renderNodes = [representative]
1032
- } else {
1033
- state.renderClusters = []
1034
- state.renderNodes = []
1035
- }
1036
- state.renderEdges = []
1037
- return
1038
- }
1039
1049
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
1040
1050
  const sampled = sourceNodes.length > sampleLimit
1041
1051
  ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), sourceNodes)
@@ -1325,6 +1335,8 @@ const fetchNodeDetails = async node => {
1325
1335
  return detail
1326
1336
  }
1327
1337
 
1338
+ const wait = async (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds))
1339
+
1328
1340
  const openContentDialog = async node => {
1329
1341
  if (!node) return
1330
1342
  elements.contentTitle.textContent = node.title || 'Loading...'
@@ -1340,19 +1352,32 @@ const openContentDialog = async node => {
1340
1352
  elements.contentDialog.showModal()
1341
1353
  }
1342
1354
 
1355
+ const applyDetailToDialog = detail => {
1356
+ elements.contentTitle.textContent = detail.title
1357
+ elements.contentPath.textContent = detail.path
1358
+ elements.contentTags.innerHTML = detail.tags.length
1359
+ ? detail.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
1360
+ : '<span>No tags</span>'
1361
+ elements.contentBody.textContent = detail.content
1362
+ }
1363
+
1343
1364
  try {
1344
1365
  const detailedNode = await fetchNodeDetails(node)
1345
1366
  if (state.selected?.id !== node.id) {
1346
1367
  return
1347
1368
  }
1348
- elements.contentTitle.textContent = detailedNode.title
1349
- elements.contentPath.textContent = detailedNode.path
1350
- elements.contentTags.innerHTML = detailedNode.tags.length
1351
- ? detailedNode.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
1352
- : '<span>No tags</span>'
1353
- elements.contentBody.textContent = detailedNode.content
1369
+ applyDetailToDialog(detailedNode)
1354
1370
  } catch {
1355
- elements.contentBody.textContent = 'Unable to load note content.'
1371
+ try {
1372
+ await wait(120)
1373
+ const retriedNode = await fetchNodeDetails(node)
1374
+ if (state.selected?.id !== node.id) {
1375
+ return
1376
+ }
1377
+ applyDetailToDialog(retriedNode)
1378
+ } catch {
1379
+ elements.contentBody.textContent = 'Unable to load note content.'
1380
+ }
1356
1381
  }
1357
1382
  }
1358
1383
 
@@ -63,22 +63,62 @@ const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
63
63
  return 30_000;
64
64
  return compactGraphLayoutEdgeLimit;
65
65
  };
66
+ const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
67
+ const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
68
+ const selectCompactEdges = (layout, limit) => {
69
+ const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
70
+ if (resolvedEdges.length <= limit) {
71
+ return resolvedEdges;
72
+ }
73
+ const bestEdgeByEndpoint = new Map();
74
+ for (let index = 0; index < resolvedEdges.length; index += 1) {
75
+ const edge = resolvedEdges[index];
76
+ const endpoints = [edge.source, edge.target];
77
+ for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
78
+ const endpoint = endpoints[endpointIndex];
79
+ const previous = bestEdgeByEndpoint.get(endpoint);
80
+ if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
81
+ bestEdgeByEndpoint.set(endpoint, edge);
82
+ }
83
+ }
84
+ }
85
+ const selected = new Map();
86
+ for (const edge of bestEdgeByEndpoint.values()) {
87
+ selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
88
+ }
89
+ if (selected.size > limit) {
90
+ return Array.from(selected.values())
91
+ .sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
92
+ .slice(0, limit);
93
+ }
94
+ const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
95
+ for (let index = 0; index < byWeight.length; index += 1) {
96
+ if (selected.size >= limit) {
97
+ break;
98
+ }
99
+ const edge = byWeight[index];
100
+ const key = edgeKey(edge.source, edge.target, edge.priority);
101
+ if (!selected.has(key)) {
102
+ selected.set(key, edge);
103
+ }
104
+ }
105
+ return Array.from(selected.values());
106
+ };
66
107
  const stripLayoutContent = (layout) => ({
67
108
  ...layout,
68
109
  nodes: layout.nodes.map(({ content, ...node }) => node)
69
110
  });
70
111
  const compactLayoutPayload = (layout) => {
71
112
  const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
113
+ const compactEdges = selectCompactEdges(layout, edgeLimit);
72
114
  const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
73
- const compactEdges = [...layout.edges]
74
- .sort((left, right) => (right.weight ?? 1) - (left.weight ?? 1))
75
- .slice(0, edgeLimit)
115
+ const compactEdgeRows = compactEdges
76
116
  .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
77
117
  return {
78
118
  compact: true,
79
119
  layout: {
80
120
  nodes: compactNodes,
81
- edges: compactEdges
121
+ edges: compactEdgeRows
82
122
  },
83
123
  totals: {
84
124
  nodes: layout.nodes.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.48",
3
+ "version": "0.1.0-beta.49",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",