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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ whygraph.toml
# Local scratch space for in-progress plan markdown (not version-controlled).
plans/

# Explorer playground: node deps, Vite output, and the built bundle packed into
# the wheel at build time (Docker COPY --from / hatch build hook) — never committed.
src/playground/node_modules/
src/playground/dist/
src/playground/*.tsbuildinfo
src/whygraph/serve/static/

# MkDocs build output (the site is built and deployed by CI, never committed).
site/

Expand Down
25 changes: 24 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ IMAGE ?= whygraph:dev
# Name of the long-running container started by `make image-debug`.
DEBUG_NAME ?= whygraph-debug

.PHONY: help sync test scan docs docs-build db db-down inspect image image-test image-inspect image-debug image-debug-down
.PHONY: help sync test scan node-check playground-deps playground playground-dev dev serve docs docs-build db db-down inspect image image-test image-inspect image-debug image-debug-down

help: ## List available targets
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN{FS=":.*?## "}{printf " %-10s %s\n", $$1, $$2}'
Expand All @@ -27,6 +27,29 @@ test: ## Run the test suite
scan: ## Re-scan this repo so WhyGraph is tested against itself
uv run whygraph scan

node-check: # (internal) assert Node >= 18 for the playground toolchain
@node -e 'process.exit(+process.versions.node.split(".")[0]>=18?0:1)' 2>/dev/null || { echo "error: the playground needs Node >= 18 (have $$(node -v 2>/dev/null || echo none)) - try 'nvm use 22'"; exit 1; }

playground-deps: node-check # (internal) install node_modules only if missing
@[ -d src/playground/node_modules ] || npm --prefix src/playground ci

playground: node-check ## Production build of the Explorer SPA into src/whygraph/serve/static
npm --prefix src/playground ci
npm --prefix src/playground run build

playground-dev: playground-deps ## Vite dev server with HMR (:5173, proxies /api to :8765) - pair with a backend or use 'make dev'
npm --prefix src/playground run dev

dev: playground-deps ## Dev loop: backend (:8765) + Vite HMR (:5173) together; open :5173; Ctrl-C stops both
@echo "backend -> http://localhost:8765 playground (HMR) -> http://localhost:5173 (open :5173)"
@uv run whygraph serve & \
api_pid=$$!; \
trap 'kill $$api_pid 2>/dev/null' EXIT INT TERM; \
npm --prefix src/playground run dev

serve: playground ## Production preview: build the SPA then serve it from whygraph serve (:8765)
uv run whygraph serve

docs: ## Serve the docs site locally with live reload (social cards skipped — no Cairo needed)
uv run mkdocs serve

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ whygraph init # bootstrap the WhyGraph DB + write config
whygraph scan # crawl history + refresh CodeGraph + LLM descriptions
whygraph init --agent claude # wire the MCP server into your editor
whygraph-mcp # sanity-check the server (Ctrl-C to exit)
whygraph serve # browse the graph, evidence + rationale in a local web panel
```

The only-Docker install needs nothing but Docker on the host — one command pulls the image and
Expand Down
22 changes: 22 additions & 0 deletions docker/whygraph/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@

# syntax=docker/dockerfile:1.7

# --- Playground build stage -----------------------------------------------
# Builds the Explorer SPA (src/playground/) into a static bundle that the final
# stage copies into the wheel. `--platform=$BUILDPLATFORM` pins this to the
# native builder: JS output is arch-independent, so we avoid QEMU emulating
# the npm build once per target arch (the multi-arch gotcha).
FROM --platform=$BUILDPLATFORM node:22-slim AS playground-build
WORKDIR /playground
COPY src/playground/package.json src/playground/package-lock.json ./
RUN npm ci
COPY src/playground/ ./
# Override the outDir to a stage-local `dist` (the checked-in vite.config.ts
# writes straight into the package for local `make playground`); the final stage
# COPYs it to the packaged location.
RUN npm run build -- --outDir dist --emptyOutDir

# --- Runtime stage --------------------------------------------------------
FROM python:3.12-slim

# Pinned via build arg so the version can be advanced without editing the
Expand Down Expand Up @@ -63,7 +79,13 @@ ENV WHYGRAPH_VERSION=${WHYGRAPH_VERSION}
# scripts land on PATH. Copy the build inputs hatchling needs and install.
WORKDIR /opt/whygraph
COPY pyproject.toml ./
COPY hatch_build.py ./
COPY src ./src
# The pre-built SPA bundle must exist under src/ BEFORE `pip install .` so
# hatchling packs it into the wheel. Copied from the playground stage, which
# means the hatch build hook (§9.3) finds static/ already populated and no-ops
# — the image build never runs npm.
COPY --from=playground-build /playground/dist ./src/whygraph/serve/static
RUN pip install --no-cache-dir .

# Image-only shortcut so `docker run IMAGE install | sh` works without an
Expand Down
13 changes: 13 additions & 0 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ whygraph scan --no-remote --skip-analyze
Descriptions backfill lazily later, so this is a fine way to get started quickly. See
[Scanning your repo](../guide/scanning.md) for what each phase does.

!!! tip "Prefer a visual view?"
Once you've scanned, `whygraph serve` opens a local, read-only web panel over the graph, evidence,
and rationale - browse it in the browser instead of (or alongside) your editor. See
[The Explorer playground](../guide/playground.md).

## 3. Wire your editor

Register the MCP server with your agent. For Claude Code:
Expand Down Expand Up @@ -86,4 +91,12 @@ function exists, and WhyGraph answers from history.

[:octicons-arrow-right-24: MCP usage](../guide/mcp-usage.md)

- :material-graph-outline:{ .lg .middle } __Explorer playground__

---

Browse the graph, evidence, and rationale in a local web panel.

[:octicons-arrow-right-24: Playground](../guide/playground.md)

</div>
8 changes: 8 additions & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ Start with the concepts, then dig into whichever piece you need.

[:octicons-arrow-right-24: MCP usage](mcp-usage.md)

- :material-graph-outline:{ .lg .middle } __Explorer playground__

---

A local, read-only web panel over the graph, evidence, and rationale.

[:octicons-arrow-right-24: Playground](playground.md)

</div>
124 changes: 124 additions & 0 deletions docs/guide/playground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# The Explorer playground

`whygraph serve` opens a local, **read-only** web panel onto everything WhyGraph and CodeGraph have
built for the current repo: browse the code graph, jump to any symbol, and read its rationale,
evidence, relationships, and history side by side. It's the same data the MCP tools serve - the web
API is just a second transport over the exact same functions, so the panel can never drift from what
your editor sees.

It runs from the **same Docker image** as every other command, as its own long-lived container - no
second image, no extra install.

## Run it

From a scanned repo:

```bash
whygraph serve
```

That starts the server in the foreground and prints a URL - open <http://localhost:8765>. `Ctrl-C`
stops it.

!!! note "Scan first"
The panel reads the CodeGraph index and the WhyGraph evidence database. Run
[`whygraph scan`](scanning.md) at least once before serving - otherwise there's no graph to draw,
and every symbol's rationale shows *"no evidence"* (see [Rationale on demand](#rationale-on-demand)).

### Lifecycle

On the Docker install the shim manages the container for you:

| Command | What it does |
|---|---|
| `whygraph serve` | Run in the foreground; `Ctrl-C` stops and removes the container. |
| `whygraph serve --detach` | Start in the background and return immediately. |
| `whygraph serve --logs` | Tail the detached server's logs. |
| `whygraph serve --stop` | Stop and remove the running server. |

The port is controlled by the `WHYGRAPH_PORT` environment variable (default `8765`):

```bash
WHYGRAPH_PORT=9000 whygraph serve --detach
```

!!! info "Localhost only"
The server is published to `127.0.0.1` only - it's a single-user local dev tool with **no auth**.
Nothing is exposed beyond your machine's loopback. The only action that writes anything is the
explicit **Generate rationale** button; everything else is read-only.

## What you see

<div class="grid cards" markdown>

- __Left - containment tree__

---

`directory → file → class → method`, lazy-loaded. Click a symbol to open it.

- __Center - graph__

---

The **overview** (directory super-nodes, colored by rationale coverage) is the landing view;
click a directory to expand it. Pick a symbol and the center switches to its **ego graph** -
what it calls, is called by, imports, and contains.

- __Right - detail panel__

---

Tabs for **Relationships**, **Rationale**, **Evidence**, and **History** on the selected symbol.

- __⌘K - search__

---

Find any symbol by name (disambiguated by file path), `Enter` to open it - recentering the
graph, opening the panel, and revealing it in the tree.

</div>

Every symbol reference in the panel - a search hit, a graph node, a relationship row - opens the same
way, so you can navigate the codebase by following edges.

### Rationale on demand

Generating a rationale card calls an LLM, so the panel never does it behind your back. The
**Rationale** tab shows a cached card if one exists; otherwise it shows a **Generate rationale**
button. Click it, watch the loading state, and the card renders - and is cached, exactly as if the
MCP tool had produced it.

The button is **disabled** when the symbol has no historical evidence to reason from - most commonly
because the repo hasn't been scanned, or the code isn't committed yet. Run `whygraph scan` and the
button lights up. The **Evidence** and **History** tabs never call an LLM, so they always work.

Generation uses the rationale LLM you configured in `whygraph.toml` - `[rationale] provider` and the
matching `[llm.<provider>]` (with its `api_key`), exactly as `whygraph init` sets it up and the same
provider the MCP tool uses. If you leave `api_key` unset, the provider's conventional env var (e.g.
`ANTHROPIC_API_KEY`) is the fallback; the Docker container reads your repo's `whygraph.toml` directly.
See [Configuration](../reference/configuration.md).

### Coverage heatmap

Because rationale cards are generated lazily, the overview colors each directory and file by how much
of it has been analyzed - a quick map of where you've already asked "why?" and where you haven't.

## Develop the UI

The panel's source lives at `src/playground/` (Vite + React + TypeScript). For a hot-reloading dev
loop - the backend on `:8765` and the Vite dev server on `:5173`, proxying the API across:

```bash
make dev # backend + Vite HMR together; Ctrl-C stops both; open :5173
```

Other targets: `make playground` builds the production bundle into the wheel's static directory, and
`make serve` builds it then serves it the way it ships. All need Node ≥ 18 (`nvm use 22`).

## Not in scope

The panel is deliberately narrow: **no chat/assistant tab** (that needs model config and an auth
story), **no writes** other than the Generate button, and **no remote hosting**. See the
[roadmap](../roadmap.md) for what's deferred.
25 changes: 24 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CLI reference

Every WhyGraph command and its flags. Run `whygraph <command> --help` to see the same text from your
own install. There are five commands.
own install. There are six commands.

```console
$ whygraph --help
Expand All @@ -10,6 +10,7 @@ Commands:
hooks Manage opt-in git hooks that auto-rescan on new commits.
init Initialize the WhyGraph database under .whygraph/whygraph.db.
scan Run the source crawlers, then describe each commit with the LLM.
serve Serve the WhyGraph Explorer panel for this repository.
version Print installed whygraph version.
```

Expand Down Expand Up @@ -65,6 +66,28 @@ picks up new commits and backfills what's missing.

See [Scanning your repo](../guide/scanning.md) for what each phase does.

## `whygraph serve`

Serve the read-only **Explorer playground** for this repository - a local web panel over the code
graph, evidence, and rationale. On the Docker install it runs as its own long-lived container, published
to `127.0.0.1` only. Run `whygraph scan` first so there's an index and evidence to show.

| Option | Default | Description |
|---|---|---|
| `--port` | `8765` | Port to bind. On the Docker install, set the port via the `WHYGRAPH_PORT` environment variable instead (the shim controls both the published and in-container port). |
| `--host` | `127.0.0.1` | Bind address. The Docker shim passes `0.0.0.0` for the container so the loopback port-forward can reach it; you rarely set this by hand. |

On the Docker install the shim also adds container-lifecycle verbs - these are **not** flags of the
Python command, they're handled on the host before the container starts:

| Command | What it does |
|---|---|
| `whygraph serve --detach` | Start in the background and return immediately. |
| `whygraph serve --logs` | Tail the detached server's logs. |
| `whygraph serve --stop` | Stop and remove the running server. |

See [The Explorer playground](../guide/playground.md) for the panel itself.

## `whygraph analyze`

Describe a single commit's diff with the configured LLM and **print** the result. Unlike `scan`, it
Expand Down
58 changes: 58 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Hatchling build hook that ships the Explorer SPA bundle in the wheel.

The React playground (``src/playground/``) builds to ``src/whygraph/serve/static/``, which
is gitignored and produced only at build time. This hook makes ``uv tool install``
/ ``pip install`` from a source tree build the bundle automatically, so the wheel
always carries a working SPA.

Behaviour, in order:

1. If ``src/whygraph/serve/static/index.html`` already exists, do nothing — the
Docker image ``COPY --from``s a pre-built bundle before ``pip install``, so the
hook must be a **no-op** there (the image build never runs npm).
2. Else, if ``src/playground/`` and ``npm`` are both present, run ``npm ci`` +
``npm run build`` to populate ``static/``.
3. Else (no bundle, no npm), warn and continue: the server still runs and its
``/`` route reports the UI is not built (see :mod:`whygraph.serve.app`).
"""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class PlaygroundBuildHook(BuildHookInterface):
"""Build the playground bundle into the package tree before packaging."""

PLUGIN_NAME = "custom"

def initialize(self, version: str, build_data: dict) -> None:
root = Path(self.root)
static = root / "src" / "whygraph" / "serve" / "static"
playground = root / "src" / "playground"

if (static / "index.html").is_file():
# Already built (Docker COPY --from, or a prior `make playground`).
return

if not (playground / "package.json").is_file():
self.app.display_warning(
"src/playground/ not found — packaging without the Explorer SPA bundle; "
"`whygraph serve` will report the UI is not built at /."
)
return

if shutil.which("npm") is None:
self.app.display_warning(
"npm not found — packaging without the Explorer SPA bundle; "
"install Node and rebuild, or run `make playground`."
)
return

self.app.display_info("building Explorer playground (npm ci && npm run build)…")
subprocess.run(["npm", "ci"], cwd=playground, check=True)
subprocess.run(["npm", "run", "build"], cwd=playground, check=True)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ nav:
- Scanning your repo: guide/scanning.md
- Wiring your editor: guide/editors.md
- Using WhyGraph (MCP): guide/mcp-usage.md
- Explorer playground: guide/playground.md
- Docker & Self-Hosting:
- deploy/index.md
- Run with Docker: deploy/docker.md
Expand Down
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description = "Rationale layer over CodeGraph — explains why code exists, not
requires-python = ">=3.11"
dependencies = [
"mcp[cli]>=1.2",
"fastapi>=0.110",
"uvicorn>=0.27",
"click>=8.1",
"rich>=13",
"scikit-learn>=1.3",
Expand All @@ -27,6 +29,14 @@ build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/whygraph"]
# The built SPA bundle is gitignored; `artifacts` force-includes it so the wheel
# carries it (populated by the build hook or the Docker COPY --from).
artifacts = ["src/whygraph/serve/static/**"]

[tool.hatch.build.hooks.custom]
# Runs hatch_build.py:PlaygroundBuildHook — builds src/playground/ into serve/static/
# at wheel-build time (no-op if the bundle is already present).
path = "hatch_build.py"

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
Loading
Loading