# Bugs

- [X] `CoreGraph.remove_node` corrupts indices (graph.rs:99-113)
      `object_map.remove()` shifts indices in the PyIndexSet, but petgraph's DiGraphMap
      and the data_map/size_map still reference old NodeIndex values. Any removal of a
      non-terminal node silently corrupts the graph.

- [X] `PointDistance` implementation for `PlacedRectangularNode` is wrong (geometry.rs:298-306)
      When the point is inside the rectangle it returns `(x - x2).pow(2)` instead of 0.
      The `y > y2` branch (line 303) also returns `(x2 - x).pow(2) + (y2 - y).pow(2)`
      when x is between x1 and x2 — should be just `(y - y2).pow(2)`.

- [X] `EdgeRouter.object_map` leaks on removal (edge_router.rs:98, 108)
      TODO comments in the code acknowledge this: the object map is never cleaned up
      when nodes/edges are removed, growing unboundedly with mutations.

# Architecture

- [X] Decompose `ConsoleGraph` (~977 lines, console_graph.py)
      This class is a god object handling: graph management, layout orchestration, zoom
      computation, edge routing coordination, buffer management, incremental updates,
      coordinate transforms, and Rich rendering. Should be split into focused components.

- [X] Eliminate duplicated graph representation
      Graph data is triple-bookkept: NetworkX → CoreGraph → EdgeRouter. The CoreGraph
      wraps DiGraphMap<NodeIndex, ()> with data in separate HashMaps, and EdgeRouter
      tracks edges again in existing_edges. This is fragile during mutations.

- [ ] Replace `$properties` key injection with proper internal state (console_graph.py:291, 359, 469)
      Node/edge data dicts are mutated in-place with a `$properties` key, mixing internal
      state into user-supplied data. If a user uses this key or iterates their data, it
      breaks silently. Use a separate internal mapping instead.

# Routing Improvements

## High Impact

- [X] Insert more intermediate grid lines between distant nodes (grid.rs)
      Midpoint always included; uniform interior fill (MIN_SPACING=4, up to 8 extra lines)
      added for gaps with ≥4-unit interior space. Turn cost raised to 3×base_cost to prevent
      zigzag detours through the denser grid.

- [ ] Add cost term for proximity to nodes (route_single.rs)
      Edges routed close to node borders look cramped. Add a distance-to-node cost so edges
      prefer to keep clearance. Currently masking blocks ±1 unit but there's no soft penalty
      for being near a node.
      ⚠ Perf: a naive approach iterates all nodes per A* step — O(N) per expansion, which
      dominates routing time. Instead, precompute a node distance field on the grid once
      before routing starts: for each grid point, store the minimum Manhattan distance to
      any node boundary. This is a one-time O(G × N) cost (G = grid points, N = nodes)
      that can be further reduced to O(G) with a BFS flood-fill from all node boundaries.
      The per-step A* lookup then becomes a single array index — O(1).

- [ ] Add cost term for proximity to other edges (route_single.rs)
      Parallel edges that share the same corridor look cluttered. A proximity penalty would
      spread edges across available routing channels.
      ⚠ Perf: tracking exact edge positions and querying neighbors per A* step is expensive.
      Better approach: reuse the existing raw_usage array. After each edge is routed, its
      segments already increment raw_usage. Extend this with an "adjacency usage" array:
      when a segment's usage increases, also add a fractional cost (e.g. 0.3) to its
      neighboring segments. This spreads congestion pressure to adjacent corridors at the
      cost of a constant-factor increase in usage bookkeeping — no per-step spatial queries.
      The A* cost function already reads raw_cost per segment, so no inner-loop change needed.

- [ ] Add cost term for crossing corners (route_single.rs)
      Crossing corners should be avoided unless edges share a node. Corners where multiple
      edges cross look messy and are hard to read.
      ⚠ Perf: minimal. Corner usage is already tracked in raw_corner_usage and checked on
      every turn. This just adjusts the existing cost formula — O(1) per turn, and turns
      are infrequent relative to straight moves. No new data structures needed.

- [ ] Graceful degradation when A* fails to find a path (route_single.rs:104-165)
      Fallback L-shaped paths may pass through nodes, producing visually broken edges.
      Try more fallback shapes (Z-shaped, S-shaped) or temporarily relax masking constraints
      rather than crashing.
      ⚠ Perf: only triggered on A* failure, so zero cost on the happy path. The fallback
      itself is cheap (constant number of candidate paths to check).

## Medium Impact

- [X] Add history cost decay (ripup.rs)
      History costs now decay by 0.85× each iteration before adding new overflow, preventing
      runaway accumulation from biasing routes toward suboptimal detours.

- [ ] Add crossing penalty for parallel overlap >1 unit (route_single.rs)
      Crossing lines for more than 1 unit should have an additional penalty, unless the
      edges share a node. Brief crossings are acceptable, long parallel overlaps are not.
      ⚠ Perf: overlap length is naturally captured by summing raw_cost along a segment run.
      If using the adjacency-usage approach from the proximity item above, this comes for
      free — sustained parallel routing accumulates cost over multiple consecutive segments.
      No additional per-step computation needed beyond what raw_cost already provides.

- [ ] Multiple intermediate routing points based on edge direction
      Edges with complex paths could benefit from intermediate waypoints computed from
      the general direction of the edge, giving the router more guidance.
      ⚠ Perf: splits one A* call into multiple shorter ones (start→wp1→wp2→end). Each
      sub-path searches a smaller space, so total work may actually decrease if waypoints
      are well-chosen. Waypoint computation itself is O(1) per edge (midpoint + offset).

- [ ] When using the same line style, connect to the closest point on existing edge
      Edges with identical styles between the same nodes could merge into a shared segment
      and split at the appropriate point, reducing visual clutter.
      ⚠ Perf: requires checking existing routed paths for style-compatible edges during
      routing. Could maintain a per-style spatial index of routed segments. Lookup is
      O(log S) per edge, but adds implementation complexity.

- [ ] Reduce costs automatically on very dense graphs
      Dense graphs overwhelm the router with congestion. Detect density and relax penalties
      (lower lambda, increase capacity) so edges can share corridors gracefully.
      ⚠ Perf: density detection is O(1) (node count / grid area). Adjusting parameters
      is free. May actually improve perf by reducing rip-up iterations on dense graphs.

## Lower Impact

- [ ] Make routing parameters configurable (edge_router.rs:120, 184-189)
      max_iterations=10, lambda=2.0, mu=0.5, corner_lambda=5.0, capacity=1,
      corner_capacity=1 are all hard-coded with no way for users to tune them.

- [ ] Increase max iterations or make convergence-based (edge_router.rs)
      Complex graphs may not converge in 10 iterations. Make configurable or continue
      until overflow stops decreasing.

- [ ] T-junctions when rendering line buffers with different styles
      When edges with different styles meet, render a proper T-junction character instead
      of overlapping characters.

- [ ] Validate duplicate node insertion (edge_router.rs:75)
      `add_node()` doesn't check for duplicates. Could cause issues in existing_edges
      tracking.

- [ ] Fix port name collision with node names (node_anchors.py:15)
      If a port name matches a node name, anchor resolution may fail.

# Layout

- [X] Rewrite `brandes_koepf_coordinates` (sugiyama.rs:109)
      Now implements proper Brandes-Köpf: Type 1 conflict marking, 4-direction vertical
      alignment, horizontal compaction, median-of-four balance. Dummy nodes are inserted
      for edges spanning multiple layers so long edges have intermediate anchors.
- [X] Sugiyama better alignment of nodes
      Achieved via the Brandes-Köpf rewrite above — chains and long edges now align
      vertically, and siblings are spaced according to subtree widths.
- [X] Barycenter ordering: add upward pass and iteration (sugiyama.rs:167)
      Now alternates down/up sweeps with O(1) position lookup, tracks best ordering by
      crossing count across sweeps, and stops early when a full round is stable.
- [ ] Tree layout
- [ ] Git Branch layout

# Rust Code Quality

- [ ] Replace `unwrap()` calls with proper error propagation
      Multiple `.unwrap()` on `into_py_any()` (graph.rs:64, 89, 243, 269) and panics in
      rstar::Point impl (geometry.rs:207) would crash the Python process rather than
      raising catchable exceptions.
- [ ] Move tracing dependencies behind a feature flag (Cargo.toml:18-19)
      tracing, tracing-flame, tracing-subscriber are non-optional dependencies adding
      binary size and compile time even when tracing is disabled.
- [X] Fix direct access to `graph.size_map` from sugiyama.rs (sugiyama.rs:123)
      Added `CoreGraph::size_by_index(NodeIndex) -> Option<&Size>`; size_map is now private.
      sugiyama.rs and force_directed.rs both go through the accessor.

# Testing

- [ ] Add unit tests for Rust routing/layout algorithms
      A*, rip-up-and-reroute, Sugiyama layering, and barycenter ordering have no
      targeted Rust-level tests. Only ~10 Python test files for ~8,200 LOC.
- [ ] Add benchmarks
- [ ] Add benchmark comparison to PRs
- [ ] Strict typechecking

# Performance

## Rust

- [ ] Force-directed layout: O(N²) repulsive force without spatial indexing (force_directed.rs:53-87)
      Nested loop over all node pairs per iteration. For 1000 nodes × 50 iterations = ~50M
      force calculations. Use quadtree/Barnes-Hut to reduce to O(N log N). Also, displacements
      HashMap is reallocated every iteration (line 54) instead of being cleared and reused.

- [ ] Sugiyama: linear search in barycenter ordering (sugiyama.rs:185)
      `layers[i-1].iter().position(|&x| x == ...)` scans the previous layer for every neighbor
      of every node. Build a position lookup HashMap before the inner loop for O(1) lookups.

- [ ] Edge router: excessive cloning in routing loops (edge_router.rs:131-190)
      `self.placed_nodes.values().cloned().collect()` clones all placed nodes into a new Vec
      multiple times. `edges.clone()` at line 190 copies full edge list each rip-up iteration.
      Pass references where possible.

- [ ] Sugiyama: full DiGraphMap clone (sugiyama.rs:39)
      `graph.graph.clone()` clones entire graph. Could borrow instead.

- [ ] A* routing: no HashMap pre-allocation (astar.rs:74-76)
      `g_score` and `came_from` HashMaps created without `with_capacity()`.

- [ ] Geometry: `.pow(2)` instead of `x * x` in hot path (geometry.rs:285-305)

- [ ] Routing types: getter clones full Vec on every access (types.rs:45-46, 64-66)

## Python

- [ ] Buffer renderer: sort on every insertion (buffer_renderer.py:34)
      `sorted(buffers_by_row[...] + [buffer])` re-sorts on every buffer added.
      Use `bisect.insort` or collect-then-sort.

- [ ] Node density: O(N²) in Python (console_graph.py:729-755)
      For each node, iterates through all other nodes. Push to Rust or use spatial bucketing.

- [ ] Four passes for min/max positions (console_graph.py:791-794)
      Four separate iterations over `node_positions.values()` for min/max x/y.
      Compute all four in a single pass.

- [ ] Path rasterizer: O(N) list membership check (edge_rendering/path_rasterizer.py:177)
      `if point.x not in y_to_x[point.y]` where `y_to_x[y]` is a list. Use a set.

- [ ] Port padding: repeated port filtering (node_rasterizer.py:41-116)
      Four nearly-identical list comprehensions each with nested filters over ports by magnet
      side. Pre-group ports by side once.

- [ ] EdgeProperties recreation in layout loop (console_graph.py:758-772)
      `EdgeProperties.from_data_dict()` called for every edge during layout but properties
      are stable. Cache them.

- [ ] Missing `@cached_property` on edge bounds (edge_routing/edge.py:43-49)
      `min_bound` and `max_bound` iterate `distinct_points` every access. Should be cached
      like `distinct_points` already is.

- [ ] Width recomputation from segments (node_rendering/buffers.py:43,95; shapes/box.py:44)
      `sum(segment.cell_length ...)` recomputed per strip in multiple places. Compute once
      during strip creation.

- [ ] Sorted ports called twice (node_rendering/buffers.py:212,229)
      Same `sorted()` on ports collection in sequential methods. Sort once and reuse.

# Other

- [ ] Remove hard-coded magic numbers
      console_graph.py:677 (+10 width, +5 height padding), console_graph.py:702
      (+0.25 offset to avoid rounding collisions). These should be configurable or
      the underlying rounding issue should be properly solved.
- [ ] Other shapes
- [ ] Overlay edges in rendering
- [ ] Remove networkx dependency
