# CLAUDE.md Guidance for Claude Code working in the `freeroute` repository. ## What this is A native Python PCB autorouter: a Java-free reimplementation of the [FreeRouting](https://github.com/freerouting/freerouting) engine. Specctra DSN in, Specctra SES out, no JVM anywhere. Published on PyPI as `freeroute` (GPL-3.0-or-later, CalVer). ## Commands ```bash uv sync uv run pytest -m "not oracle" # the fast suite (this is what you run) uv run pytest -m oracle # differential tests against the real JAR (needs Java) uv run ruff check src/ tests/ uv run freeroute board.dsn --engine room --pack -do board.ses ``` ## The three non-negotiable invariants Break any of these and the change is wrong, regardless of how good the routing looks: 1. **DRC-clean output.** For every different-net item pair, the exact `TileShape` clearance intersection must be empty (`ShapeSearchTree.has_violation()` returns `None`). The `exact` and `room` engines are clean *by construction*: they drop a net rather than emit a violation. A router that packs traces densely and quietly shorts two nets is the failure mode this codebase exists to prevent. It has already happened once (a multi-pin wire/owner desync in shove recovery reported `drc_clean=True` over a real short) and was caught only by an independent adversarial check. 2. **Exact integer arithmetic in every geometric decision.** No floats in clearance or intersection tests. Points, vectors, lines and tiles use Python's unbounded `int` and projective rationals, mirroring FreeRouting's use of `BigInteger`. `FloatPoint` exists but is quarantined to distances, rounding and heuristics. Where an irrational is unavoidable (the `sqrt(2)` in the 45° octagon cover), it is bounded to an exact integer **once** at tile construction (`math.isqrt`, rounded outward so the cover is a provable superset) and never appears in a pairwise test. 3. **Determinism.** Same input, byte-identical output, every run. Sorted iteration, fixed candidate ladders, no RNG. Rip-up and shove are bounded (pass limits, rip caps, escalating penalties) so they provably terminate. ## Architecture ``` dsn/ Specctra DSN parser (tokenizer -> sexp -> typed DsnBoard) ses/ Specctra SES writer (the routed session file) geometry/ exact planar geometry: point, vector, line, box, simplex, polygon, polyline (trace copper), octagon (45 degree copper) board/ BasicBoard, items, nets, clearance, build_board (DSN -> board), search_tree (ShapeSearchTree: the exact clearance oracle) route/ grid_router | exact_router | room_router, shove, pipeline, cli ``` ### The three engines (`--engine`) - **`grid`** (default) is the MVP maze router over a uniform occupancy grid. Highest raw connectivity, but grid-quantised, so its clearance is approximate and its traces are staircases. - **`exact`** routes orthogonally and verifies every trace against exact geometry. DRC-clean where it succeeds. Supports `--shove` and `--diagonal`. - **`room`** is the continuous expansion-room router: free space is exactly decomposed into convex rooms joined by doors, so it routes off-grid channels the grid literally cannot see. DRC-clean. Supports `--pack` and `--shove`. The engine/option matrix lives in `freeroute.cli.ENGINE_OPTIONS` and is importable, so consumers can assert against it. **An unsupported combination is a hard error (exit 2), never a silent no-op.** Downstream code (`mckicad`) reads this matrix to avoid sending an invalid pair. ## `reference/` is GPL Java. Never commit it, never ship it. `reference/freerouting` is a clone of the upstream Java source, used only as a porting reference. It is gitignored and excluded from the sdist. Study it and reimplement; do not paste it. Before any release, confirm the built sdist contains zero `.java` / `.jar` / `.class` / `.flex` files and no `reference/` directory. Redistributing upstream source inside this package would be a licensing problem. ## The oracle: validate behaviour, not vibes FreeRouting ships **no unit tests for its geometry or router**, so there are no value-level oracles to port against. Instead, `tests/oracle.py` runs the real `freerouting.jar` as a differential reference: route the same DSN with both, and compare connectivity (`routed_net_set`). Oracle tests are marked `@pytest.mark.oracle` and skip cleanly without Java, so the normal suite stays JVM-free. When you cannot get an oracle, use **invariants and falsifiable fixtures**: - `split_to_convex`: tiles union to the polygon, interiors disjoint, a point is inside the polygon iff inside exactly one tile. - Routing: endpoints exactly on pads, traces on valid layers, inside the outline, no different-net crossing, vias at real layer transitions. ## Hard-won lessons (do not relearn these) - **KiCad emits net names like `/*52`.** Specctra's `SpecCharASCII` includes `/` and `*`, so an unclosed `/*` is a *name*, not an unterminated block comment. The tokenizer must fall through to a token when no closing `*/` exists. - **Exact clearance is load-bearing, not decoration.** A 45° trace whose *bounding box* hits an obstacle can still be legal, because its true octagon copper misses. Coarse box checks reject legal routes. See `test_exact_clearance_is_load_bearing`. - **Shove is not a densifier.** It relocates a blocker into space that already exists; it cannot compress copper. Proven with an order-independent fixture (`true_density.dsn`) plus a parameter sweep: shove flips from "recovers" to "powerless" exactly when detour headroom runs out. The real densifier is **lane/gate packing** (`--pack`). - **A density claim needs a falsifiable fixture.** "Plain routing drops a net" is worthless if the drop is an *ordering artifact*. The bar: the drop must hold under **every** net ordering (enumerate the permutations), and the pack/fail boundary must sit at the true geometric feasibility width. This bar killed one over-claim (`rooms_shove_channel`) and validated a real win (`channel_pack.dsn`). - **KiCad's `LoadBoard` is headless-safe; `ImportSpecctraSES` is not.** Use `LoadBoard` to verify KiCad accepts routed output without needing a display. ## Honest scope Alpha. It routes real KiCad boards, multi-layer, with vias, rip-up, shove, channel packing, 45° shortening, and DRC-clean output. It is **not** at FreeRouting/JAR density parity on dense commercial boards: the maze search is orthogonal (45° is a recovery pass, not diagonal-native search), there is no fanout pass and no post-route optimiser, and on a crowded board it will complete fewer nets than the JAR because it drops rather than violates. Say this plainly; do not oversell it in code comments, the README, or release notes. ## Conventions `uv` for everything, src-layout, ruff, pytest. Small single-purpose modules. Commit in logical chunks with professional messages and no AI attribution. Cite the upstream Java file in a module docstring when porting one.