lini 0.14.0

Pretty diagrams from plain text, with fine-grained control. Compiles to clean, themeable SVG.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
# Routing v2 — implementation log

> The v2 rewrite's plan and its **Execution log** — all seven stages landed
> (2026-07). Kept as the record of what was built and *why*: the log's dated
> entries hold every design decision the plan didn't anticipate, and the
> open-bug diagnoses future sessions start from — most importantly the
> **bug batch** entry on links_hard's four strays (estimate-overshot
> admission; needs connection-feasible pricing + coupled cross-axis
> placement bounds). Re-orient with `ROUTING.md` (the contract, the source
> of truth) first, then the Execution log at the bottom, newest first. If a
> rendered result looks stale: `cargo clean -p lini`.

**Goal:** Replace the v1 guess-then-repair router with the ROUTING.md v2
contract: a sequential weighted channel router with constructive placement,
a strategy seam (`orthogonal` / `straight`), and sequence messages migrated
onto `straight`.

**Architecture:** Six decide-once steps (keep-outs & worlds → channels →
requests → weighted search → placement → geometry). No audit, no repair, no
gap growth, no port pre-pinning: ports *are* end-run ordinates. Cost =
`length + 2·clearance·turns + 4·clearance·crossings`, links routed in
declaration order.

**Tech stack:** Rust, `insta` snapshots, `resvg` for visual verification
during development only (never in CI tests).

---

## Global constraints

Every stage inherits these; they restate AGENTS.md + the spec where routing
work is most likely to violate them.

- **No `unsafe`.** No panics on user input — impossible layouts are strays.
- **One mechanism per problem; no parallel implementations.** The v1 router is
  *deleted*, never kept alongside. If a stage needs behavior v1 had, port the
  code, don't re-derive a sibling.
- **Code reads as designed-for-v2-day-one.** No compatibility shims, no
  `_v2` suffixes, no dead flags. Comments only for non-obvious *why*.
- **Modularity:** one concept per file, split past ~500 LOC.
- **Determinism:** `f64::total_cmp` for every float comparison that orders
  anything; iterate `BTreeMap`/sorted vecs, never `HashMap` order; no
  wall-clock, no randomness.
- **Cost constants** live in exactly one place (`ortho/cost.rs`):
  `TURN_COST = 2.0 * clearance`, `CROSS_COST = 4.0 * clearance`, minimum pitch
  `= clearance / 2`. Put a comment at the definition: a per-diagram override
  (e.g. `link-crossing-cost:`) is a plausible future property — keep the
  constants threaded, not inlined.
- **Tests:** `insta` for output-shaped code; contract tests assert on route
  *geometry* (turn counts, ordinates, crossings) — fast, no image reading in
  CI. Rendered-PNG eyeballing (`--bake-vars` + `resvg`) is for development and
  the final visual pass only.
- **Git:** one purposeful change per commit; descriptive messages; **never**
  "Co-Authored-By"; `cargo fmt && cargo clippy && cargo test` before every
  commit that ends a stage; pushing is the user's call.

### Fixed cross-stage vocabulary

These names are the contract between stages (internals are yours):

```rust
// src/routing/mod.rs
pub struct Routing {
    pub links: Vec<RoutedLink>,          // crate::layout::ir::RoutedLink (unchanged contract w/ renderer)
    pub report: Vec<Violation>,
    pub strays: Vec<Stray>,
}
pub fn route(program: &Program, nodes: &[PlacedNode]) -> Result<Routing, Error>;

// src/routing/report.rs
pub enum Rule { Clearance, Separation, Contact, Crossing, Impossible }
pub enum Severity { Warning, Info }
pub struct Violation { pub rule: Rule, pub severity: Severity, pub links: Vec<String>, pub detail: String, pub span: Span }

// src/routing/ortho/ — the six-step pipeline
pub(crate) struct Chain {                // one link's route, search output → placement input → geometry input
    pub link: usize,                     // request index (declaration order)
    pub world: usize,
    pub runs: Vec<Run>,                  // alternating axes, ends first/last
    pub ends: [EndInfo; 2],              // path, side, rect, fan group
}
pub(crate) struct Run {
    pub axis: Axis,
    pub channel: usize,                  // index into that world's ChannelGraph
    pub span: (f64, f64),                // travel extent (provisional until geometry)
    pub ordinate: Option<f64>,           // None until placement
}

// src/routing/ortho/ledger.rs — the one committed-state ledger
impl Ledger {
    pub fn tracks_left(&self, world: usize, axis: Axis, chan: usize, span: (f64, f64), graph: &ChannelGraph) -> usize;
    pub fn crossings(&self, world: usize, band: Rect, axis: Axis) -> u32;
    pub fn side_free(&self, path: &str, side: Side, rect: Rect) -> usize;
    pub fn commit(&mut self, chain: &Chain, k: usize);
}

// src/routing/ortho/ladder.rs — the placement primitive
/// Order-preserving ordinates minimizing Σ(x_i − pref_i)², subject to
/// x_{i+1} − x_i ≥ pitch, lo_i ≤ x_i ≤ hi_i. Unique; caller guarantees feasibility.
pub(crate) fn ladder(prefs: &[f64], bounds: &[(f64, f64)], pitch: f64) -> Vec<f64>;
```

### Module map (end state)

```
src/routing/
  mod.rs        strategy dispatch, Routing, route()
  report.rs     Rule / Severity / Violation, crossing + stray reports
  straight.rs   the straight strategy (sequence messages; routing: straight)
  validate.rs   independent v2-law checker — test oracle, never a repair
  ortho/
    mod.rs      the six-step driver
    cost.rs     TURN_COST, CROSS_COST, MIN_PITCH (+ future-override comment)
    rect.rs     Rect                        (moved from layout/links)
    scene.rs    SceneIndex                  (moved)
    graph.rs    ChannelGraph decomposition  (moved, tests intact)
    request.rs  EdgeReq / Bundle / Fans     (moved from bundle.rs, split() deleted)
    ledger.rs   committed runs + side ports: closure, crossing counts
    search.rs   entries/punch + weighted Dijkstra
    place.rs    clusters, nesting order, preferences, ports
    ladder.rs   bounded min-separation isotonic regression (PAV)
    geometry.rs chains → polylines, jogs, self-loops, strays, min end segments
    labels.rs   label placement along routes (moved)
```

`src/layout/links/` ceases to exist. The renderer contract (`RoutedLink`
polylines, fillets at render time, concentric nesting, crossing-capped radii
in `src/render/links.rs`) is **unchanged**.

---

## Stage 1 — Demolition & scaffold

Everything v1-specific dies; the keepers move to their v2 home; the diagram
still compiles and renders (every link as an honest stray). After this stage
the tree contains no code that fights the v2 contract.

**Files:**
- Create `src/routing/{mod,report,straight,validate}.rs` (straight/validate as
  minimal stubs), `src/routing/ortho/{mod,cost,rect,scene,graph,request,labels,geometry}.rs`
- Move (git mv + adjust paths, keep unit tests): `layout/links/rect.rs`,
  `scene.rs`, `graph.rs`, `labels.rs`, `bundle.rs → ortho/request.rs`
  (delete `split()` and its callers), `geometry.rs` (keep `polyline`,
  `simplify`, `stray_segment`, `self_loop_chain` skeleton; delete
  chain-construction tied to v1 runs)
- Delete: `layout/links/{audit,capacity,feedback,runs,order,path,validate,mod}.rs`
- Modify `src/layout/mod.rs`: delete the gap-growth loop (`layout_mode`
  rerun block, `GapGrowth`, `grow`, `growable`, the `growth` param threaded
  through `attempt`/`layout_inst`/`lay_out_container_children`) and
  `Routing.starved` plumbing; `attempt` calls `routing::route`
- Modify `src/render/mod.rs` / `src/main.rs` / `src/lib.rs`: imports of
  `layout::links::{Violation, Rule, …}``routing::…`; keep
  `layout::cross` re-export alive by moving `cross()` (the transversal
  primitive shared with the fillet pass) into `routing/report.rs`
- Tests: delete `tests/linking.rs` + `tests/linking_sweep.rs` assertions that
  pin v1 behavior (compaction, gap growth, splitting, port slides). Mark
  link-bearing snapshot tests and the sample-law sweep
  `#[ignore = "routing-v2: re-enabled in stage 6/7"]`.

**Steps:**
- [x] Move the keepers; fix imports; delete the v1 router files
- [x] Excise gap growth from `src/layout/mod.rs` (see Execution log if the
      threading has drifted from this description)
- [x] `routing::route()` stub: expand requests via `request.rs`, return every
      link as a stray + `Rule::Impossible` violation (`detail: "routing v2 in
      construction"`)
- [x] Prune/ignore tests as above; `cargo fmt && cargo clippy && cargo test`
      green
- [x] Render `samples/pcb_fail.lini` — nodes + 8 strays, nothing panics
- [x] Commit (`refactor: raze v1 router, scaffold src/routing`)

**Done when:** no `layout/links` directory, no gap-growth code, green build,
strays render.

---

## Stage 2 — Search: ledger, entries, weighted Dijkstra

The Law-3 engine. Pure code against `ChannelGraph` + `Ledger`; no pipeline
integration yet (the stub keeps returning strays).

**Files:**
- Create `src/routing/ortho/ledger.rs`, `src/routing/ortho/search.rs`,
  fill `src/routing/ortho/cost.rs`
- Port from v1 `path.rs` (from git history): `entries()`/`punch()` largely
  as-is — an `Entry` gains its side's **port window**
  `(f64, f64)` (side span minus `clearance` corner margins); Dijkstra is
  rewritten for scalar weighted cost

**Interfaces (consumed by stage 4):**
```rust
pub(crate) struct Entry { pub side: Side, pub window: (f64, f64), pub tip: (f64, f64), pub axis: Axis, pub cell: usize }
pub(crate) fn entries(graph: &ChannelGraph, body: Rect, stub: f64, forced: Option<Side>, blockers: &[Rect], inward: bool) -> Vec<Entry>;
pub(crate) struct Route { pub cells: Vec<usize>, pub start: usize, pub goal: usize, pub cost: f64 }
pub(crate) fn cheapest(graph: &ChannelGraph, world: usize, starts: &[Entry], goals: &[Entry], ledger: &Ledger, k: usize, clearance: f64) -> Option<Route>;
```

**Guidance:**
- Cost is one `f64`: estimated length + `TURN_COST`·turns + `CROSS_COST`·crossings.
  Length estimate: L1 through cell entry midpoints (v1's estimator is fine);
  it must rank straight < dogleg < staircase — placement, not search, decides
  exact ordinates. Ties: goal-side rank, then start-side rank
  (right → bottom → left → top), then smaller cell id — total order, no NaN.
- Turns = axis changes along the cell path, **plus 2** for the single-channel
  case whose two entry windows don't overlap (the jog — ROUTING.md model
  step 4). The jog's perpendicular run must also pass `tracks_left` on the
  cell's crossing channel, and is costed as a crossing band like any run.
- Closure is counting, not simulation: `tracks_left` = capacity of the span's
  usable width at `MIN_PITCH` minus the **max point-load** of committed runs
  overlapping the span (sweep over span endpoints). An edge is closed when
  `tracks_left < k`. Sides close the same way: `side_free` =
  `floor(window / MIN_PITCH) + 1 − landed`.
- `crossings(band)` = committed perpendicular runs whose `(channel width ×
  span)` band intersects the candidate's band, counting bundle multiplicity.
- Fan groups: the shared end consumes one port slot and its side is fixed by
  the first-routed sibling (carry v1's `fan_pick` idea into stage 4's driver;
  the ledger only needs `side_free` to treat a fan group as one landing).

**Tests (unit, in-module):**
- [x] Port v1 `path.rs` entry/punch tests (facing sides, walled-off side,
      forced side, transparent-wall punch, blocked sibling)
- [x] `cheapest` picks facing sides for neighbours; picks an L for diagonal
      placement; detours around a closed channel
- [x] Weighted trade: a scene where the crossing-free route is > `CROSS_COST`
      longer → route crosses; shrink the detour below `CROSS_COST` → route
      detours. (Build committed state via `Ledger::commit` by hand.)
- [x] Jog: same-channel misaligned windows costs 2 turns and respects the
      crossing channel's capacity
- [x] `tracks_left`: overlapping committed spans reduce it by max point-load,
      disjoint spans don't; capacity floor at `MIN_PITCH` matches
      `floor(usable/(c/2)) + 1`
- [x] Determinism: 100 identical runs, identical `Route`
- [x] `cargo fmt && cargo clippy && cargo test`; commit
      (`feat: weighted search + capacity ledger for routing v2`)

---

## Stage 3 — Placement: ladder, clusters, preferences, ports

The Law-2 engine: per-channel constructive placement. Still pure; integrated
in stage 4.

**Files:**
- Create `src/routing/ortho/ladder.rs`, `src/routing/ortho/place.rs`

**Interfaces (consumed by stage 4):**
```rust
pub(crate) fn ladder(prefs: &[f64], bounds: &[(f64, f64)], pitch: f64) -> Vec<f64>;
/// Assign every Run::ordinate in every chain. Channels processed in fixed
/// order (world, axis, index); within a channel: cluster → order → ladder.
pub(crate) fn place(worlds: &[World], chains: &mut [Option<Chain>], reqs: &[EdgeReq], clearance: f64);
```

**Guidance:**
- `ladder` is bounded isotonic regression with minimum separation: substitute
  `y_i = x_i − i·pitch`, run pool-adjacent-violators on `y` with per-item
  bounds (pool blocks average their prefs; clamp each block into the
  intersection of its members' transformed bounds; re-pool on violation).
  Verify against brute force on small n — this function is the mathematical
  heart of Law 2, make its tests merciless.
- Clusters: runs in one channel chain-cluster when spans come within `pitch`;
  cluster pitch = `min(clearance, usable/(n−1))` floored at `MIN_PITCH`
  (search guaranteed fit; `debug_assert` it).
- Preferences and bounds per ROUTING.md model step 5, verbatim:
  interior run → channel anchor (midline between two keep-out walls; hug the
  keep-out wall when the other wall is the canvas edge); end run serving both
  ports → `clamp((centre_a+centre_b)/2, shared window)` with the shared
  window as bounds; end run serving one port → own side centre, own window
  as bounds.
- Order within a cluster: bundle members keep declaration order; otherwise by
  preference (`total_cmp`); tie → one-hop key (the neighbouring run's
  preference/ordinate, sign-flipped by turn direction so wires leave in the
  order they arrive — nested, never braided); final tie → declaration order.
  This comparator is the successor of v1 `order.rs`'s recursive walk. Start
  one-hop; if a contract test in stage 4/6 exposes a braid, deepen it and log
  the case in the Execution log rather than adding a repair pass.
- Ports are not placed separately: the end-run ordinate **is** the port.
  Fan siblings' shared end: one run/ordinate shared by the group.

**Tests:**
- [x] `ladder` vs brute force (n ≤ 5, random-ish fixed seeds hand-inlined):
      optimality, order, pitch, bounds — plus degenerate cases (one item,
      all-equal prefs, tight walls forcing exact pitch)
- [x] A 4-run bundle cluster centres its ladder on the midline preference
- [x] Two buses landing on one side ladder without braid (flash/pwr shape:
      prefs 300×4 + [285,295,305,315], pitch 10 → flash block yields
      [245,255,265,275], pwr exact — the worked example from the design)
- [x] Cluster chaining: spans touching within pitch merge; disjoint spans
      place independently (both may sit on the midline)
- [x] `cargo fmt && cargo clippy && cargo test`; commit
      (`feat: constructive placement — ladder, clusters, ports`)

---

## Stage 4 — Assembly: the six-step driver, geometry, first light

Wire search + placement into `ortho/mod.rs`, lower chains to polylines,
delete the stray-stub, and land the contract tests that prove the two pcb
bugs dead.

**Files:**
- Fill `src/routing/ortho/mod.rs` (driver), finish `ortho/geometry.rs`
- Modify `src/routing/mod.rs`: `route()` dispatches scopes to `ortho`
- Create `tests/routing.rs` (the v2 contract tests)
- Modify `src/lib.rs`: add the test hook
  ```rust
  /// Test-facing: routed polylines by endpoint pair, in declaration order.
  pub fn routes_str(src: &str) -> Result<Vec<((String, String), Vec<(f64, f64)>)>, Error>
  ```
  (thin wrapper: parse → resolve → layout → collect `RoutedLink.seg_from/seg_to/path`).

**Driver (ROUTING.md model, six steps):** build SceneIndex → per-world
`ChannelGraph`s (world ladder as v1: innermost first) → requests/bundles/fans
in declaration order → per bundle: entries per permitted side, `cheapest`
across the world ladder, commit to `Ledger` (no route in any world → all
members stray with reason) → `place()` over all chains → geometry: runs +
ordinates + jogs → polyline, collinear merge, end segments ≥ marker run-up
(`EdgeReq.stub_*`), self-loops around the keep-out corner (port v1
`self_loop_chain`), strays, labels (`labels::place`), report (exact crossing
count over final polylines using `report::cross` — one O(L²·S²) pass, tiny).

**Contract tests — `tests/routing.rs`** (helpers: `turns(path)` = direction
changes; `is_straight`; assert with geometry, never images):
- [x] **Consequences table, one test per row** (small inline `.lini` sources):
      facing aligned → 2 points; offset-within-windows → still 2 points;
      offset-past-windows → 4 points with mid-run on the gap midline
      (assert ordinate == midline ± ε); bundle ×4 → 4 parallel rails at pitch
      centred on midline; canvas-edge wire hugs keep-outs (ordinate ==
      keep-out wall); crossing beats orbit (assert total length < direct
      distance + margin, crossings reported == expected count)
- [x] **pcb_fail pins:** all 4 `pwr → mcu` have `turns == 0`; all 4
      `flash → mcu` have `turns == 2` and every point stays left of `mcu`'s
      right edge (no orbit); crossings reported == 0
- [x] Forced sides honored or stray (`a:left -> b` with `a`'s left walled)
- [x] Fan trunk: `a -> b & c` siblings share their first point
- [x] Self-loop right → top; both-ends-one-side errors
- [x] Containment link lands on the parent's inner side
- [x] Determinism: `routes_str` twice, assert equal
- [x] Render `pcb.lini`, `pcb_fail.lini`, `links_hard.lini` to PNG
      (`--bake-vars` + `resvg`) and **look at them** — development
      verification, not CI
- [x] `cargo fmt && cargo clippy && cargo test`; commit
      (`feat: routing v2 orthogonal pipeline — search, placement, geometry`)

**Done when:** the two pcb bugs are impossible per tests, samples render sane
wires, and `time ./target/release/lini samples/pcb.lini -o /dev/null` is
< 50 ms.

---

## Stage 5 — Straight strategy & sequence migration

**Files:**
- Fill `src/routing/straight.rs`: a link is one segment between two anchors,
  trimmed to the endpoint bodies (reuse `stray_segment`'s trim math — one
  mechanism), plus the rectangular self-hook; markers/labels ride as on any
  wire
- Modify `src/layout/sequence/messages.rs`: messages emit `RoutedLink`s
  (polyline + markers + label texts) through `straight` instead of lowering
  to `prim::line`; the renderer's existing link path (fillets, markers,
  label masks) draws them
- Modify `src/resolve/links.rs`: `routing: straight` becomes a legal value
  (any scope): straight trimmed segments, no avoidance, no report;
  `curved` stays deferred with the same error
- Update sequence snapshots (`cargo insta review`) after a visual PNG pass on
  `samples/sequence.lini`

**Steps:**
- [x] `straight.rs` + unit tests (trim math, self-hook shape, marker anchors)
- [x] Sequence migration; visual check that arrows/hooks/labels look
      unchanged (hook rounding now via the shared fillet pass — log any
      radius drift in the Execution log)
- [x] `routing: straight` end-to-end test in `tests/routing.rs` (an oblique
      pair renders one trimmed segment)
- [x] Snapshots re-accepted for sequence; `cargo fmt && cargo clippy &&
      cargo test`; commit (`feat: straight strategy; sequence messages ride
      the routing spine`)

---

## Stage 6 — Validator & the hard tests

The independent oracle plus the adversarial suite. This is where "works"
becomes "works at the edge".

**Files:**
- Fill `src/routing/validate.rs` for the v2 laws; re-enable the sample sweep
  (`tests/linking.rs` → rename `tests/laws.rs`)
- Extend `tests/routing.rs` with the stress suite

**Validator checks (each drawn polyline, no router knowledge):** ends
perpendicular on a side, ≥ clearance from corners; no point of any link
inside a foreign keep-out (own end segments excepted); pairwise separation
≥ `MIN_PITCH − ε` everywhere, with < clearance gaps flagged unless the local
corridor demonstrably can't hold its wires at full clearance; crossings
square-on point contacts, each present in the report and vice versa
(`reconcile` both directions, as v1 did); fan trunks shared-then-split.
Port v1 `validate.rs` geometry helpers from git history where they fit —
delete the compaction/slide excuses (those laws are gone).

**Steps:**
- [x] Validator + unit tests (hand-built violations are caught: oblique
      landing, corner graze, sub-pitch hug, unreported crossing)
- [x] `tests/laws.rs`: every sample routes with zero Warning-severity
      violations
- [x] **Tightness sweep** (the "right before impossible" test): a facing pair
      with a k-bundle, `gap` swept from roomy down to below `(k−1)·c/2`;
      assert lawful at every step, pitch compresses only when full clearance
      can't fit, and the stray appears exactly at the capacity boundary —
      no silent squeeze, no ugly detour
- [x] **Side-capacity sweep:** land n wires on one short side, n swept past
      `floor(window/MIN_PITCH)+1` → excess routes to other sides, then
      strays; port order matches wire order at every n (no braid)
- [x] **Crossing-vs-orbit torture:** the pcb_fail shape with the corridor
      progressively walled until crossing is forced; assert crossings appear
      one at a time (never a wrap), each reported
- [x] Determinism: full `routes_str` over every sample ×3, byte-equal
- [x] Perf tripwire: route `pcb.lini` 10× in a test under a generous bound
      (e.g. < 2 s debug total) — catches any audit-style regression
- [x] `cargo fmt && cargo clippy && cargo test`; commit
      (`test: v2 law oracle + adversarial routing suite`)

---

## Stage 7 — Polish, snapshots, docs

- [x] Visual pass: render **every** sample `--bake-vars` → PNG, read each one
      (AGENTS.md: don't make the user spot-check). Specifically confirm:
      concentric fillets on turning buses (also at compressed pitch — the
      radii grow by the *actual* track offset), crossing never mid-arc,
      labels legible, `pcb.lini` pretty end to end
- [x] Re-accept remaining snapshots; remove every stage-1 `#[ignore]`
- [x] Cosmetics-last pass over `src/routing/` (naming, dead imports, doc
      comments; every file ≤ ~500 LOC)
- [x] Docs: purge stale references — `PLAN.md` routing/architecture sections
      (point at `ROUTING.md`), `graph.rs`'s "PLAN.md §Architecture" header,
      SPEC.md §9's "only mode built today" line (now orthogonal + straight),
      README if it names routing modes
- [x] Full gate: `cargo fmt --all -- --check && cargo clippy && cargo test`
- [x] Final commit; leave pushing/merging to the user
      (`superpowers:finishing-a-development-branch`)

---

## Execution log

Executing sessions: append dated notes here — decisions the plan didn't
anticipate, gotchas, deferred items, comparator cases that needed deepening,
anything the next session must know. Keep entries terse.

- **2026-07-03, stage 7.** Done (snapshots, cosmetics, docs; the bug batch
  above landed first so snapshots were accepted once).
  - Visual pass: all 31 samples rendered `--bake-vars` → PNG and read.
    pcb end-to-end clean, flash/pwr/usb buses fully concentric (radii
    10/20/30/40 measured in the SVG arcs), links_medium/links_simple nest
    without braids, links_hard stands at its four pinned strays.
  - Snapshots re-accepted; both stage-1 `#[ignore]`s removed (conformance,
    hello). Link samples stay excluded from snapshots by design — routing
    is gated semantically (tests/laws.rs, tests/routing.rs). links.lini
    gained its first accepted snapshot.
  - Splits along concept seams, code ≤ ~500 lines per file:
    `ortho/pairwise.rs` (the general pairwise settle) out of place.rs,
    `ortho/entry.rs` (punches, port windows, clipping) out of search.rs,
    `validate/excuse.rs` (Law 1's contention-component excuse) out of
    validate.rs. Shared predicates (`contend`, `near`) stayed with the
    cluster model in place.rs.
  - Docs: SPEC §9/§10/§16/§20 now say `straight` is built (`curved` alone
    deferred); the resolver's deferred-routing message pointed at §19,
    fixed to §20; sequence/mod.rs's PLAN.md pointer and README/conformance
    references to the old `tests/linking.rs` updated.
- **2026-07-03, bug batch (user-reported, pre stage 7).** Two fixed, one
  diagnosed to its architectural root and deferred whole:
  - **Duplicate parallels braided** (links_simple `over`/`chat`,
    links_medium `.happy` ×2). Exact parallels tie at every estimate; every
    cluster fell to the declaration tie, each flipped by its own walk
    parity `m` — the clusters disagreed about who is inner. `cmp_runs` now
    falls to one oriented convention (`order::convention`): the
    earlier-declared wire keeps the left of its own travel through the
    queried channel — the offset-curve rule, self-consistent across every
    channel a pair shares. Tests: `duplicate_detours_nest_without_crossing`,
    `links_simple_reports_zero_crossings`.
  - **Fillet radii squashed** (pcb_fail outer track drew r 27.9, nest wants
    40). Both `fillet_targets`' ceiling and `rounding::round` capped every
    corner at *half* of each adjacent leg. The real constraint is joint:
    the two arcs sharing a leg together fill its length; a terminal leg
    belongs to its one corner (marker pull-back already shortened it).
    `round()` scales an over-full leg's pair in proportion to their desires
    — concentric desired sums are constant per shared leg, so one factor
    scales the whole bus and pitch stays uniform under squeeze.
  - **links_hard's 4 strays are NOT capacity truths** — stage 6's "no
    k-track exit at gap 30 / clearance 12" is hereby corrected. Lawful
    routes exist (hand-verified against the channel table): hub→n1 up the
    west flank with a corner shuffle through the H sliver below north;
    n2→e1 ×3 down through north's bottom wall into the hub pocket and
    into e1.left, V legs ending above hub's inflated top. They stray
    because **admission prices estimate-overshot spans**: a run's span
    reaches its neighbours' *anchor estimates* (mid-void), which poke past
    fragment boundaries the traversed cells never cross — the corridor
    walk then loses its absorbing neighbours and reads an inverted sliver.
    Three fixes tried, each surfacing coupling one layer deeper: (a)
    in-search pass-capacity (prev-centre→next-centre spans on straight
    extensions) — sound, but the probe still denies on estimates; (b)
    containment-based deny — sound, but estimate-driven denials contain no
    pass, so the loop loses progress and *more* links stray; (c) admission
    over the traversed extent (`Run.ext`) — all four draw, but **placement
    cannot realize them lawfully**: the lawful geometry needs an H run
    hugging the top of its pocket because its perpendicular *neighbour's*
    range ends there — a coupled cross-axis constraint placement doesn't
    model (corner clamps only bound by travel extents), so round-2 span
    refresh stretched V legs into west's latitudes (11.5 < 12) and pinned
    three wires onto one boundary ordinate (0 apart). The honest fix is a
    stage of its own: connection-feasible admission (price the traversed
    extent + the nearest lawful connection ordinate, not the anchor) plus
    coupled-axis placement bounds (a run's law range clipped by its
    perpendicular neighbours' placed/feasible ordinates). Until then the
    four strays stand: honest, conservative, and pinned in laws.rs with the
    real reason.
- **2026-07-03, stage 6.** Done (validate.rs filled, tests/laws.rs, three
  adversarial sweeps in tests/routing.rs). The oracle earned its keep on day
  one: it found six real engine bugs, each fixed at source — stage 6 turned
  into half validator, half the bug hunt the plan hoped it would be.
  - **`RoutedLink` gained `strategy: Strategy`** — the checker judges
    orthogonal wires only (a `straight` wire is lawfully oblique; sequence
    messages ride the same stream). `testing::drawn_edges`/`declared_edges`
    count per strategy: completeness is an orthogonal property (a
    `routing: straight` pair whose trim is empty lawfully draws nothing,
    and a root sequence's messages never lived on a `PlacedNode`).
  - **The checker's separation excuse** is one mechanism: floor (c/2)
    absolute; a sub-clearance gap excused only if the pair's **contention
    component** (parallel wires transitively owing pitch, drawn order kept)
    cannot spread to full clearance under per-wire lawful ranges — port
    window ∩ corridor usable, the graph rebuilt from `child_rects` of the
    pair's common world (walked up like the world ladder). Judged by
    longest-path reach, so a chain pinched at any cross-section excuses the
    group it compresses with. Simpler prongs (side-only, corridor-only,
    single cross-section) all mis-judge multi-window chains — tried and
    discarded.
  - **Engine bugs the oracle caught, and their fixes:**
    (a) *Phantom relief* (links.lini): provisional spans meeting at a shared
    estimate charged pitch never owed; the relief then compressed two nodes'
    windows as one scarce side. Fix: `place()` settles twice — round two
    re-derives spans from round-one ordinates (the search's probe-refine
    shape) and settles real contention.
    (b) *Cross-world blindness* (links.lini @6): an inner world's port and
    an outer punch landing on one physical side never coupled. Fix: items
    group by axis alone; clusters union across worlds on shared
    `(side, rect)` landings.
    (c) *Chain-model coupling* (links_hard): the stage-3 "known
    approximation" bit both ways — a zero-sep bridge dissolves a contending
    pair's pitch while order+envelope bind travel-disjoint groups. Fix:
    such clusters settle on true pairwise constraints (feasibility relief
    on the gap DAG + Dykstra projections; exact ladder kept for chains).
    (d) *Corner escape* (links_hard @9): a run priced over an estimated span
    drew through hub when a later round moved its far corner. Fix:
    `corner_clamp` — every ordinate stays inside its perpendicular
    neighbours' channel travel extents, so a corner never leaves either
    run's channel. (A max-extent bounds variant was tried first and
    reverted: pricing a ±200-long leg over the hull of its neighbours'
    ranges manufactures scarcity.)
    (e) *Ledger fragment blindness* (links_hard @8): `max_load` only counted
    commits in absorbed channels; a partially-covering fragment's rails
    park in the same void invisibly, so two overlapping corridors each
    admitted a full complement. Fix: foreign commits count wherever their
    estimated ordinate lies inside the corridor walls.
    (f) *Overflow blindness* (links_hard @9, the caption): `child_rects` and
    `gather` collapsed subtrees to the node's own rect; a group's caption
    overflowing its bbox was invisible to every world graph and wires drew
    through the drawn text. Fix: `SceneNode.overflow` — a collapsed
    keep-out is the rect plus each overflowing descendant rect (a hull was
    tried and reverted: it walls off free space beside a narrow caption
    and strayed hero.lini).
  - **Boundary condition worth remembering:** round one separates
    contenders by exactly the pitch they owe, so refreshed spans sit at
    exactly one clearance — `near` must be inclusive there (strict `<` let
    round two forget the contention and collapse wires onto a shared
    corner). The checker's component edges match, so both models charge
    tips flanking at exactly the pitch; wires that could lawfully share an
    ordinate across an exact-clearance tip gap ladder apart instead — a
    recorded conservatism.
  - **Relief is feasibility-driven now**, both paths: compress only what a
    greedy/longest-path pass proves cannot fit (the envelope test squeezed
    stretches whose staggered boxes actually held full clearance). A chain
    the floors cannot save falls through to `pairwise`, whose final clamp
    keeps windows and walls absolute — pitch carries the visible debt.
  - **Known limits, pinned in tests/laws.rs** (`known_limit`): admission is
    per corridor and per side, so a group jointly pinched by several nodes'
    port windows can over-admit by a hair and compress below the floor
    instead of straying — links_medium @13 and pcb @12 sit exactly there.
    The honest fix is a placement-aware admission probe; future work.
    Native attributes: all samples clean; links_hard's 4 strays pinned.
  - **Deviations from the plan's test sketch:** the tightness sweep sweeps
    the shared window (node height, sides forced) — the `gap` knob doesn't
    bound a facing bundle, and unforced the router lawfully spills the whole
    bundle over the top (a better outcome the test notes). The torture test
    pins that *every* rail costs exactly one crossing: a dodge needs two
    turns — already the crossing's whole price — so Law 3 never hops; count
    increments one per rail, no wrap, each reported. Perf tripwire budget is
    10 s / 10 debug compiles (~4 s measured; debug ≈ 25× the ~17 ms release
    route — catches an audit-style blowup, not machine variance).
  - `entries()` clips port windows by blockers in the punch stretch
    (labels inside transparent ancestors reduce the lawful window; a
    mid-window blocker keeps the wider shore). The punch itself still casts
    from the side centre — a blocker dead ahead kills the side even when
    the clipped window has room; logged for stage 7+.
  - `routing: straight` trim-empty silence stands (strategy has no report
    by contract; the checker skips straight wires).
- **2026-07-02, corridor view** (between stages 5 and 6; fixes the pcb_fail
  aesthetics the user flagged — fragment-midline bends, sub-clearance pitch
  with room to spare — both symptoms of the fragmentation weakness the
  stage-4 log recorded). The sweep slices one free corridor into several
  same-axis channels wherever any far-away node edge cuts the strip list;
  every consumer now reads the reassembled **corridor** instead
  (`ChannelGraph::corridor(axis, chan, span)`: walk shared boundaries into
  each same-axis channel free over the whole span, clamped to the origin
  channel's travel extent so an end segment's keep-out tail never blocks the
  walk). Consequences and decisions:
  - **One decision surface, three consumers**: `Corridor::{anchor, usable}`
    replace `Channel::{anchor, usable}` (deleted); `Ledger::tracks_left`
    takes corridor capacity minus the committed load of *every* absorbed
    channel (fragments share the void's tracks — over-stuffing one closes
    all, see the rebuilt `closed_channel_forces_the_detour`); commit
    ordinates, placement preferences, and geometry's seed estimates (seeds
    now carry their travel extent) all use the corridor anchor.
  - **Placement clusters across fragments** (union-find on span proximity +
    corridor connectivity, per world/axis) — without this, corridor-wide
    bounds would let blind per-fragment ladders collide. The cluster-pitch
    shortcut `min(clearance, usable/(n−1))` died: seps start at clearance
    and the **relief valve is the one compression mechanism**. Seps are owed
    only by *contending* neighbours (spans overlapping or within a
    clearance); transitively-chained far-apart items owe 0 — links_medium's
    20-item merged cluster is serializable only under that rule. Known
    approximation, logged for stage 6: an overlapping pair bridged *only* by
    span-disjoint middles can under-separate (the chain model can't express
    pairwise constraints); the validator should watch for it.
  - **Whole-span admission with learned closures**: the search prices edge
    by edge, but a merged run needs one ordinate lawful over its entire
    travel — the corridor intersection, which junction-fed edges overstate
    (the hub row-gaps admitted six wires through a 6 px band; stage 5 drew
    them in mutually-blind fragment clusters, quietly unlawful). After
    `cheapest`, the driver probes the chain run by run; a failing run's
    `(channel, span)` becomes a `Deny` and the same world searches again
    around it (bounded, no-progress-guarded) — so links with lawful
    alternatives find them and only genuine oversubscription strays.
  - Interior bounds fall back to the corridor **walls** when a sliver's soft
    margins cross — the search admitted the run; it draws there.
  - **Results**: pcb_fail — drops at clearance pitch 10 (was 6.13) centred
    by the void midline (was the fragment's), flash exits and the 8-port
    mcu:left ladder at pitch 10 (were 8.87/8.09); pins added to
    tests/routing.rs. pcb — `mcu → rf` ×2 now routes (was 2 honest strays).
    links_hard — 4 strays (was 6): `w2 → s1` ×2 now route; the rest are
    genuine capacity truths at gap 30/clearance 12. Sequence output
    byte-identical. pcb.lini ~17 ms release (corridor walks; budget 50 ms).
- **2026-07-02, stage 5.** Done (straight.rs, dispatch in `routing::route`,
  sequence messages on the spine). Decisions and drift:
  - **Dispatch shape:** `ResolvedLink`/`EdgeReq` carry a typed
    `routing: Strategy` (resolve still strips the attr, so nothing leaks
    into `style=`); `bundles()` admits only orthogonal requests;
    `ortho::route(index, reqs)` skips foreign requests and returns its
    drawn links' request indices; `routing::route` merges the strategies'
    output in declaration order and runs the **shared label pass** — labels
    moved out of the ortho driver into the spine, where ROUTING.md puts
    them.
  - **Sequence seam:** the layout owns *where*, so messages are lowered in
    the sequence's local frame through `straight::wire`/`hook` and stored on
    the container's new `PlacedNode::links`; `routing::owned_links` lifts
    them into scene coordinates (the root-sequence branch collects them
    directly). The renderer's one link path draws them — no `prim::line`
    arrows, no `.lini-sequence-message` labels (the text carries
    `font-size: 13` itself; the rule stays for any future use).
  - **Visual drift** (sequence.lini before/after, 0.2 % of pixels, all on
    the wires): arrow tips gain the standard `MARKER_OVERLAP` half-pixel
    nudge, dash phase starts from the marker-shortened end, and the
    self-hook's return corner radius moved 6.5 → 6.75 (the fillet pass
    caps at half the *drawn* leg, the old line path at half the shortened
    leg). No layout movement; labels, frames, bars identical. Messages now
    render in the link layer — above frame tabs and notes rather than
    below; nothing in the samples overlaps to show it.
  - Sequence "snapshots" are the conformance suite, ignored until stage 7 —
    the before/after PNG diff replaces the re-accept here. Orthogonal
    samples are byte-identical across the refactor.
  - A `routing: straight` pair whose trim leaves nothing (containment,
    coincident bodies) draws nothing, silently — the strategy has no
    report by contract. Revisit in stage 6 if the validator wants a say.
- **2026-07-02, stage 4.** Done (driver in ortho/mod.rs, chain construction +
  polylines in geometry.rs, labels.rs ported, `ortho/order.rs` added,
  tests/routing.rs with 17 contract tests; `routes_str` lives in
  `lini::testing` beside the other hooks, not top-level). pcb.lini routes in
  3 ms release. Decisions and fixes the stage forced — stage 5/6 sessions
  read these first:
  - **Self-loops ride the ordinary search** — no ported `self_loop_chain`.
    `self_loop_sides` (v1's resolution: default right → top, forced side's
    partner stays adjacent, one side = Impossible) turns the loop into a
    forced-sides bundle; wall runs hug the keep-out through ordinary channel
    anchors, ties wrap over the top through ordinary state-id ties. One
    mechanism; the contract test pins the drawn corner shape.
  - **The one-hop nesting key died; `ortho/order.rs` replaces it** — the v1
    outward-walk comparator rebuilt over v2 chains, reading placement
    estimates (ports/anchors) instead of assigned ordinates. The stage-3 log's
    predicted braid materialized immediately (pcb_fail: flash landing *below*
    pwr, 16 crossings). Cluster order is preference → walk → declaration:
    prefs sit inside their boxes so disjoint windows order themselves; the
    walk arbitrates equal preferences (nested, never braided); same-chain
    pieces order by estimate (no self-nesting).
  - **Search holes found by first light, fixed at source, all with the same
    shape — a run the Dijkstra edges never priced:**
    (a) the single-run straight never checked its channel's committed load
    nor that the *shared window holds k* rails at min pitch — `fits` added,
    and `jog_span`'s formula already generalizes to the too-tight-overlap
    case (it yields the shared window); geometry mirrors the same rule;
    (b) a reversal's U-connector was costed but never capacity-checked —
    `u_open` closes it, estimating the connector span from the doubled leg's
    port when it is an end run (links.lini: three side-hooks overcommitted
    one 8.8 px corridor without this).
  - **`usable()` charges soft walls the span faces, not walls it merely comes
    within a clearance of** — proximity charging sealed lawful single-track
    corner passes shut (links_hard's hub pocket). Plus `tracks_left` takes a
    −1e-6 epsilon: an exact-zero usable width is one lawful track, and float
    noise in wall coordinates flipped it closed.
  - **`settle` grew the general relief valve**: any stretch of cluster items
    pinned between two hard boxes compresses *uniformly* to fit (floored at
    min pitch), subsuming per-side and per-window compression. Same-wire
    seps stay 0. pcb_fail's 8-rail cluster (flash's channel floor vs pwr's
    window cap: 54 px for 7 gaps) needs exactly this.
  - The canvas-edge hug is keep-out edge **plus clearance/2** wherever the
    crossing span faces free space past the wall's ends — the soft-boundary
    surrender, pinned in the hug test. Bottom outranks top on side-rank
    ties, so the symmetric over/under detour goes under.
  - The consequences dogleg row holds for **facing sides**; unforced, the
    one-turn L via a third side is cheaper and correct (turns cost length) —
    both pinned as tests.
  - **Strays that remain by design** (v1 drew these by splitting bundles or
    ignoring capacity; v2 must not): pcb `mcu → rf` ×2 (a k=2 bundle through
    a 10 px soft-walled neck holding one track), links_hard `n2 → e1` ×3
    and `w2 → s1` ×2 (no k-track exit exists). The user's lever is `gap`.
  - **Stage-6 work items:** links_hard `hub → north.n1` (k=1) strays because
    the sweep fragments one wide junction void into parallel same-axis
    channels that each charge phantom-neighbour soft margins — capacity
    needs to see the void, not the fragment (or junction-aware merging).
    Fan siblings routing in *different worlds* would not share a port
    (`merge_fans` is per-channel) — theoretical, but the law says one port.
    Min end-segment length is enforced only by the punch stub (as v1);
    a marker run-up exceeding clearance isn't specially held.
- **2026-07-02, stage 1.** Done; deviations from the stage-1 file list:
  - `graph.rs` and `labels.rs` were *not* pre-moved (they'd sit dead until
    their consumers exist). Retrieve from history when their stage lands:
    `git show c70b13f:src/layout/links/graph.rs` (unit tests included, port
    them too) in stage 2, same for `labels.rs` (+ `order.rs`/`path.rs`/
    `runs.rs` as reference) in stages 2–4. v1 `geometry.rs`'s
    `polyline`/`simplify`/`self_loop_chain` likewise — they typed against v1's
    `Chain`; v2's `Chain` differs, so stage 4 rebuilds them from that
    reference rather than inheriting a mismatched shape.
  - Transitional `#[allow(dead_code)]`s are all tagged with a `// Scaffold:`
    comment naming the stage that consumes them — `grep -rn "Scaffold:" src/`
    is the cleanup list (request.rs, scene.rs, rect.rs module-level;
    `Side::ALL` in ast.rs; `ResolvedText.along/attrs` + `Along::Fraction` in
    resolve/ir.rs). Remove each allow when its consumer lands.
  - Ignored tests all carry the literal prefix `routing-v2:` in their ignore
    reason — `grep -rn "routing-v2:" tests/ src/` is the re-enable list
    (conformance glob, hello snapshot, 3 link tests in tests/rendering.rs,
    2 unit tests in src/render/rules.rs). `tests/linking.rs` and
    `tests/linking_sweep.rs` are deleted outright — stage 6 rebuilds the law
    sweep as `tests/laws.rs`.
  - `layout_raw` / `route_sample_raw` died with gap growth (raw is the only
    path now); `lini::testing` keeps `node_rect`, `route_sample`,
    `declared_edges`, `laws`.
  - `layout::ir` became `pub(crate)` so `routing` can name IR types directly;
    `cross()` now lives in `routing::report` (renderer calls
    `crate::routing::cross`).
- **2026-07-02, stage-4 handoff notes** (written at the stage-3/4 boundary —
  tacit knowledge the driver session needs):
  - Where this log contradicts the plan's earlier sections (the vocabulary
    block especially), **the log wins**: it records what stages 1–3 actually
    built. Current true signatures: `entries(graph, body, stub, clearance,
    forced, blockers, inward)`; `cheapest(graph, world, starts, goals,
    ledger, k, clearance) -> Option<Route>`; `Ledger::{commit_run,
    commit_port, tracks_left, side_free, crossings_covering,
    crossings_overlapping}`; `ladder(prefs, bounds, seps)`;
    `place(worlds, chains, clearance)`; types in `ortho/mod.rs`.
  - The driver replaces the stray-stub body of `routing::route()` — keep the
    stub's stray+Impossible construction for links that end up undrawn.
  - Filter each end's entries to sides with `ledger.side_free(path, side,
    rect) ≥ k` before search (a fan group needs 1 slot, not k). Fan side
    fixing: the first-routed sibling's side binds the group — port the
    `fan_pick` idea from v1's driver, reference at
    `git show c70b13f:src/layout/links/mod.rs` (also `world_ladder`,
    `route_self_loop`, and commit ordering). Port patterns, never v1
    mechanisms the spec killed.
  - Route → Chain: merge consecutive same-channel cells into one run (v1
    `geometry::chain` is the reference, `git show c70b13f:src/layout/links/
    geometry.rs`); a repeated cell in `Route::cells` is a U-turn — expand to
    run–jog–run; a single-run route whose windows don't overlap likewise
    expands to end–jog–end (the search already verified the jog's channel
    and priced its two turns; the jog run's channel is the cell's
    perpendicular one). End-run spans start at `EndInfo::side_coord()`.
  - `Ledger::commit_run` per run with provisional spans right after each
    bundle routes; `commit_port` per landing side (fan groups once).
  - After `place()`: corners are adjacent runs' ordinates; polyline = port →
    corners → port; collinear points merge (port v1 `simplify`); first/last
    segments stay ≥ `EdgeReq::stub_*` (marker run-up). `labels.rs` returns
    from history (`git show c70b13f:src/layout/links/labels.rs`) — it reads
    `EdgeReq` + polylines and should port nearly clean.
  - Remove each `// Scaffold:` allow whose code the driver consumes (grep
    `Scaffold:`); re-enable the `routing-v2:`-tagged tests drawn links
    satisfy again (grep `routing-v2:` — the two `src/render/rules.rs` unit
    tests and the three `tests/rendering.rs` link tests belong to stage 4/5;
    conformance + hello snapshots stay ignored until stage 7).
  - Visual dev loop: `lini <sample> --bake-vars -o x.svg && resvg x.svg
    x.png --zoom 2` — without `--bake-vars`, resvg renders var() as black.
- **2026-07-02, stage 3.** Done (ladder.rs, place.rs, pipeline types in
  ortho/mod.rs; 67 unit tests). Notes for stages 4/6:
  - **`ladder` takes per-pair separations**, not one pitch: two pieces of one
    wire owe each other nothing unless their spans overlap (a U's doubled
    legs keep pitch; a Z's jog may collapse to zero and the legs weld).
    `settle()` derives the seps; the same-wire test is member-chain overlap.
  - The first ladder cut (clip the unbounded isotonic fit by bound
    envelopes) was provably suboptimal — a bound activating inside a pooled
    block must re-balance its neighbours. The landed algorithm is bounded
    PAV with clipped block minimizers; the brute-force tests pin the
    difference. Treat `ladder` as sealed math.
  - `place(worlds, chains, clearance)` — the plan's `reqs` param went unused
    (declaration order rides `Chain::link`; bundles need no bookkeeping:
    rails come out adjacent because equal prefs + equal keys fall to
    declaration ties).
  - Nesting comparator is one-hop: sort by (pref, key-to-prev, key-to-next,
    link, run); a key is (arm_n, arm_r, arm_n·arm_r·neighbour_pref) — the
    sign product makes ascending key = ascending ordinate for corners
    turning the same way. If stage 4/6 exposes a braid, deepen the walk
    here, never add a repair pass.
  - Pipeline types live in `ortho/mod.rs`: `Chain{link, world, runs, ends}`,
    `Run{axis, chan, span, ord}`, `EndInfo{side, rect, window, fan}` (+
    `side_coord()`/`centre()`), `World{path, graph}`. Ports are end-run
    ordinates; fan siblings merge to one ladder item (windows intersected).
- **2026-07-02, stage 2.** Done (graph restored, cost.rs, ledger.rs,
  search.rs; 53 unit tests). Design decisions the plan didn't anticipate —
  stage 3/4 sessions read these first:
  - **Dijkstra states are (cell, travel direction) — 4 per cell, not 2.**
    Axis-only turn counting priced a doubling-back U at zero turns (a v1
    blind spot that made backtracking artificially cheap once crossings cost
    money). A reversal now costs its real two corners plus the U-connector's
    crossing estimate. **Stage 4's chain builder must expand a reversal
    (repeated cell in `Route::cells`) into run–jog–run**, same shape as the
    misaligned-window jog; a bundle's U needs 2k concurrent tracks but the
    search checks k per leg — placement's pitch compression absorbs it;
    revisit if a stage-6 sweep catches a violation.
  - **Crossing charges are certainty-based and optimistic.** Committed runs
    are stored as `(span, k, ord)` with `ord = Channel::anchor()` — the same
    anchor placement's interior-run preference uses (midline between
    keep-out walls, hug when one wall is the canvas edge; `graph.rs`).
    `crossings_covering` (rail's span covers the candidate's whole ordinate
    window → unavoidable) charges run travel, edges, jogs; half-open travel
    intervals stop double-charging at piece joints. `crossings_overlapping`
    charges only pinned stubs and U-connectors. Dodgeable rails are *never*
    charged — optimism is deliberate: over-charging is what bred v1's
    orbits; the exact count lands post-placement in the report.
  - Interface drift from the plan's vocabulary block: `entries()` gained
    `clearance` (port windows) and entries carry `dir`; the ledger is
    chain-agnostic (`commit_run`/`commit_port` instead of `commit(&Chain)`)
    — the stage-4 driver iterates a chain's runs itself; `crossings(band)`
    became the covering/overlapping pair.
  - v1's `Channel::capacity/width/capacity_for` died with their tests;
    capacity exists only in the ledger, at min pitch over `usable()`. Soft
    walls shrink pinches more than raw width suggests (a 28px pinch holds 6
    tracks, not 8) — remember when hand-deriving capacities in tests.