@andespindola/brainlink 0.1.0-beta.54 → 0.1.0-beta.55

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.
@@ -18,6 +18,7 @@ const transformCoordinateLimit = 20_000_000
18
18
  const hoverHitTestIntervalMs = 64
19
19
  const overviewClusterMaxCount = 1400
20
20
  const zoomRecoveryGuardMs = 1500
21
+ const zoomCapTargetViewportShare = 0.72
21
22
  const state = {
22
23
  graph: { nodes: [], edges: [] },
23
24
  nodes: [],
@@ -53,6 +54,7 @@ const state = {
53
54
  macroCenter: { x: 0, y: 0 },
54
55
  macroRepresentative: null,
55
56
  primaryHub: null,
57
+ hubNeighborDistance: Number.POSITIVE_INFINITY,
56
58
  filterWorker: null,
57
59
  filterReady: false,
58
60
  lastHoverHitAt: 0,
@@ -232,6 +234,19 @@ const resize = () => {
232
234
  const normalizeQuery = value => value.trim().toLowerCase()
233
235
  const hubNodeRetentionLimit = 2
234
236
  const hubNodePattern = /\b(memory\s*hub|knowledge\s*hub|hub|moc|map|memory\s*map|mapa)\b/i
237
+ const memoryHubPathPattern = /\bmemory[-_\s]*hub\b/i
238
+
239
+ const hubNodeScore = node => {
240
+ const title = node.title.trim().toLowerCase()
241
+ if (title === 'memory hub') return 6
242
+ if (title === 'knowledge hub') return 5
243
+ if (memoryHubPathPattern.test(node.path || '')) return 4
244
+ if (node.tags.some(tag => tag.trim().toLowerCase() === 'memory-hub')) return 3
245
+ if (/\bmoc\b/i.test(node.title)) return 2
246
+ return hubNodePattern.test(node.title) || hubNodePattern.test(node.path || '') || node.tags.some(tag => hubNodePattern.test(tag))
247
+ ? 1
248
+ : 0
249
+ }
235
250
 
236
251
  const localFilteredNodes = query =>
237
252
  state.nodes.filter(node =>
@@ -246,8 +261,10 @@ const rankedHubNodes = () => {
246
261
  }
247
262
 
248
263
  const byTitleAndDegree = [...state.nodes]
249
- .filter(node => hubNodePattern.test(node.title) || hubNodePattern.test(node.path) || node.tags.some(tag => hubNodePattern.test(tag)))
264
+ .filter(node => hubNodeScore(node) > 0)
250
265
  .sort((left, right) => {
266
+ const byHubScore = hubNodeScore(right) - hubNodeScore(left)
267
+ if (byHubScore !== 0) return byHubScore
251
268
  const byDegree = (state.nodeDegrees.get(right.id) ?? 0) - (state.nodeDegrees.get(left.id) ?? 0)
252
269
  if (byDegree !== 0) return byDegree
253
270
  return left.title.localeCompare(right.title)
@@ -292,7 +309,10 @@ const resolveMacroRepresentative = (nodes) => {
292
309
  return null
293
310
  }
294
311
 
295
- let best = nodes[0]
312
+ const hubCandidate = state.primaryHub && nodes.some(node => node.id === state.primaryHub.id)
313
+ ? state.primaryHub
314
+ : null
315
+ let best = hubCandidate ?? nodes[0]
296
316
  let bestDegree = state.nodeDegrees.get(best.id) ?? 0
297
317
 
298
318
  for (let index = 1; index < nodes.length; index += 1) {
@@ -307,6 +327,24 @@ const resolveMacroRepresentative = (nodes) => {
307
327
  return best
308
328
  }
309
329
 
330
+ const nearestHubNeighborDistance = (hub, nodes) => {
331
+ if (!hub || nodes.length <= 1) {
332
+ return Number.POSITIVE_INFINITY
333
+ }
334
+
335
+ let minimum = Number.POSITIVE_INFINITY
336
+ for (let index = 0; index < nodes.length; index += 1) {
337
+ const node = nodes[index]
338
+ if (node.id === hub.id) continue
339
+ const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
340
+ if (distance < minimum) {
341
+ minimum = distance
342
+ }
343
+ }
344
+
345
+ return minimum
346
+ }
347
+
310
348
  const recomputeVisibility = () => {
311
349
  const nodes = filteredNodes()
312
350
  const ids = new Set(nodes.map(node => node.id))
@@ -322,15 +360,17 @@ const recomputeVisibility = () => {
322
360
  state.visibleNodeSpatial = createSpatialIndex(nodes)
323
361
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
324
362
  state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
363
+ const primaryHub = rankedHubNodes()[0] ?? null
364
+ state.primaryHub = primaryHub
365
+ state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
325
366
  const bounds = graphBounds(nodes)
326
367
  state.macroCenter = bounds
327
368
  ? {
328
- x: (bounds.minX + bounds.maxX) / 2,
329
- y: (bounds.minY + bounds.maxY) / 2
369
+ x: primaryHub ? primaryHub.x : (bounds.minX + bounds.maxX) / 2,
370
+ y: primaryHub ? primaryHub.y : (bounds.minY + bounds.maxY) / 2
330
371
  }
331
372
  : { x: 0, y: 0 }
332
373
  state.macroRepresentative = resolveMacroRepresentative(nodes)
333
- state.primaryHub = rankedHubNodes()[0] ?? null
334
374
  markRenderDirty()
335
375
  }
336
376
 
@@ -641,23 +681,61 @@ const ensureHubNodesInRenderedSet = (nodes) => {
641
681
  return nodes
642
682
  }
643
683
 
644
- const maxNodes = Math.max(renderNodeBudget, nodes.length)
684
+ const maxNodes = Math.max(Math.min(renderNodeBudget, nodes.length), 1)
645
685
  const ids = new Set(nodes.map((node) => node.id))
646
686
  const hubs = rankedHubNodes()
647
687
  const merged = [...nodes]
648
688
 
649
- for (let index = 0; index < hubs.length && merged.length < maxNodes; index += 1) {
689
+ for (let index = 0; index < hubs.length; index += 1) {
650
690
  const hub = hubs[index]
651
- if (!ids.has(hub.id)) {
691
+ if (ids.has(hub.id)) {
692
+ continue
693
+ }
694
+
695
+ if (merged.length < maxNodes) {
652
696
  merged.push(hub)
653
697
  ids.add(hub.id)
698
+ continue
699
+ }
700
+
701
+ const replacementIndex = merged.findIndex((node) => !hubs.some((candidate) => candidate.id === node.id))
702
+ if (replacementIndex >= 0) {
703
+ ids.delete(merged[replacementIndex].id)
704
+ merged[replacementIndex] = hub
705
+ ids.add(hub.id)
654
706
  }
655
707
  }
656
708
 
657
709
  return merged
658
710
  }
659
711
 
660
- const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
712
+ const zoomCapByNodeCount = (nodeCount) => {
713
+ if (nodeCount > 50000) return 0.88
714
+ if (nodeCount > 20000) return 1.15
715
+ if (nodeCount > 6000) return 1.65
716
+ if (nodeCount > 2000) return 2.2
717
+ return zoomRange.max
718
+ }
719
+
720
+ const zoomCapByHubDistance = (distance) => {
721
+ if (!Number.isFinite(distance) || distance <= 0) {
722
+ return zoomRange.max
723
+ }
724
+
725
+ const rect = canvas.getBoundingClientRect()
726
+ const viewportWidth = Math.max(rect.width, 320)
727
+ const viewportHeight = Math.max(rect.height, 320)
728
+ const reference = Math.max(220, Math.min(viewportWidth, viewportHeight) * zoomCapTargetViewportShare)
729
+ return Math.max(0.3, Math.min(zoomRange.max, reference / distance))
730
+ }
731
+
732
+ const currentZoomMax = () => {
733
+ const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
734
+ const capped = Math.min(zoomCapByNodeCount(nodeCount), zoomCapByHubDistance(state.hubNeighborDistance))
735
+ return Math.max(zoomRange.min * 2, capped)
736
+ }
737
+
738
+ const clampScale = value => Math.max(zoomRange.min, Math.min(currentZoomMax(), value))
661
739
  const isFiniteNumber = value => Number.isFinite(value)
662
740
  const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
663
741
  const clampTransformCoordinate = value => {
@@ -1123,7 +1201,7 @@ const computeRenderVisibility = () => {
1123
1201
  if (shouldRenderMacroGalaxy) {
1124
1202
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1125
1203
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1126
- const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
1204
+ const representative = state.primaryHub ?? state.macroRepresentative ?? sourceNodes[0] ?? null
1127
1205
  if (representative) {
1128
1206
  state.renderClusters = [
1129
1207
  {
@@ -1371,6 +1449,13 @@ const render = now => {
1371
1449
  ctx.lineWidth = 1.4 / safeScale
1372
1450
  ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
1373
1451
  ctx.stroke()
1452
+ if (isMacro && cluster.representative?.title) {
1453
+ ctx.fillStyle = '#edf2f7'
1454
+ ctx.font = 12 / safeScale + 'px Inter, system-ui, sans-serif'
1455
+ ctx.textAlign = 'center'
1456
+ ctx.textBaseline = 'top'
1457
+ ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
1458
+ }
1374
1459
  // Keep cluster markers minimal and faster to draw on large graphs.
1375
1460
  })
1376
1461
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.54",
3
+ "version": "0.1.0-beta.55",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",