@andespindola/brainlink 0.1.0-beta.131 → 0.1.0-beta.132

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
@@ -606,7 +606,7 @@ The graph UI shows:
606
606
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
607
607
  - 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
608
608
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
609
- - large graph LOD keeps a recursive graph-of-graphs model: zoom-out fills the projected macro level toward 1000 lightweight group nodes with sparse strongest-link edges, zoom-in fits the framed node's full child-graph circumference with extra margin before expanding, groups with child groups open another graph level instead of jumping to leaf notes, and zoom-out restores sibling groups
609
+ - large graph LOD keeps a recursive graph-of-graphs model: zoom-out fills the projected macro level toward 1000 lightweight group nodes with every aggregated link between visible groups, zoom-in grows the focused node into a local child graph anchored at the same point, once the child graph is framed the sibling groups stop rendering, groups with child groups open another graph level instead of jumping to leaf notes, and zoom-out restores sibling groups
610
610
 
611
611
  The server indexes before starting by default. Use `--no-index` to skip that step:
612
612
 
@@ -16,7 +16,9 @@ const hierarchyMicroEnterCoverage = 0.58
16
16
  const hierarchyMicroExitCoverage = 0.38
17
17
  const hierarchyMicroEnterScale = 0.18
18
18
  const hierarchyMicroExitScale = 0.12
19
- const hierarchyChildGraphFitMargin = 1.38
19
+ const hierarchyFocusedOnlyProgress = 0.5
20
+ const hierarchyFocusedOnlyCoverage = 0.4
21
+ const hierarchyChildGraphFitMargin = 1.28
20
22
  const minNodePixelRadius = 2.3
21
23
  const viewportPaddingPx = 280
22
24
  const worldCoordinateLimit = 5_000_000
@@ -647,7 +649,7 @@ const createVisibleEdgeLookup = edges => {
647
649
 
648
650
  const edgeBudgetForCurrentFrame = () => {
649
651
  const zoom = state.transform.scale
650
- if (zoom < 0.12) return 380
652
+ if (zoom < 0.12) return state.groups.length > 0 ? 1200 : 380
651
653
  if (zoom < 0.18) return 900
652
654
  if (zoom < 0.28) return 1700
653
655
  if (zoom < 0.45) return 2800
@@ -1319,6 +1321,43 @@ const arrangeChildGraphNodes = (nodes, group, origin = group) => {
1319
1321
  return arranged
1320
1322
  }
1321
1323
 
1324
+ const arrangeChildGroupNodes = (groups, parentGroup, origin) => {
1325
+ if (groups.length <= 1) {
1326
+ return groups.map(group => ({
1327
+ ...createGroupRenderNode(group),
1328
+ x: origin.x,
1329
+ y: origin.y
1330
+ }))
1331
+ }
1332
+
1333
+ const targetRadius = childGraphRenderRadius(parentGroup)
1334
+ const centerGroup = groups
1335
+ .map(group => ({
1336
+ group,
1337
+ score: Math.max(group.nodeIds.length, group.childGroupIds.length, 1)
1338
+ }))
1339
+ .sort((left, right) => right.score - left.score || left.group.title.localeCompare(right.group.title))[0]?.group
1340
+ const outerGroups = groups
1341
+ .filter(group => group.id !== centerGroup?.id)
1342
+ .sort((left, right) => left.segment.localeCompare(right.segment) || left.title.localeCompare(right.title))
1343
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5))
1344
+ const arranged = centerGroup
1345
+ ? [{ ...createGroupRenderNode(centerGroup), x: origin.x, y: origin.y }]
1346
+ : []
1347
+
1348
+ outerGroups.forEach((group, index) => {
1349
+ const ringRadius = targetRadius * Math.sqrt((index + 1) / Math.max(outerGroups.length, 1))
1350
+ const angle = index * goldenAngle
1351
+ arranged.push({
1352
+ ...createGroupRenderNode(group),
1353
+ x: origin.x + Math.cos(angle) * ringRadius,
1354
+ y: origin.y + Math.sin(angle) * ringRadius
1355
+ })
1356
+ })
1357
+
1358
+ return arranged
1359
+ }
1360
+
1322
1361
  const interpolateNodeFromGroup = (node, origin, progress) => {
1323
1362
  return {
1324
1363
  ...node,
@@ -1364,7 +1403,7 @@ const hierarchyGroupsForScale = () => {
1364
1403
  const groupViewportCoverage = (group, viewport) => {
1365
1404
  const viewportWidth = Math.max(viewport.maxX - viewport.minX, 1)
1366
1405
  const viewportHeight = Math.max(viewport.maxY - viewport.minY, 1)
1367
- const viewportRadius = Math.max(viewportWidth, viewportHeight) / 2
1406
+ const viewportRadius = Math.min(viewportWidth, viewportHeight) / 2
1368
1407
  const centerX = (viewport.minX + viewport.maxX) / 2
1369
1408
  const centerY = (viewport.minY + viewport.maxY) / 2
1370
1409
  const centerDistance = Math.hypot(group.x - centerX, group.y - centerY)
@@ -1440,8 +1479,8 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
1440
1479
 
1441
1480
  if (
1442
1481
  candidate &&
1443
- state.transform.scale >= hierarchyMicroEnterScale &&
1444
- candidate.coverage >= hierarchyMicroEnterCoverage
1482
+ state.transform.scale >= hierarchyMicroExitScale &&
1483
+ candidate.coverage >= hierarchyMicroExitCoverage
1445
1484
  ) {
1446
1485
  state.hierarchyFocusGroupId = candidate.group.id
1447
1486
  if (candidate.group.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== candidate.group.id) {
@@ -1508,19 +1547,8 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
1508
1547
  })
1509
1548
  }
1510
1549
 
1511
- const degreeCounts = new Map()
1512
1550
  return Array.from(selected.values())
1513
1551
  .sort((left, right) => edgeWeight(right) - edgeWeight(left) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target))
1514
- .filter((edge) => {
1515
- const sourceCount = degreeCounts.get(edge.source) ?? 0
1516
- const targetCount = degreeCounts.get(edge.target) ?? 0
1517
- if (sourceCount >= 3 || targetCount >= 3) {
1518
- return false
1519
- }
1520
- degreeCounts.set(edge.source, sourceCount + 1)
1521
- degreeCounts.set(edge.target, targetCount + 1)
1522
- return true
1523
- })
1524
1552
  .slice(0, Math.min(hierarchyGroupEdgeLimit, edgeBudgetForCurrentFrame()))
1525
1553
  }
1526
1554
 
@@ -1547,25 +1575,23 @@ const computeHierarchyRenderVisibility = (viewport) => {
1547
1575
  const focusChildGroups = focus.childGroupIds
1548
1576
  .map(groupId => state.groupById.get(groupId))
1549
1577
  .filter(Boolean)
1550
- const childLimit = Math.max(90, Math.min(renderNodeBudget, Math.floor(renderNodeBudget * progress)))
1578
+ const childLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * progress)))
1551
1579
  const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
1552
1580
  const arrangedChildren = focusChildGroups.length > 0
1553
- ? arrangeGraphLevelNodes(focusChildGroups, groupRenderRadius).map(group => ({
1554
- ...createGroupRenderNode(group),
1555
- x: focusRenderNode.x + group.x,
1556
- y: focusRenderNode.y + group.y
1557
- }))
1581
+ ? arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
1558
1582
  : arrangeChildGraphNodes(rawChildNodes, focus, focusRenderNode)
1559
1583
  const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
1560
1584
  .map(node => interpolateNodeFromGroup(node, focusRenderNode, progress))
1561
1585
  const childIds = new Set(childNodes.map(node => node.id))
1562
1586
  const childById = new Map(childNodes.map(node => [node.id, node]))
1563
- const isMicroView = progress >= 0.72
1587
+ const isMicroView =
1588
+ progress >= hierarchyFocusedOnlyProgress ||
1589
+ groupViewportCoverage(focus, viewport) >= hierarchyFocusedOnlyCoverage
1564
1590
  const visibleGroupNodes = isMicroView
1565
1591
  ? []
1566
1592
  : groupNodes.filter(node => node.groupId !== focus.id || progress < 0.92)
1567
1593
  const groupEdges = isMicroView ? [] : groupEdgesForRenderedGroups(visibleGroupNodes)
1568
- const childEdges = progress > 0.32
1594
+ const childEdges = (isMicroView || progress > 0.02)
1569
1595
  ? focusChildGroups.length > 0
1570
1596
  ? groupEdgesForRenderedGroups(childNodes)
1571
1597
  : collectVisibleEdgesForNodes(childIds).map(edge => ({
@@ -2086,7 +2112,7 @@ const childGraphFitTransition = (node, group) => {
2086
2112
  const width = Math.max(rect.width, 320)
2087
2113
  const height = Math.max(rect.height, 320)
2088
2114
  const fitScale = childGraphFitScaleForGroup(group, width, height)
2089
- const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.16, hierarchyMicroEnterScale * 1.04))
2115
+ const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
2090
2116
 
2091
2117
  return {
2092
2118
  active: true,
@@ -2946,7 +2972,7 @@ const selectNode = (node, options = { openContent: false }) => {
2946
2972
  screenY: state.viewport.height / 2,
2947
2973
  worldX: node.x,
2948
2974
  worldY: node.y,
2949
- targetScale: clampScale(Math.max(state.transform.scale * 1.16, hierarchyMicroEnterScale * 1.04))
2975
+ targetScale: clampScale(Math.max(state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
2950
2976
  }
2951
2977
  state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
2952
2978
  state.hierarchyFocusGroupId = node.groupId
@@ -607,7 +607,7 @@ Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
607
607
 
608
608
  The frontend includes an agent selector that shows only the agent id. Selecting an agent calls the same read APIs with `agent=<agent-id>` and renders that namespace instead of merging every agent into one graph.
609
609
 
610
- Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy groups that fill each visible graph level toward 1000 nodes while keeping every group capped at 1000 child nodes; zoom-out renders the macro level as a sparse strongest-link graph of group nodes, zoom-in fits the focused node's full child-graph circumference with extra margin before expanding, and groups with child groups open another graph level instead of jumping directly to leaf notes.
610
+ Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy groups that fill each visible graph level toward 1000 nodes while keeping every group capped at 1000 child nodes; zoom-out renders the macro level with every aggregated link between visible group nodes, zoom-in grows the focused node into a local child graph anchored at the same point, once the child graph is framed the sibling groups stop rendering, and groups with child groups open another graph level instead of jumping directly to leaf notes.
611
611
  During graph filtering, Brainlink keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) so filtered views still show relationship anchors.
612
612
 
613
613
  The command reindexes by default, then serves:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.131",
3
+ "version": "0.1.0-beta.132",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",