disposition 0.4.0

SVG diagram generator
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
# Edge Spacers

Edge spacer nodes are small invisible taffy leaf nodes inserted into rank containers to provide
coordinate waypoints for edge path routing. When an edge connects two diagram nodes that are
separated by one or more intermediate nodes in the layout, the edge path must route around those
intermediaries. Without spacer nodes the layout engine has no awareness of where the path should
pass, and the path calculation code cannot locate safe routing waypoints.

Spacer nodes participate in the flex layout so that after `taffy_tree.compute_layout_with_measure`
runs, each spacer has a computed absolute position. The edge path builder then uses those positions
as waypoints to route the edge around intermediate nodes.

Edges drawn with a direct curvature (`EdgeCurvature::is_direct()`, i.e. the `DirectStraight` /
`DirectCurved` variants) bypass spacers entirely. Their spacer taffy nodes are still inserted so the
tree structure is unchanged, but `EdgeSpacerBuilder::edge_spacer_length` returns `0.0` for them
instead of `EDGE_SPACER_LENGTH`, so they reserve no layout space. The effective curvature is selected
per edge from `RenderOptions::dependency_edge_curvature` /
`RenderOptions::interaction_edge_curvature` based on whether the edge is a dependency or interaction
edge.


## Background Concepts

### NodeNestingInfo

Every diagram node has a [`NodeNestingInfo`](crate/ir_model/src/node/node_nesting_info.rs) stored
in `NodeNestingInfos`. It records two parallel sequences:

- `ancestor_chain: Vec<NodeId>` -- the sequence of node IDs from the root of the hierarchy down to
  the node itself (inclusive). For a node `c01` nested inside `c0`, the chain is
  `["c0", "c01"]`. For a top-level node `a`, the chain is `["a"]`.
- `nesting_path: Vec<usize>` -- the sequence of sibling indices at each level, from root to the
  node. Entry `nesting_path[i]` is the position of the node's `ancestor_chain[i]` among its
  siblings. For example `[2, 0]` means "third top-level node, first child".

The two sequences are always the same length and aligned: `nesting_path[i]` is the sibling index
of the node at `ancestor_chain[i]`.

### LCA Depth

The Lowest Common Ancestor (LCA) depth of two nodes is the length of the longest common prefix of
their `ancestor_chain`s, computed by
[`LcaDepthCalculator`](crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder/lca_depth_calculator.rs).

    ancestor_chain of from_node: ["outer", "a",  "a01"]
    ancestor_chain of to_node:   ["outer", "b",  "b01"]
    longest common prefix:       ["outer"]
    LCA depth: 1

A depth of `0` means the two chains diverge at the first element -- the nodes share no ancestor
within the hierarchy.

    ancestor_chain of from_node: ["a", "a01"]
    ancestor_chain of to_node:   ["c", "c01"]
    longest common prefix:       []
    LCA depth: 0

### Divergent Ancestors

Given an LCA depth `d`, the divergent ancestors of two nodes are the elements at index `d` in each
`ancestor_chain`. They are the first nodes where the two chains differ -- i.e. the direct children
of the LCA that are ancestors of (or equal to) each endpoint.

    from_node ancestor_chain: ["outer", "a", "a01"]  -->  divergent_from = "a"   (index 1)
    to_node   ancestor_chain: ["outer", "c", "c01"]  -->  divergent_to   = "c"   (index 1)

The ranks of the divergent ancestors within their shared parent container determine the visual rank
span the edge must cross.

### Rank Span

Ranks are stored in `NodeRanksNested`, which holds a per-container `NodeRanks` map. For each pair
of divergent ancestors, the rank span is `[rank_low, rank_high]` where `rank_low` and `rank_high`
are the min and max of their two ranks. Intermediate ranks are the integers strictly between
`rank_low` and `rank_high`. If `rank_high - rank_low <= 1` there are no intermediate ranks and no
spacers are needed.

### Spacer Node Style

All spacer nodes are taffy leaf nodes with the following style:

    min_size: 5px x 5px
    align_self: Stretch

The 5 x 5 px minimum size ensures the spacer always occupies a non-zero area so the path builder
can compute a meaningful entry and exit point. `align_self: Stretch` makes the spacer expand to
fill the cross axis of its rank container, which keeps it vertically or horizontally centered with
the other nodes in its row.

Each spacer carries a `TaffyNodeCtx::EdgeSpacer(EdgeSpacerCtx { edge_id, rank })` context value
so the path builder can identify which edge the spacer belongs to and at which rank it sits.


## Five Kinds of Spacer

    EdgeSpacerTaffyNodes {
        # Same-level cross-rank spacers, keyed by intermediate rank.
        rank_to_spacer_taffy_node_id:                          Map<NodeRank, taffy::NodeId>,

        # Cross-container spacers, ordered by layout position.
        cross_container_spacer_taffy_node_ids:                 Vec<taffy::NodeId>,

        # BetweenRanks edge description container spacers, ordered by layout position.
        edge_desc_container_spacer_taffy_node_ids:             Vec<taffy::NodeId>,

        # SameRank edge description container crossing spacers, ordered by layout position.
        same_rank_edge_desc_container_spacer_taffy_node_ids:   Vec<taffy::NodeId>,

        # Text-content (node-label) spacers, ordered by layout position.
        text_content_spacer_taffy_node_ids:                    Vec<taffy::NodeId>,
    }

### 1. Same-Level Cross-Rank Spacers

Built by `EdgeSpacerBuilder::build`. Used when the two endpoints of an edge diverge at the same
hierarchy level and span more than one rank. A spacer is inserted at each intermediate rank in the
same rank-container group that the divergent ancestors belong to.

### 2. Cross-Container Spacers

Built by `EdgeSpacerBuilder::build_cross_container_spacers`. Used when one endpoint is inside a
container and the other is outside, and the edge path must pass alongside the container's sibling
children to reach the deeply-nested endpoint. Spacers are placed at the ranks of those intermediate
sibling children within the container.

### 3. Edge Description Container Spacers

Built by `EdgeSpacerBuilder::build_edge_desc_container_spacers`. For a **cross-rank**
(`BetweenRanks`) container, used when an edge's rank span `[rank_low, rank_high)` crosses the
position `rank_P` of an `edge_description_container` that belongs to a **different** described edge
(condition: `rank_low <= rank_P < rank_high`). For a **same-rank** (cycle edge) container, the
sibling-index-axis analog applies instead (`EdgeSpacerBuilder::build_edge_desc_container_spacers_for_edge_same_rank`):
another edge sharing the same rank whose own two divergent siblings' indices span across the
container's `sibling_index_middle` -- e.g. the reverse direction of the same cyclic pair -- is
crossing it too, even without a description of its own. In both cases a single spacer leaf is
appended as a direct child of that container, providing a coordinate waypoint so the crossing edge's
path builder routes through the container's visual space rather than jumping over it. The described
edge whose container it is does **not** receive a spacer inside its own container -- instead its path
bends directly to the description box's own resolved rect, via
`SpacerCoordinatesResolver::description_contact_resolve` (see
[edge_descriptions.md](edge_descriptions.md)). That contact is applied to the owning edge's path
**unconditionally**, regardless of `EdgeCurvature::is_direct()` -- unlike every other spacer kind
here, which direct-curvature edges bypass entirely.

A same-rank crossing spacer is tracked in a **separate** list,
`same_rank_edge_desc_container_spacer_taffy_node_ids`, and its `EdgeSpacerCtx` carries this edge's
own `sibling_index_from.cmp(&sibling_index_to)` ordering (`same_rank_sibling_index_from_cmp_to`).
`SpacerCoordinatesResolver::resolve` uses this to resolve the spacer's entry/exit via
`EdgeSpacerCoordinatesCalculator::calculate_description_thread_same_rank` -- the same
direction-aware, rotated-axis calculation the owning edge's own contact already uses -- instead of
the direction-oblivious generic `calculate` used for every other spacer kind. This matters because a
same-rank container's children stack *perpendicular* to the edge's own travel direction: resolving
with the diagram's canonical direction regardless of which end the crossing edge actually approaches
first can select the far end as the nominal entry, forcing the connector between the `from`-node's
protrusion leg and that entry point to cut straight through the container's interior to reach it,
instead of jogging clear of the container first. (Before this fix, `edge_dep_server_proc_1_proc_2__1`
in `021_interaction_halo_with_desc_cyclic.yaml` -- whose divergent ancestors sit in the reverse order
along the shared rank relative to the described `edge_dep_server_proc_1_proc_2__0` -- visibly cut
through the top of `proc1_proc2_desc`'s box.)

### 4. Text-Content (Node-Label) Spacers

Built by `EdgeSpacerBuilder::build_text_content_spacers`. Used when a container node that renders a
**non-empty top text band** -- a title and/or a `thing_desc` -- is entered by a cross-container edge
to reach a node nested strictly inside it. Node names are rendered as markdown in the same band as
descriptions, so a wide title is just as crossable as a description; the gate keys off the rendered
text (`TaffyBuildCtx::node_md_text`), not the presence of a description, and only genuinely text-less
containers are skipped. The text block renders at the top of the node body, above its rank
containers; without a waypoint there, the entering edge cuts straight across the label. One spacer
leaf is appended **to the right of the text content** -- the text node and its spacers are wrapped in
a `FlexDirection::Row`, so a single spacer per edge (not one per text line) marks a column just past
the label. The edge then bows around the label and descends on its outer side before returning to its
rank column.

Scope and behaviour differ from the other kinds in three ways:

- **To-side only, cross-rank entries only.** A spacer is created only for the container in which the
  **`to`** node is nested (`edge.to`'s `ancestor_chain` contains the container; the `from` node for
  `BottomToTop`). The from side (where the edge merely exits its source) gets none, the diagram root
  gets none (it contains both endpoints, so the decider skips it), and the container that *is* the
  endpoint node gets none (the path stops at its face). Only **vertical** rank directions
  (`TopToBottom` / `BottomToTop`) build them -- horizontal flows enter past a side-strip label, not
  through it. A spacer is also skipped when the edge's divergent ancestors share a rank (e.g. a
  same-rank cyclic edge between adjacent siblings): such an edge enters the container from the side,
  past the label, so a text spacer would needlessly loop it up to the label band and back.
- **Excluded from the cross-container column snap.** Unlike cross-container spacers, text-content
  spacers are **not** passed to `cross_container_spacers_snap_to_column`
  ([edge_paths.md]edge_paths.md). This keeps the detour **local** to the text band -- the edge
  approaches at its normal column, bows out only around the label, then returns to its rank column
  for the descendants -- rather than the whole descent column being pulled onto the label's far side.
- **Stored in `text_content_spacer_taffy_node_ids`**, merged as a routing waypoint and ordered by
  main-axis coordinate (the label sits above rank 0, so it is the first waypoint).

To keep the return jogs (text-band column back to each edge's rank column) at visually distinct
depths, the text `Row` is given a **bottom margin** of `N * TEXT_CONTENT_SPACER_GAP_PX` (where `N`
is the number of text spacers), enlarging only the label -> rank-0 gap so the protrusion band has
room to stagger one jog depth per edge.

> Note: appending the spacers grows the node's width by more than the bare spacer + gap sum, due to
> a taffy slack-distribution quirk in the text `Row` that has not been fully pinned down. This is
> cosmetic and may be revisited.


## When Spacer Building is Triggered

Spacer building is orchestrated by `IrToTaffyBuilder` in
[`crate/input_ir_rt/src/ir_to_taffy_builder.rs`](crate/input_ir_rt/src/ir_to_taffy_builder.rs).

After all diagram nodes and their child hierarchies have been added to the taffy tree, spacers are
inserted in five stages per dimension:

1. **Nested same-level spacers.** For every container diagram node (one that has children),
   `build_taffy_nodes_for_node_with_child_hierarchy` calls `EdgeSpacerBuilder::build` once per
   entity type (`ThingDefault`, `TagDefault`, `ProcessDefault`), passing `lca_node_id = Some(&container_id)`.
   This inserts spacers for edges whose LCA is exactly this container.

2. **Nested cross-container spacers.** The same function also calls
   `EdgeSpacerBuilder::build_cross_container_spacers` once per container, inserting spacers for
   edges that enter the container from outside.

3. **Nested edge description container spacers.** After `EdgeDescriptionBuilder::build` has
   produced `edge_description_container` nodes for this nesting level,
   `build_taffy_nodes_for_node_with_child_hierarchy` calls
   `EdgeSpacerBuilder::build_edge_desc_container_spacers` once per entity type with
   `lca_node_id = Some(&container_id)`. Each edge whose rank span crosses a `BetweenRanks`
   `edge_description_container` at this level, or whose sibling-index span crosses a `SameRank` one
   (other than its own container, in either case) receives a spacer leaf appended as a child of that
   container.

4. **Top-level same-level spacers.** After all containers have been processed,
   `build_taffy_trees_for_dimension` calls `EdgeSpacerBuilder::build` once per entity type with
   `lca_node_id = None`. This inserts spacers for top-level edges whose LCA is the diagram root.

5. **Top-level edge description container spacers.** Immediately after the top-level
   `EdgeDescriptionBuilder::build` calls, `build_taffy_trees_for_dimension` calls
   `EdgeSpacerBuilder::build_edge_desc_container_spacers` once per entity type with
   `lca_node_id = None`. Each top-level edge whose rank span (or, for a same-rank container,
   sibling-index span) crosses a top-level `edge_description_container` (other than its own)
   receives a spacer inside that container.

All five stages accumulate their results into a single `Map<EdgeId, EdgeSpacerTaffyNodes>`. Stages
1-3 return results that are merged into `nested_edge_spacer_taffy_nodes` and carried back up to
the top level. Stages 4-5 extend the top-level map directly. The fully-merged map is stored as
`TaffyNodeMappings::edge_spacer_taffy_nodes`.

Edge description container spacer results are merged entry-by-entry (not with a plain `extend`)
to preserve any rank or cross-container spacer entries that may already exist for the same edge.


## Same-Level Cross-Rank Spacers: EdgeSpacerBuilder::build

Source: [`crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder.rs`](crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder.rs)

### Signature

    EdgeSpacerBuilder::build(
        taffy_tree:          &mut TaffyTree<TaffyNodeCtx>,
        edge_groups:         &EdgeGroups,
        node_nesting_infos:  &NodeNestingInfos,
        node_ranks_nested:   &NodeRanksNested,
        entity_types:        &EntityTypes,
        target_entity_type:  &EntityType,
        rank_to_taffy_ids:   &mut BTreeMap<NodeRank, Vec<taffy::NodeId>>,
        lca_node_id:         Option<&NodeId>,
    ) -> Map<EdgeId, EdgeSpacerTaffyNodes>

The `rank_to_taffy_ids` parameter is modified in place: spacers are inserted into its vectors.
The function also maintains an internal `rank_spacer_counts: BTreeMap<NodeRank, Vec<usize>>` to
track how many spacers have already been inserted at each position within each rank, so that
successive edges insert their spacers at the correct offset.

### Algorithm: edge_spacers_build (per edge)

For each edge in every edge group the function calls the private `edge_spacers_build` helper. The
helper performs the following steps:

**Step 1 -- Resolve nesting infos.**
Look up `NodeNestingInfo` for both `edge.from` and `edge.to` in `node_nesting_infos`. Return early
(`None`) if either is missing.

**Step 2 -- Compute LCA depth and divergent ancestors.**

    lca_depth      = LcaDepthCalculator::calculate(nesting_info_from, nesting_info_to)
    divergent_from = nesting_info_from.ancestor_chain[lca_depth]
    divergent_to   = nesting_info_to.ancestor_chain[lca_depth]

Return `None` if either index is out of bounds (one node is an ancestor of the other).

**Step 3 -- Entity type filter.**
Both `divergent_from` and `divergent_to` must match `target_entity_type` in `entity_types`. If
either does not match, return `None`. This ensures that, for example, a cross-rank edge between
things does not accidentally insert spacers into a process rank container.

**Step 4 -- Compute rank span.**
Using `node_ranks_nested`, look up the rank of `divergent_from` and `divergent_to` in the rank map
for their shared parent container. The shared parent container is:

    lca_container = nesting_info_from.ancestor_chain[lca_depth - 1]  (None if lca_depth == 0)

    rank_from = container_ranks[divergent_from]
    rank_to   = container_ranks[divergent_to]
    rank_low  = min(rank_from, rank_to)
    rank_high = max(rank_from, rank_to)

Return `None` if `rank_low == rank_high` (same visual row, no intermediate ranks) or if
`rank_high - rank_low <= 1` (adjacent rows, no intermediate ranks).

**Step 5 -- LCA level filter.**
The `lca_node_id` parameter controls which hierarchy level this call handles:

- `lca_node_id = None` (top-level call): return `None` unless `lca_depth == 0`.
- `lca_node_id = Some(expected_id)` (nested call): return `None` if `lca_depth == 0`, or if
  `nesting_info_from.ancestor_chain[lca_depth - 1] != expected_id`.

This ensures each edge's same-level spacers are inserted exactly once, into the correct rank
container group.

**Step 6 -- Compute insertion base index.**

    from_sibling_index = nesting_info_from.nesting_path[lca_depth]
    to_sibling_index   = nesting_info_to.nesting_path[lca_depth]
    insertion_base_index = (from_sibling_index + to_sibling_index) / 2 + 1

This places the spacer between the two divergent sibling subtrees.

**Step 7 -- Insert a spacer at every intermediate rank.**
For each `rank` in the exclusive range `(rank_low + 1 .. rank_high)`:

1. Create a taffy leaf node with `TaffyNodeCtx::EdgeSpacer { edge_id, rank }` and the spacer style.
2. Compute the effective insertion index (see next section).
3. Insert the new taffy node ID into `rank_to_taffy_ids[rank]` at the effective index.
4. Update `rank_spacer_counts[rank]` to record the insertion.
5. Record `rank -> spacer_taffy_node_id` in `EdgeSpacerTaffyNodes::rank_to_spacer_taffy_node_id`.

### Insertion Index Computation

Because multiple edges may insert spacers at the same rank, later insertions must account for the
nodes that earlier insertions have already placed. The effective index is computed as follows:

    spacers_at_or_before = sum(rank_spacer_counts[rank][0 .. insertion_base_index])
    effective_index      = min(insertion_base_index + spacers_at_or_before, current_len)

Where `current_len` is `rank_to_taffy_ids[rank].len()` at the time of insertion. Capping at
`current_len` ensures the spacer is appended when the computed index would exceed the end of the
vector.

After insertion, `rank_spacer_counts[rank]` is updated by inserting `1` at `effective_index`,
shifting all subsequent counts right. This keeps the count vector aligned with the position vector
so future insertions at the same rank compute the correct offset.

Both the base-index formula and the effective-index/count-tracking logic above live in a shared
helper, [`RankSiblingInserter`](crate/input_ir_rt/src/ir_to_taffy_builder/rank_sibling_inserter.rs)
(`insertion_base_index_compute`, `node_insert`), rather than being private to `EdgeSpacerBuilder`.
`EdgeDescriptionBuilder` calls the same helper to place same-rank edge description containers (see
[edge_descriptions.md](edge_descriptions.md) -- Same-Rank (Cycle Edge) Placement) between two
same-ranked divergent ancestors, keeping its own separate `same_rank_insertion_counts` tracker since
the two builders insert into `rank_to_taffy_ids` at different times.


## Cross-Container Spacers: EdgeSpacerBuilder::build_cross_container_spacers

Source: [`crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder.rs`](crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder.rs)

### Signature

    EdgeSpacerBuilder::build_cross_container_spacers(
        taffy_tree:                 &mut TaffyTree<TaffyNodeCtx>,
        edge_groups:                &EdgeGroups,
        node_nesting_infos:         &NodeNestingInfos,
        node_ranks_nested:          &NodeRanksNested,
        rank_to_taffy_ids:          &mut BTreeMap<NodeRank, Vec<taffy::NodeId>>,
        container_node_id:          &NodeId,
        container_node_hierarchy:   &NodeHierarchy,
    ) -> Map<EdgeId, EdgeSpacerTaffyNodes>

Returns immediately with an empty map if `container_node_hierarchy` has one or fewer direct
children, because a single-child container has no siblings that could block an edge.

### Purpose

When an edge has one endpoint outside a container and the other endpoint nested inside (at any
depth), the edge path must travel alongside the container's internal children to reach the target.
Cross-container spacers mark the positions of those intermediate siblings so the path builder can
use them as waypoints.

Only siblings whose rank is **strictly less than** the target child's rank receive a spacer. A
sibling at the same rank as the target is placed side-by-side with it and does not obstruct the
incoming edge. Siblings at higher ranks are beyond the target and are also outside the path.
Consequently, if the target child is at rank 0 there are no qualifying siblings and no spacers are
created.

At most one spacer is created per rank value within a given edge. Multiple siblings at the same
rank occupy the same layout row, so a single spacer is sufficient to route around the entire row.

### Decision Logic: EdgeSpacerBuildDecider::decide

Source: [`crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder/edge_spacer_build_decider.rs`](crate/input_ir_rt/src/ir_to_taffy_builder/edge_spacer_builder/edge_spacer_build_decider.rs)

For each edge, `EdgeSpacerBuildDecider::decide` returns either `EdgeSpacerBuildDecision::Build`
or `EdgeSpacerBuildDecision::Skip`. It proceeds through the following checks in order:

Note: the decision rests only on containment and the target's position *inside* the container --
**not** on the root-level distance between the two divergent ancestors. An edge into a deeply-ranked
child must route around the container's lower-rank siblings even when its divergent ancestor is
adjacent to the from-node's divergent ancestor. (The actual "is a spacer needed?" question is
answered by the [Spacer Insertion](#spacer-insertion) loop, which only routes around siblings at
ranks strictly before the target; a target at rank 0 therefore yields no spacers regardless.)

**Check 1 -- Nesting info availability.**
If `NodeNestingInfo` is missing for `edge.from` or `edge.to`, skip with
`NestingInfoFromNotFound` / `NestingInfoToNotFound`.

**Check 2 -- Containment check.**
Determine which endpoints are inside `container_node_id` by searching each endpoint's
`ancestor_chain` for the container ID:

    container_contains_from = container_node_id in info_from.ancestor_chain
    container_contains_to   = container_node_id in info_to.ancestor_chain

Skip with `ContainerNodeContainsBothFromAndToNodes` if both are inside.
Skip with `ContainerNodeContainsNeitherFromAndToNodes` if neither is inside.
Continue only when exactly one endpoint is inside.

**Check 3 -- Find the target child.**
Let `info_inside` be the nesting info of the contained endpoint. Find the depth of
`container_node_id` in `info_inside.ancestor_chain`:

    container_depth = position of container_node_id in info_inside.ancestor_chain
    target_child_id = info_inside.ancestor_chain[container_depth + 1]

If `container_depth + 1` is out of bounds (the contained endpoint IS the container node itself),
skip with `ContainerNodeIsFromOrToNode`.

**Result.**
Return `EdgeSpacerBuildDecision::Build { target_child_id }` where `target_child_id` is the direct
child of the container whose subtree contains the inside endpoint.

### Spacer Insertion

After `EdgeSpacerBuildDecider::decide` returns `Build`, the following steps are performed in
`build_cross_container_spacers_for_edge`:

1. Look up `target_rank`: the rank of `target_child_id` among the container's direct children
   using `node_ranks_nested.ranks_for(Some(container_node_id))`.

2. Iterate over every other direct child (`sibling_id`) of the container:

   a. Skip `sibling_id == target_child_id`.

   b. Look up `sibling_rank` using the same container's rank map.

   c. Skip if `sibling_rank >= target_rank` (sibling is at or past the target, not between the
      container entry and the target).

   d. Skip if this rank has already received a spacer for this edge (tracked via a local
      `ranks_with_spacers: BTreeSet<NodeRank>`).

   e. Create a taffy leaf node with `TaffyNodeCtx::EdgeSpacer { edge_id, rank: sibling_rank }` and
      the spacer style.

   f. Append the new node ID to `rank_to_taffy_ids[sibling_rank]` (cross-container spacers are
      always appended, not inserted at a computed position).

   g. Record the node ID in `EdgeSpacerTaffyNodes::cross_container_spacer_taffy_node_ids`.

3. If any spacers were created, merge them into the `edge_spacer_taffy_nodes` map under the edge's
   ID.


## Data Produced

All spacer nodes from both kinds of building are accumulated into:

    TaffyNodeMappings::edge_spacer_taffy_nodes: Map<EdgeId, EdgeSpacerTaffyNodes>

The `EdgeSpacerTaffyNodes` for each edge contains:

    EdgeSpacerTaffyNodes {
        rank_to_spacer_taffy_node_id:                         Map<NodeRank, taffy::NodeId>,
        cross_container_spacer_taffy_node_ids:                Vec<taffy::NodeId>,
        edge_desc_container_spacer_taffy_node_ids:            Vec<taffy::NodeId>,
        same_rank_edge_desc_container_spacer_taffy_node_ids:  Vec<taffy::NodeId>,
        text_content_spacer_taffy_node_ids:                   Vec<taffy::NodeId>,
    }

`rank_to_spacer_taffy_node_id` maps each intermediate rank value to the single spacer node
inserted there (same-level cross-rank spacers). Because each edge inserts at most one
same-level spacer per rank, a map keyed by rank is sufficient.

`cross_container_spacer_taffy_node_ids` is an unkeyed list of cross-container spacer node IDs.
Multiple cross-container spacers can share the same global rank value (each container has its own
independent rank numbering), so a map keyed by rank would conflate them. Instead the list is
ordered by iteration order, and the edge path builder uses the computed absolute positions after
layout to sort and route through them correctly.

`edge_desc_container_spacer_taffy_node_ids` is an unkeyed list of `BetweenRanks` edge description
container spacer node IDs. Each entry corresponds to a spacer appended inside one
`edge_description_container` that the edge's path crosses. Like cross-container spacers, the edge
path builder sorts all kinds together by main-axis coordinate after layout to determine the correct
waypoint order, and their coordinates are resolved via the direction-oblivious generic
`EdgeSpacerCoordinatesCalculator::calculate`.

`same_rank_edge_desc_container_spacer_taffy_node_ids` is the `SameRank` (cycle edge) counterpart:
an unkeyed list of spacer node IDs appended inside a same-rank `edge_description_container` that the
edge crosses without owning. These are tracked separately because their coordinates are resolved via
`EdgeSpacerCoordinatesCalculator::calculate_description_thread_same_rank` instead -- the rotated,
direction-aware calculation, using the `sibling_index_from_cmp_to` ordering recorded on each node's
own `EdgeSpacerCtx` -- not the generic `calculate`, since a same-rank container's children stack
perpendicular to the edge's own travel direction (see [Edge Description Container
Spacers](#3-edge-description-container-spacers)).

`text_content_spacer_taffy_node_ids` is an unkeyed list of text-content (node-label) spacer node
IDs -- one per cross-container edge that enters a described container to reach a node nested inside
it (see [Text-Content (Node-Label) Spacers](#4-text-content-node-label-spacers)). They are merged
and sorted with the other kinds by main-axis coordinate, but -- unlike the cross-container list --
are **excluded from the cross-container column snap**, so the detour around the label stays local
to the text band.

`TaffyNodeMappings::edge_spacer_taffy_nodes` is consumed by `TaffyToSvgElementsMapper` and the
edge path routing code in
[`edge_path_builder_pass_1.rs`](crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_builder_pass_1.rs).
The coordinates of spacer nodes are extracted by
[`EdgeSpacerCoordinatesCalculator`](crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_spacer_coordinates_calculator.rs),
which walks up the taffy tree to accumulate each spacer's absolute position and returns entry and
exit points based on the configured `RankDir`.


## End-to-End Example

### Diagram input

    things (row layout):
      t_a:                   # rank 0
      t_b:                   # rank 1
      t_c:                   # rank 2
        t_c0:                # rank 0 within t_c
        t_c1:                # rank 1 within t_c

    edges:
      edge_a_c1: from t_a, to t_c1

### NodeNestingInfo

    t_a:
      ancestor_chain: ["t_a"]
      nesting_path:   [0]

    t_b:
      ancestor_chain: ["t_b"]
      nesting_path:   [1]

    t_c:
      ancestor_chain: ["t_c"]
      nesting_path:   [2]

    t_c0:
      ancestor_chain: ["t_c", "t_c0"]
      nesting_path:   [2, 0]

    t_c1:
      ancestor_chain: ["t_c", "t_c1"]
      nesting_path:   [2, 1]

### Same-level cross-rank spacers for edge_a_c1

Triggered by the top-level call `EdgeSpacerBuilder::build(..., lca_node_id = None)`:

    lca_depth      = 0   (chains ["t_a"] and ["t_c", "t_c1"] share no prefix)
    divergent_from = "t_a"   (ancestor_chain_from[0])
    divergent_to   = "t_c"   (ancestor_chain_to[0])
    lca_container  = None    (lca_depth == 0, root level)
    rank_from      = 0
    rank_to        = 2
    rank_low       = 0,  rank_high = 2
    intermediate ranks: [1]

    insertion_base_index = (0 + 2) / 2 + 1 = 2

A spacer leaf node tagged `EdgeSpacer { edge_id: edge_a_c1, rank: 1 }` is inserted at index 2 of
`rank_to_taffy_ids[rank=1]`, which is the rank 1 row of the top-level things container. This row
contains `t_b`. The spacer is positioned adjacent to `t_b` and serves as a waypoint so the edge
path passes through rank 1 space without crossing `t_b`.

    EdgeSpacerTaffyNodes for edge_a_c1:
      rank_to_spacer_taffy_node_id: { rank=1: <spacer_node_id> }

### Cross-container spacers for edge_a_c1

Triggered by `EdgeSpacerBuilder::build_cross_container_spacers(..., container_node_id = "t_c")`:

**Decider:**

    container_contains_from = "t_c" in ["t_a"]          = false
    container_contains_to   = "t_c" in ["t_c", "t_c1"]  = true
    exactly one endpoint is inside, continue

    info_inside = NodeNestingInfo of t_c1
    container_depth = position of "t_c" in ["t_c", "t_c1"] = 0
    target_child_id = ancestor_chain_inside[0 + 1] = "t_c1"

    Result: Build { target_child_id: "t_c1" }

**Spacer insertion:**

    target_rank = rank of "t_c1" within t_c = 1

    siblings of t_c1 inside t_c: [t_c0]
      t_c0: sibling_rank = 0
        sibling_rank (0) < target_rank (1) --> qualify
        ranks_with_spacers does not contain 0 --> qualify
        Create EdgeSpacer { edge_id: edge_a_c1, rank: 0 } and append to rank_to_taffy_ids[rank=0]

    EdgeSpacerTaffyNodes for edge_a_c1 (merged):
      rank_to_spacer_taffy_node_id:          { rank=1: <same_level_spacer_node_id> }
      cross_container_spacer_taffy_node_ids: [<cross_container_spacer_node_id>]

The cross-container spacer sits in rank 0 of `t_c`'s rank containers alongside `t_c0`. After
layout its absolute position is used by the edge path builder as a waypoint inside the container,
ensuring the path routes around `t_c0` as it descends toward `t_c1`.