# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

nf-metro generates metro-map-style SVG diagrams from Mermaid graph definitions augmented with `%%metro` directives. It is designed for visualizing bioinformatics pipeline workflows (e.g., nf-core pipelines) as transit-style maps where each analysis route is a colored "metro line."

## Build & Development

```bash
# Install in development mode (uses hatchling build system)
pip install -e ".[dev]"

# Run CLI
nf-metro render examples/rnaseq_sections.mmd -o output.svg
nf-metro validate examples/rnaseq_sections.mmd
nf-metro info examples/rnaseq_sections.mmd

# Run via module
python -m nf_metro

# Run all tests
pytest

# Run a single test
pytest tests/test_parser.py::test_parse_title

# Lint
ruff check src/ tests/
```

Dependencies: click, drawsvg, networkx, pillow. Dev: pytest, ruff.

## Documentation site

Content and tooling are separated:

- **`docs/`** — hand-written Markdown only (plus `docs/assets/` media), as it
  was under MkDocs. This is the source of truth for page content.
- **`website/`** — the **Astro / Starlight** site (it replaced MkDocs-Material):
  theme/layout overrides in `website/src/components/`, styles in
  `website/src/styles/custom.css`, nav + config in `website/astro.config.mjs`.
  It loads the Markdown via a symlink: `website/src/content/docs -> ../../docs`.

The fastest way to preview the site locally is the convenience script, which
generates the git-ignored dynamic content and then starts the dev server in one
step (serves at http://localhost:4321/nf-metro/):

```bash
scripts/serve_docs.sh              # generate content if missing, then dev server
scripts/serve_docs.sh --branch X   # serve branch X: worktree + its nf_metro + renders
scripts/serve_docs.sh --rebuild    # force-refresh gallery + playground content
scripts/serve_docs.sh --skip-content  # fastest; assumes content already generated
scripts/serve_docs.sh --preview    # production build + preview instead of dev
scripts/serve_docs.sh --help       # all flags; `-- <args>` pass through to astro
```

It needs the `nf_metro` package importable (e.g. `source ~/.local/bin/mm-activate nf-metro`)
to build the gallery; without it the server still starts but those pages are empty.

The underlying npm commands, if you prefer to drive Astro directly:

```bash
cd website
npm install
npm run dev      # serves at http://localhost:4321/nf-metro/
npm run build    # static output to website/dist/
```

Maps are embedded on a page with the **`<Metro>`** component
(`website/src/components/Metro.astro`): `<Metro src="examples/guide/01_minimal.mmd" />`
names a committed `.mmd` by repo-relative path and renders it live at build
time — source block, CLI command, and the map as three toggle sections (see
[contributing.mdx](docs/contributing.mdx) for the `purpose`/flag options). The
SVG is produced by a Vite plugin (`website/src/lib/render-metro.mjs`) that
resolves `.mmd?metro` imports through the `nf-metro` CLI and caches them under
`website/.metro-cache/` (git-ignored); importing the SVG (rather than rendering
to a runtime `set:html` string) is what keeps its `light-dark()` chrome and
embedded logo through the production build. Plain ` ```metro ` fences are left
alone — nothing intercepts them, so they stay highlight-only snippets.

The **Gallery** and **nf-core pipelines** pages are generated by
`python scripts/build_gallery.py` (writes Markdown emitting `<Metro>` tags into
`docs/{gallery,pipelines}/`, git-ignored). The same script also renders the
example SVG corpus into `docs/assets/renders/` (git-ignored), but that corpus
now only feeds the **CI render-diff** (`pr-renders.yml` + `build_render_diff.py`)
and the metrics scorecard — no page references it; pages render live via
`<Metro>`. The interactive **playground** is static files under
`website/public/playground/`. Deployment is `.github/workflows/docs.yml`
(Astro build -> `gh-pages` branch, preserving PR render previews under `_pr/`).

## Architecture

The pipeline is: **Parse** -> **Layout** -> **Render**. This section is a map; each
subsystem has a reader-friendly deep dive under `docs/dev/` - read the relevant one
before changing that subsystem rather than reverse-engineering it from the code.

| Subsystem | Dir                            | Role                                                                                                                                                                                                                                                                                                                                                                                                                                | Deep dive                                                                                          |
| --------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Parser    | `src/nf_metro/parser/`         | `.mmd` text -> `MetroGraph` via a Lark grammar, `%%metro` directives, post-parse rewrites (`resolve.py`) that insert ports/junctions. `model.py` holds the central `MetroGraph` dataclass.                                                                                                                                                                                                                                          | `docs/dev/parser.mdx`                                                                              |
| Layout    | `src/nf_metro/layout/`         | Section-first pipeline of 40+ numbered stages (`engine.py` orchestrates; phase impls in `phases/`, re-exported from `engine`). Also `auto_layout.py` (infers grid/direction/ports), `section_placement.py`, `ordering.py`, `layers.py`, `rail_mode.py` (opt-in interchange idiom), `geometry.py`, `constants.py`.                                                                                                                   | `docs/dev/layout_pipeline.mdx`; per-stage pre/post/invariants in `src/nf_metro/layout/CONTRACT.md` |
| Routing   | `src/nf_metro/layout/routing/` | Edge routing (horizontal runs + 45-degree diagonals; L-shaped inter-section). `core.py` is a thin first-match dispatcher over handler families in sibling modules; `invariants.py` checks output every render.                                                                                                                                                                                                                      | `docs/dev/routing.mdx`; inter-section dispatch table in `docs/dev/inter_section_dispatch.mdx`      |
| Render    | `src/nf_metro/render/`         | Laid-out `MetroGraph` -> SVG via `drawsvg` (`svg.py`), plus `animate.py`, `bridges.py`, `html.py`, `manifest.py`, `legend.py`, `icons.py`, `constants.py`.                                                                                                                                                                                                                                                                          | `docs/dev/render.md`                                                                               |
| Themes    | `src/nf_metro/themes/`         | Brand identity × display mode as orthogonal axes. Per-brand `Theme` pairs (`nfcore.py`, `seqera.py`, each a light + dark variant); `light.py` is the transparent embed theme. `THEME_MODES` maps brand→{light,dark}; `resolve_theme(brand, mode)` combines them. Chrome colours emit as `light-dark()` so one render adapts to the viewer's `color-scheme`. New brand: add light+dark `Theme`s, register in `THEME_MODES`/`THEMES`. | -                                                                                                  |

## Input Format

`.mmd` files use a subset of Mermaid `graph LR` syntax with `%%metro` directive extensions:

```
%%metro title: Pipeline Name
%%metro style: nfcore
%%metro mode: light
%%metro line: line_id | Display Name | #hexcolor | style
%%metro line_order: span
%%metro compact_offsets: true
%%metro legend_min_height: 72
%%metro grid: section_id | col,row

graph LR
    subgraph section_id [Section Name]
        %%metro entry: left | line1, line2
        %%metro exit: right | line1, line2
        node_id[Label]
        node_id -->|line_id| other_node
    end
    %% Inter-section edges outside subgraphs
    node_a -->|line_id| node_b
```

Edges support comma-separated line IDs: `a -->|line1,line2,line3| b` creates one edge per line.

Lines support an optional style (4th field): `solid` (default), `dashed`, or `dotted`. Dashed/dotted lines render with SVG `stroke-dasharray` on both edge paths and legend swatches.

`%%metro style:` picks the brand (`nfcore`, `seqera`; `dark` aliases nfcore) and `%%metro mode:` picks `light`/`dark` - independent axes (CLI: `--theme`, `--mode`). SVG output carries both palettes via `light-dark()` and adapts to the viewer's `color-scheme`, so mode only needs setting to bake a concrete PNG; an unset mode falls to a single global default.

## Key Design Decisions

- Stations are mutable dataclasses; layout phases write coordinates directly onto `Station.x`/`.y` fields.
- Port stations (`is_port=True`) participate in layout but are invisible during rendering.
- Layout uses networkx only for DAG operations (topological sort); all coordinate computation is custom.
- Auto-layout (`auto_layout.py`) infers everything from the section DAG, so most `.mmd` files need no `%%metro grid:` directives. Explicit directives override inferred values.

## Station-as-Elbow Constraint (CRITICAL)

**NEVER position a perpendicular port at the same coordinate as an internal station.** This is validated by `check_station_as_elbow` in `tests/layout_validator.py` (10px tolerance).

- TOP/BOTTOM ports on LR/RL sections must NOT share X with any internal station.
- LEFT/RIGHT ports on TB sections must NOT share Y with any internal station.

When fixing routing or alignment issues, do NOT "solve" a kink by moving a port to match a station's coordinate. That creates a station-as-elbow violation where the line visually passes through the station marker. Instead, accept small offsets between ports and stations and handle them via routing (near-vertical drops, gentle curves, etc.).

## Visual Review

**Primary method**: Push to a PR. The CI workflow (`.github/workflows/pr-renders.yml`) renders all gallery examples on both the PR branch and base, generates a before/after visual diff, and posts a preview link:

```
https://seqeralabs.github.io/nf-metro/_pr/<PR_NUMBER>/
```

This is the authoritative visual review and should be used for all layout or rendering changes.

**Quick local render** (for fast iteration before pushing):

```bash
source ~/.local/bin/mm-activate nf-metro

# Render SVG. --no-chrome-css bakes concrete colors so cairosvg can rasterize it;
# without it cairosvg aborts on the var() chrome custom properties.
python -m nf_metro render examples/rnaseq_sections.mmd -o /tmp/rnaseq_sections.svg --x-spacing 60 --y-spacing 40 --no-chrome-css

# Convert SVG to PNG via cairosvg (scale=2 for retina)
python -c "import cairosvg; cairosvg.svg2png(url='/tmp/rnaseq_sections.svg', write_to='/tmp/rnaseq_sections.png', scale=2)"

# Open it
open /tmp/rnaseq_sections.png
```

The `nf-metro` micromamba environment has the project installed in editable mode along with cairosvg for PNG conversion.

## Test Fixtures & Topology Stress Tests

- Test fixtures: `tests/fixtures/`
- Example pipelines: `examples/` (including `rnaseq_sections.mmd` with manual grid and `rnaseq_auto.mmd` with fully inferred layout)
- Topology stress tests: `examples/topologies/*.mmd` - 38 fixtures covering fan-out, fan-in, diamonds, folds, mixed port sides, etc. See `examples/topologies/README.md` for the documented inventory and known issues (note: the README covers ~33 fixtures; a handful of newer regression fixtures are present but not yet catalogued there).
- `tests/layout_validator.py` - Programmatic layout checks (section overlap, station containment, port positioning, edge waypoints).
- `tests/test_topology_validation.py` - Parametrized tests running all validator checks against every topology fixture.
- `scripts/render_topologies.py` - Batch render all fixtures to `/tmp/nf_metro_topology_renders/`.
