Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,19 @@ ICEBUG_DIR = { value = "icebug", relative = true }
# - macOS .app bundles stage them under Contents/Frameworks
# - cargo dev/test binaries resolve ../../../ entries
# (target/{debug,release}/deps/ -> repo root)
# Absolute well-known rpaths (macOS only): the algo extension is downloaded
# at runtime and its own LC_RPATHs point at its build machine, so its
# @rpath deps (libarrow, libomp, libnetworkit) expand against *this*
# binary's rpaths. These entries let it resolve from Homebrew / system
# locations with no DYLD_LIBRARY_PATH. Missing dirs are harmless.
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/lib",
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/opt/apache-arrow/lib",
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/opt/libomp/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/opt/apache-arrow/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/opt/libomp/lib",
"-C", "link-arg=-Wl,-rpath,@executable_path",
"-C", "link-arg=-Wl,-rpath,@executable_path/icebug/lib",
"-C", "link-arg=-Wl,-rpath,@executable_path/liblbug",
Expand All @@ -37,6 +48,12 @@ rustflags = [

[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/lib",
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/opt/apache-arrow/lib",
"-C", "link-arg=-Wl,-rpath,/opt/homebrew/opt/libomp/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/opt/apache-arrow/lib",
"-C", "link-arg=-Wl,-rpath,/usr/local/opt/libomp/lib",
"-C", "link-arg=-Wl,-rpath,@executable_path",
"-C", "link-arg=-Wl,-rpath,@executable_path/icebug/lib",
"-C", "link-arg=-Wl,-rpath,@executable_path/liblbug",
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,24 @@ jobs:
$hash = (Get-FileHash "dist/$name.zip" -Algorithm SHA256).Hash.ToLower()
[IO.File]::WriteAllText("dist/$name.zip.sha256", "$hash $name.zip`n")

- name: Assert macOS well-known rpaths
if: matrix.platform == 'macos-latest'
shell: bash
run: |
# The runtime-downloaded algo extension resolves its @rpath deps
# against this binary's LC_RPATHs: fail if the Homebrew / system
# fallbacks are missing (no DYLD_LIBRARY_PATH in the field).
rpaths="$(otool -l dist/Bugscope.app/Contents/MacOS/bugscope | grep -A 2 LC_RPATH | grep 'path ')"
echo "$rpaths"
missing=0
for want in /opt/homebrew/lib /opt/homebrew/opt/apache-arrow/lib /opt/homebrew/opt/libomp/lib /usr/local/lib; do
if ! echo "$rpaths" | grep -Fq "path $want "; then
echo "missing LC_RPATH: $want" >&2
missing=1
fi
done
exit $missing

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ This is the native port of [bugscope-tauri](https://github.com/LadybugDB/bugscop
- **Visual Encoding** - Node size reflects connection count (more connections = larger nodes), and colors differentiate entity types.
- **Dark/Light Mode** - Follows the system appearance automatically, same themes as the Tauri app.
- **Relationship Labels** - Hover over edges to see the type of relationship between connected nodes.
- **Search + Focus** - Substring search over node properties; click a match to focus its 1-hop neighborhood.
- **Schema View** - Toggle to see node tables instead of the edge graph.
- **Search + Focus** - Substring search over node properties (`/foo`, bare `/` clears); click a match to focus its 1-hop neighborhood.
- **Breadcrumbs + dot-commands** - Drill-down trail under the header (`Root › A › B`, cached jumps) plus query-box `.root` / `.parent` (`.up`/`.back`) and `.schema` / `.data` (schema view without the menu).
- **Schema View** - Header `Data`/`Schema` toggle (or `.schema` / `.data` in the query box, `File → Toggle Schema View` menu) to switch between node tables and the edge graph. The breadcrumb badge (`schema ✕`) is also a one-click way back to data.
- **Live Layout** - Force simulation (repulsion + springs + damping) runs at 30 Hz and settles when the layout goes quiet; pause/resume any time.
- **Leiden Treemap** - Header toggle switches the canvas between the graph and a squarified treemap of Leiden communities (in-process icebug Leiden, same family as `GDS_LEIDEN`); tile area follows PageRank weight. Double-click drills into a neighborhood.
- **Leiden Treemap + Sunburst** - Header toggle (or `⌘1`/`⌘2`/`⌘3`) switches the canvas between the graph, a squarified treemap of Leiden communities, and a classic sunburst of the same communities (in-process icebug Leiden, same family as `GDS_LEIDEN`): the center disc is the root of the displayed graph, rings move outward with hierarchy depth (communities, then members), slice angles are proportional to PageRank share while each member's reach encodes its absolute weight — heavy members stick out, so the outer edge reads jagged rather than a perfect disc. Double-click drills into a neighborhood; clicking the sunburst center steps one level up the trail. At startup, graphs with more than 64 edges open in the treemap (smaller ones in the graph view); the choice then sticks — navigating never switches views, only the header toggle, menu, or `⌘1`/`⌘2`/`⌘3` do.
- **Insights Pane** - Collapsible right pane with the top 10 nodes by PageRank plus the Leiden community summary; clicking a row focuses (PageRank) or selects (community) it.

## Usage
Expand All @@ -25,8 +26,23 @@ This is the native port of [bugscope-tauri](https://github.com/LadybugDB/bugscop
4. Scroll to zoom in/out (zooms at the cursor), click and drag the canvas to pan
5. Hover over nodes to see their labels
6. Hover over edges to see relationship types
7. Double-click a node (or select it and choose "Expand neighborhood") to focus its 1-hop neighborhood
7. Double-click a node (or select it and choose "Expand neighborhood") to focus its 1-hop neighborhood.
Each focus pushes the breadcrumb trail under the header (`Root › A › B`):
click any crumb to jump back, or use `↑ Parent` / `⟲ Root` on the right.
8. Type in the query box and press Enter to search; click a match to focus it
9. Switch between the data graph and the schema (table) view with the header `Data`/`Schema` toggle, `.schema` / `.data` in the query box, or `File → Toggle Schema View`

### Query box: search, Cypher, and dot-commands

| Input | Effect |
|---|---|
| `/foo` | Substring search over node `name`/`title`/`label`/`id` + properties. Matches list in the sidebar; starting a new search first resets any zoom so matches are in full-graph context. |
| `/` (bare slash) | Clears text-search state: sidebar matches + any search zoom/selection, back to the root view. |
| `.root` | Breadcrumb root: clear the drill-down trail, show the full graph (or current Cypher result) from cache — no DB reload. |
| `.parent` (aliases: `.up`, `.back`) | One step up the trail (`B` → `A` → root). Reports `Already at root` at the top. |
| `.schema` / `.schema on` | Switch to the schema view (node tables + rel connectivity). Already there → resets to the schema root. Same as the header `Schema` pill. |
| `.data` / `.graph` (also `.schema off`) | Leave the schema view, back to the data graph. Same as the header `Data` pill or clicking the `schema ✕` badge in the breadcrumb bar. `.schema toggle` flips either way. |
| anything else | Runs as read Cypher and replaces the graph (new root, trail cleared). Unknown `.foo` is rejected with the valid list — it never runs as Cypher. |

## Prerequisites

Expand Down Expand Up @@ -101,10 +117,11 @@ The backend runs the same Cypher as the Tauri commands (`MATCH (a)-[r]->(b) RETU

## Deliberately out of scope for v1

Summary-space PageRank sidecars, LLM cluster naming, Voronoi overlay, the lever panel, Arrow IPC transport — all were web-renderer or sidecar concerns in bugscope-tauri. The native port loads the edge graph directly and lays it out live. The sunburst view is skipped for now (arc-heavy painting in GPUI needs more scaffolding than the treemap's rects).
Summary-space PageRank sidecars, LLM cluster naming, Voronoi overlay, the lever panel, Arrow IPC transport — all were web-renderer or sidecar concerns in bugscope-tauri. The native port loads the edge graph directly and lays it out live.

### Troubleshooting

- Window doesn't appear on launch - older versions scanned `$HOME` recursively on the UI thread before first paint and hung. Current versions only scan the working directory; update and retry.
- `Load failed: ...` in the status bar - the picked file isn't a readable LadybugDB database, or it's locked by another process.
- Search returns nothing - search scans node `name`/`title`/`label`/`id` plus all properties (first 50k nodes, 50 results); check the query box text and the selected database.
- Opening a file fails with `Failed to load library: .../.lbdb/extension/<ver>/.../libalgo.lbug_extension` - that DB ran `LOAD algo` before, which ladybug WAL-logs and replays on every open, so one stale cached build bricks the file. Library paths are not the cause (the binary resolves `@rpath` deps from its bundled `Frameworks` plus Homebrew/system locations, no `DYLD_LIBRARY_PATH` needed): it is symbol skew between the cached build and `liblbug` (check with plain `dlopen` — `symbol not found in flat namespace` means skew, `Library not loaded` means paths). Fix: back up, then replace the named cached file with a symbol-matching build (a plain `LOAD EXTENSION` of that file must succeed); `INSTALL algo` may re-serve the stale build, so avoid re-running it until upstream publishes matching bits. `BUGSCOPE_ALGO_EXTENSION` only affects post-open `LOAD`, not WAL replay.
53 changes: 53 additions & 0 deletions packaging/homebrew/bugscope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Homebrew formula for bugscope (native LadybugDB graph visualizer).
#
# Install from this tap:
# brew tap <you>/bugscope # with this file at Formula/bugscope.rb
# brew install bugscope
#
# Layout: the binary lives in libexec next to icebug/lib + liblbug, so the
# baked @executable_path rpaths (.cargo/config.toml) resolve with no env
# vars; `bin` gets a symlink. The runtime-downloaded algo extension
# (INSTALL algo on first GDS_* use) resolves its @rpath deps via the
# binary's LC_RPATHs, which also cover HOMEBREW_PREFIX lib dirs below.
#
# TODO: fill in `url`/`sha256` per release and the repo license.
class Bugscope < Formula
desc "Native graph visualizer for LadybugDB"
homepage "https://github.com/LadybugDB/bugscope"
url "https://github.com/LadybugDB/bugscope/archive/refs/tags/v0.1.0.tar.gz"
sha256 "REPLACE_WITH_RELEASE_TARBALL_SHA256"
# license "REPLACE_WITH_REPO_LICENSE"

depends_on "cmake" => :build
depends_on "pkg-config" => :build
depends_on "rust" => :build
depends_on "apache-arrow"
depends_on "libomp"
depends_on "openssl@3"

def install
# Prebuilt Networkit + shared liblbug (the algo extension dlopens
# against the shared ladybug symbols at LOAD time).
system "bash", "scripts/download_icebug.sh"
system "bash", "scripts/download-liblbug.sh"
# Vendor brew arrow/omp next to libnetworkit for version-pinned loads.
system "bash", "scripts/vendor_arrow.sh"

system "cargo", "build", "--release", "--locked"

libexec.install "target/release/bugscope"
libexec.install "icebug" => "icebug"
libexec.install "liblbug" => "liblbug"
# Belt and braces next to the baked-in rpaths: the install prefix and
# the brewed arrow/omp locations, so relocation never breaks @rpath.
for rpath in [libexec/"icebug/lib", libexec/"liblbug", HOMEBREW_PREFIX/"lib",
HOMEBREW_PREFIX/"opt/apache-arrow/lib", HOMEBREW_PREFIX/"opt/libomp/lib"] do
quiet_system "install_name_tool", "-add_rpath", rpath, libexec/"bugscope"
end
bin.install_symlink libexec/"bugscope"
end

test do
assert_match "Usage", shell_output("#{bin}/bugscope --help")
end
end
28 changes: 28 additions & 0 deletions scripts/stage_macos_bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ change_if_present() {
fi
}

# Well-known system/Homebrew locations, so the runtime-downloaded algo
# extension (whose own LC_RPATHs point at its build machine) resolves its
# @rpath deps with no DYLD_LIBRARY_PATH. Absolute LC_RPATH entries are
# honored by dyld and harmless when the dir is absent.
WELL_KNOWN_RPATHS=(
/opt/homebrew/lib
/opt/homebrew/opt/apache-arrow/lib
/opt/homebrew/opt/libomp/lib
/usr/local/lib
/usr/local/opt/apache-arrow/lib
/usr/local/opt/libomp/lib
)

add_rpath_if_missing() {
local file="$1" rpath="$2"
if ! otool -l "$file" | grep -A 2 LC_RPATH | grep -Fq "path $rpath "; then
install_name_tool -add_rpath "$rpath" "$file"
fi
}

patch_binary() {
local binary="$1"
while IFS= read -r dep; do
Expand All @@ -89,6 +109,14 @@ for dylib in "$FRAMEWORKS_DIR"/*.dylib; do
install_name_tool -id "@rpath/$(basename "$dylib")" "$dylib"
patch_binary "$dylib"
done
# The shipped binary (and each bundled dylib, for transitive deps) also
# searches Homebrew / system locations.
for rpath in "${WELL_KNOWN_RPATHS[@]}"; do
add_rpath_if_missing "$BINARY" "$rpath"
for dylib in "$FRAMEWORKS_DIR"/*.dylib; do
add_rpath_if_missing "$dylib" "$rpath"
done
done
chmod -w "$FRAMEWORKS_DIR"/*.dylib 2>/dev/null || true

echo "Staged macOS bundle in $APP_DIR"
6 changes: 6 additions & 0 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ fn graph_node_from_value(val: &Value) -> Option<GraphNode> {
}

pub fn open_connection(path: &str) -> Result<Connection<'_>> {
// NOTE: if this DB ever ran `LOAD <ext>`, ladybug WAL-logs it and
// replays the load on every open — a stale cached extension build
// (symbol skew vs liblbug) then fails EVERY open of that file, and no
// path/rpath change can fix it. The failing cache path is in the error;
// replacing it with a symbol-matching build unblocks the file. See
// README troubleshooting.
let db = Database::new(path, SystemConfig::default())
.with_context(|| format!("failed to open database {path}"))?;
// Leak the Database so the Connection can outlive this call, mirroring the
Expand Down
Loading
Loading