Skip to main content

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:

rhumb CLI on TanStack kitchen-sink

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

  1. Detect frontend framework(s) from package.json
  2. Extract routes (config AST, filesystem, or generated route tree)
  3. Scan navigation (Link, navigate, redirect, …) via tree-sitter (+ TypeScript binder on miss)
  4. Enumerate journey paths, group by end route, attach gaps → JSON envelope

See docs/journey-architecture.md for the plugin model.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rhumb-0.1.0.tar.gz (42.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rhumb-0.1.0-py3-none-any.whl (51.2 kB view details)

Uploaded Python 3

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

Hashes for rhumb-0.1.0.tar.gz
Algorithm Hash digest
SHA256 966557b5c781e1de2de4eb48a732938d1ca3ecc6948c9ad3d104841ed061bb08
MD5 420d05c78f3da4f3d38e4080fdf6a80c
BLAKE2b-256 57d9f894bedbf65d15f2a28ad37403446a4539ec632a008ed4197a83c7e17195

See more details on using hashes here.

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

Hashes for rhumb-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b5d9489e47162c62c71c9bbcccf793fca21296e0349835dab6edc1ad007c048
MD5 0f32efa7f301c75718afbb43eec5eba6
BLAKE2b-256 7c13e7947187105e37c718a957b6a4fb797892693d035d3f528aa13b7a602fd6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page