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

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.
@@ -7,7 +7,7 @@ const renderNodeBudget = 900
7
7
  const renderEdgeBudget = 2400
8
8
  const clusterActivationNodeThreshold = 600
9
9
  const clusterZoomThreshold = 0.18
10
- const macroGalaxyZoomThreshold = 0.012
10
+ const macroGalaxyZoomThreshold = 0.0012
11
11
  const massiveAutoFitMacroScale = 0.006
12
12
  const defaultMacroScale = 0.006
13
13
  const clusterCellPixelSize = 64
@@ -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,20 @@ 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
+ const isMemoryHubNode = node => node.title.trim().toLowerCase() === 'memory hub'
239
+
240
+ const hubNodeScore = node => {
241
+ const title = node.title.trim().toLowerCase()
242
+ if (title === 'memory hub') return 6
243
+ if (title === 'knowledge hub') return 5
244
+ if (memoryHubPathPattern.test(node.path || '')) return 4
245
+ if (node.tags.some(tag => tag.trim().toLowerCase() === 'memory-hub')) return 3
246
+ if (/\bmoc\b/i.test(node.title)) return 2
247
+ return hubNodePattern.test(node.title) || hubNodePattern.test(node.path || '') || node.tags.some(tag => hubNodePattern.test(tag))
248
+ ? 1
249
+ : 0
250
+ }
235
251
 
236
252
  const localFilteredNodes = query =>
237
253
  state.nodes.filter(node =>
@@ -246,8 +262,10 @@ const rankedHubNodes = () => {
246
262
  }
247
263
 
248
264
  const byTitleAndDegree = [...state.nodes]
249
- .filter(node => hubNodePattern.test(node.title) || hubNodePattern.test(node.path) || node.tags.some(tag => hubNodePattern.test(tag)))
265
+ .filter(node => hubNodeScore(node) > 0)
250
266
  .sort((left, right) => {
267
+ const byHubScore = hubNodeScore(right) - hubNodeScore(left)
268
+ if (byHubScore !== 0) return byHubScore
251
269
  const byDegree = (state.nodeDegrees.get(right.id) ?? 0) - (state.nodeDegrees.get(left.id) ?? 0)
252
270
  if (byDegree !== 0) return byDegree
253
271
  return left.title.localeCompare(right.title)
@@ -292,11 +310,13 @@ const resolveMacroRepresentative = (nodes) => {
292
310
  return null
293
311
  }
294
312
 
295
- let best = nodes[0]
313
+ const nonHubNodes = nodes.filter(node => !isMemoryHubNode(node))
314
+ const pool = nonHubNodes.length > 0 ? nonHubNodes : nodes
315
+ let best = pool[0]
296
316
  let bestDegree = state.nodeDegrees.get(best.id) ?? 0
297
317
 
298
- for (let index = 1; index < nodes.length; index += 1) {
299
- const node = nodes[index]
318
+ for (let index = 1; index < pool.length; index += 1) {
319
+ const node = pool[index]
300
320
  const degree = state.nodeDegrees.get(node.id) ?? 0
301
321
  if (degree > bestDegree) {
302
322
  best = node
@@ -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 => {
@@ -748,9 +826,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
748
826
  const macroScale = nodes.length > massiveGraphNodeThreshold ? massiveAutoFitMacroScale : defaultMacroScale
749
827
  const scale = options.macro && nodes.length > 1
750
828
  ? clampScale(Math.min(baselineScale, macroScale))
751
- : nodes.length > massiveGraphNodeThreshold
752
- ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
753
- : baselineScale
829
+ : baselineScale
754
830
  const hubCenter =
755
831
  options.preferHubCenter && state.primaryHub && nodes.some((node) => node.id === state.primaryHub.id)
756
832
  ? state.primaryHub
@@ -768,7 +844,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
768
844
  markRenderDirty()
769
845
  }
770
846
 
771
- const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
847
+ const resetView = () => fitView({ useFiltered: false, macro: false, preferHubCenter: true })
772
848
 
773
849
  const createLayout = graph => {
774
850
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
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.56",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",