Rhumb: constant-bearing journeys through your app. Static journey mapping from frontend source.
Project description
Rhumb
Constant-bearing journeys through your app.
Static user journey graphs from frontend source — routes, navigation edges, and concrete paths. No runtime. No LLM in the extract path.
Live sample
Ran on the public TanStack Router kitchen-sink example:
Checked-in output: examples/tanstack-kitchen-sink/journeys.json (ends + real gaps from dynamic nav). How to reproduce: examples/tanstack-kitchen-sink/README.md.
Framework support
| Framework | Status | Notes |
|---|---|---|
| React Router | Supported | Config / JSX routes + Link / navigate / redirect |
| TanStack Router | Supported | FS + routeTree.gen.ts + virtual routes + nav |
| Expo Router | Supported | Filesystem app/ + Link / router.* |
| Vue Router | Coming soon | Stub registered — SFC + router TS planned |
| SvelteKit | Coming soon | Stub registered — src/routes + Svelte nav planned |
| Next.js | Coming soon | app/ / pages/ filesystem + Link / redirects |
| Remix | Coming soon | Filesystem routes + nav |
| Angular | Coming soon | TS Routes arrays |
| Vite + React (no router) | Coming soon | Thin / delegate when React Router present |
Detection already recognizes several of the “coming soon” stacks from package.json; journey extraction ships only for the Supported rows today.
Prerequisites
| Tool | Version | Why |
|---|---|---|
| Python | 3.11+ | Runtime (requires-python = ">=3.11") |
| Git | any recent | Clone for local development |
| uv | latest | Recommended — sync deps + run CLI without a global pip install |
Optional: Node.js only if you later use the TypeScript binder miss-path against a project that needs it; the default tree-sitter path does not require Node.
Install uv (if needed):
curl -LsSf https://astral.sh/uv/install.sh | sh
Install
pip install rhumb
# or
uv add rhumb
Then:
rhumb ./my-app --journey
from rhumb import extract_end_routes
print(extract_end_routes("./my-app"))
Pre-release / from git:
uv pip install git+https://github.com/satnam-sandhu/rhumb.git
# or
pip install git+https://github.com/satnam-sandhu/rhumb.git
Develop from clone
git clone https://github.com/satnam-sandhu/rhumb.git
cd rhumb
uv sync --group dev # creates .venv + installs deps
uv run rhumb ./path/to/my-app --journey
uv run pytest -q
CLI
# Journey JSON on stdout (pipe-friendly) — includes ends + gaps
rhumb ./my-app --journey
# Same JSON + end-grouped journeys / routes / edges / gaps on stderr
rhumb ./my-app --journey --verbose
# Write to a file
rhumb ./my-app --journey > journeys.json
# PostHog instrumentation (stub)
rhumb ./my-app --instrument
Stdout is only the JSON envelope when you use --journey (verbose detail goes to stderr).
Library (programmatic)
Preferred: ends + gaps envelope
extract_end_routes returns a JSON-serializable dict (not a string). Same shape as the CLI.
from rhumb import extract_end_routes
import json
result = extract_end_routes("./my-app")
for project in result["projects"]:
print(project["framework"], project["root"])
for end_route, paths in project["ends"].items():
print(end_route, len(paths))
for gap in project["gaps"]:
print("gap:", gap["message"])
# need a JSON string / HTTP body / file?
payload = json.dumps(result, indent=2)
json.dump(result, open("journeys.json", "w"), indent=2)
Reuse detection across calls
from rhumb import analyze, extract_end_routes
ctx = analyze("./my-app") # detect frameworks once
result = extract_end_routes(ctx)
Lower-level: full journey graphs
from rhumb import extract_journeys
for graph in extract_journeys("./my-app"):
print(graph.framework) # e.g. "tanstack-router"
print(len(graph.routes), "routes")
print(len(graph.edges), "edges")
print(len(graph.journeys), "journeys")
for gap in graph.gaps:
print("gap:", gap.message)
Per-framework extractors
from pathlib import Path
from rhumb import detect_all_frameworks, get_extractor
root = Path("./my-app")
for detection in detect_all_frameworks(root):
extractor = get_extractor(detection.framework)
if extractor is None:
continue
graph = extractor.extract(root, detection)
Output format
Always one envelope (single app or monorepo):
{
"projects": [
{
"framework": "tanstack-router",
"root": "shop",
"ends": {
"/checkout": [
["/search", "/product-details", "/cart", "/checkout"],
["/search", "/cart", "/checkout"]
],
"/profile": [
["/", "/profile"]
]
},
"gaps": [
{
"message": "nav target not in route tree: /legacy",
"source_file": "src/layout/AppSidebar.tsx",
"source_line": 48,
"confidence": "medium"
}
]
}
]
}
| Field | Meaning |
|---|---|
ends |
End screen → all inbound journey paths that terminate there |
gaps |
What we could not resolve (same rows -v prints) |
Monorepo = multiple objects inside projects.
CLI sample session
$ rhumb ./shop --journey
{
"projects": [
{
"framework": "react-router",
"root": "shop",
"ends": {
"/cart": [
["/search", "/product-details", "/cart"]
],
"/checkout": [
["/search", "/product-details", "/cart", "/checkout"],
["/search", "/cart", "/checkout"]
]
},
"gaps": []
}
]
}
With --verbose, stderr mirrors ends (grouped by destination) and lists gaps:
journeys: 3 → 2 ends
/cart:
/search → /product-details → /cart
/checkout:
/search → /product-details → /cart → /checkout
/search → /cart → /checkout
gaps: 1
- nav target not in route tree: /legacy (src/layout/AppSidebar.tsx:48)
What it cannot do (gaps & limits)
Rhumb prefers a flagged gap over a silent wrong edge. Incomplete ends + non-empty gaps is expected — not a crash.
Not covered today (typical gap sources):
| Limit | Why |
|---|---|
| Runtime-only routes | Paths built only after fetch / feature flags / CMS |
| Fully dynamic targets | navigate(variable), to={href} with no static string |
| Cross-package deep links | Nav in one package to a route owned by another (unless both scanned) |
| Non-JS / coming-soon frameworks | Detected sometimes; extract may be stub or skipped |
| Auth / permission branches | Who may open a screen is not modeled — only static graph edges |
| Full TypeScript project typecheck | Miss-path binder is opt-in; default is tree-sitter syntax only |
| Pixel / UX “screens” | We map URL routes, not visual screen names or analytics events |
Also not claimed:
- 100% of sidebar / marketing links will resolve
- Parity with the running app after every deploy (re-run extract after route changes)
- Replacement for E2E tests — this is static structure, not behavior
Read gaps[].message + source_file to see what to fix or ignore. Empty gaps means extract had nothing to flag — not that the product has no dark corners.
Core types
| Type | Role |
|---|---|
JourneyGraph |
routes, edges, gaps, journeys, meta |
RouteNode |
one URL path + source file |
NavEdge |
link / navigate / redirect transition |
JourneyPath |
ordered URL steps (/a → /b → /c) |
JourneyGap |
unresolved target or conflict |
from rhumb import JourneyGraph, RouteNode, NavEdge, Confidence
How it works
- Detect frontend framework(s) from
package.json - Extract routes (config AST, filesystem, or generated route tree)
- Scan navigation (
Link,navigate,redirect, …) via tree-sitter (+ TypeScript binder on miss) - Enumerate journey paths, group by end route, attach gaps → JSON envelope
See docs/journey-architecture.md for the plugin model.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rhumb-0.1.0.tar.gz.
File metadata
- Download URL: rhumb-0.1.0.tar.gz
- Upload date:
- Size: 42.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
966557b5c781e1de2de4eb48a732938d1ca3ecc6948c9ad3d104841ed061bb08
|
|
| MD5 |
420d05c78f3da4f3d38e4080fdf6a80c
|
|
| BLAKE2b-256 |
57d9f894bedbf65d15f2a28ad37403446a4539ec632a008ed4197a83c7e17195
|
File details
Details for the file rhumb-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rhumb-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b5d9489e47162c62c71c9bbcccf793fca21296e0349835dab6edc1ad007c048
|
|
| MD5 |
0f32efa7f301c75718afbb43eec5eba6
|
|
| BLAKE2b-256 |
7c13e7947187105e37c718a957b6a4fb797892693d035d3f528aa13b7a602fd6
|