bat-cli 0.31.0

Blockchain Auditor Toolkit (BAT)
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
# The contract map — how `bat-cli map` should draw a system's shape

This document is the design behind `map`: one frame that answers "which contracts talk to
which, which one everybody writes to, and what is wired to what at construction". It exists
because the first attempt (`src/batbelt/evm/miro/map.rs` on `feature/architecture-map`) was
honest about the data and unreadable as a picture: one box per contract, one arrow per relation,
the deploy's left-to-right Sugiyama, and the auditor's verdict was that arrows flew across several
boxes, interfaces stood beside the contracts that implement them, and nothing said what to look at
first. The interface half has since been fixed in `relations()` (a type name collapses into its
implementer when that is a fact). What is left — and what this document decides — is the model
behind the nodes and edges, the layout, the reading order, and what the map must refuse to draw.

It is written in three parts, in the order the decisions were reached: what the people who do this
well actually do (§1–§3), the design that follows from it against the numbers this tool's own scans
produce (§4–§10), and the implementation against the code as it is today (§11–§14). Nothing here
changes code. Line references are to `develop` at 0.27.0 plus the uncommitted `map.rs`.

---

## 1. What audit firms draw, and what they leave off

Nobody who publishes a system overview draws every contract in the repository. ConsenSys
Diligence is the only firm with a stated, repeatable legend, and its definition of the unit is the
one that matters here: the overview "describes the top-level/**deployable** contracts, their
inheritance structure and interfaces, actors, permissions and important contract interactions"
(Arbitrum audit, 2021, §3 —
<https://docs.arbitrum.io/assets/files/2021_11_05_consensys_diligence_security_audit_core_contracts_token_bridge-664fbe3e5a14a41acaee4af64ae06100.pdf>).
Inheritance is "a yellow dashed row at the top of the contract", a `using for` library "a green
dashed row" — rows *inside* the box, never edges between boxes — and access-control modifiers are
"yellow bubbles in front of methods". The figures are per subsystem ("core", "bridge
peripherals"), not one all-in picture, and the deploy wiring ("RollupCreator … configures the
NodeFactory, ChallengeFactory") is prose beside the figure.

Trail of Bits does not put architecture diagrams in code reviews at all; its threat-modelling
deliverable identifies "all in-scope system components", places "a trust boundary anywhere that
security controls gate connections between components", and enumerates threats over "connections
that cross trust boundaries" (<https://blog.trailofbits.com/2025/02/28/threat-modeling-the-trail-of-bits-way/>).
OpenZeppelin, Zellic, Sigma Prime and Spearbit write the overview as prose plus a roles table; the
thing every one of them records somewhere is the out-of-scope boundary ("all the contracts that the
in-scope contracts interact with work correctly", Across audit —
<https://www.openzeppelin.com/news/across-audit>) and who holds which admin key. Contest READMEs
(Code4rena's LoopFi, <https://github.com/code-423n4/2024-10-loopfi>) draw actors and core contracts
and list external integrations; Sherlock's guide names the pre-code step as "map out the trust
boundaries (who can call what, and under which conditions), and diagram the token flow"
(<https://sherlock.xyz/post/a-complete-guide-to-solidity-security-audits-for-web3-protocols>).

Three things to carry forward. The node is the deployable contract. Inheritance and libraries are
part of a node, not relations between nodes. And the boundary — what the audited code calls that it
does not contain — is always written down, but collapsed: the far side is a black box.

## 2. What the Solidity tools draw, and what their own flags admit

Slither's `call-graph` is a function-level DOT graph with one cluster per contract
(<https://github.com/crytic/slither/wiki/Printer-documentation>); Secureum files it under "in-depth
review" and recommends `human-summary` and `contract-summary` for first contact
(<https://secureum.substack.com/p/audit-techniques-and-tools-101>). Surya's `graph` is also
function-level, and its README is the most candid source on readability: `-l` exists "to prevent
libraries like SafeMath from polluting the call graph", and `-s/--simple` "makes the command chart
only the contract call graph, instead of the function call graph. It's super useful for
higher-level analyses!" (<https://github.com/ConsenSys/surya>). sol2uml ships a depth limit and six
`hide` flags (<https://github.com/naddison36/sol2uml>). The pattern is the same everywhere: the
tool draws everything, then grows switches to draw less, and the thing practitioners praise is the
summary, not the graph. Dreamlab's comparison singles out Slither because it lets "written variables
be seen along with the authorisation checks that evaluate msg.sender"
(<https://dreamlab.net/en/blog/post/ethereum-smart-contracts-visualisation-tools-surya-solgraph-slither-piet-sol2uml-and-sol-functi/>).

So the contract-level map is the view the field already asked for (Surya `-s`) and never got a
good drawing of, because a DOT render of a contract graph still has every library and interface as
a node. This tool has one edge over all of them, and the capability-ideas note already names it:
`contract_in_scope` resolves a name through the caller's import graph the way `solc` does, so a
repository that vendors three `BeaconProxy`s produces one node where Slither produces a wrong one.

## 3. What the layout and readability literature says

**Node-link or matrix.** Ghoniem, Fekete and Castagliola (2004/2005,
<https://datavis2020.github.io/pdfs/ghoniem2004.pdf>) tested both on random graphs of 20, 50 and
100 nodes at densities 0.2–0.6: "when graphs are bigger than twenty vertices, the matrix-based
visualization performs better than node-link diagrams on most tasks. Only path finding is
consistently in favor of node-link diagrams." The per-task numbers are what decide the design: on
"is there a link between A and B", matrices held ~98 % correct at every size while node-link fell
to 67 % on large graphs; on "which node is most connected", matrices ~98 % regardless while
node-link degraded with size and density; on "find a path", node-link scored 99 % versus 70 % on
small graphs. Two caveats apply directly. Their graphs were random at density ≥ 0.2 — a contract
graph is far sparser (§4) — and their own discussion qualifies path-finding: it "is difficult to
carry out visually when the distance between the extremities is greater than two or three arcs".
NodeTrix (Henry, Fekete, McGuffin 2007, <https://arxiv.org/pdf/0705.0599>) states the resulting
rule: "Node-link diagrams are good for sparse networks, matrices are good for dense ones", and
mixes them. Lattix's DSM paper (Sangal et al., OOPSLA 2005,
<https://groups.csail.mit.edu/sdg/pubs/2005/oopsla05-dsm.pdf>) is honest about the cost: "following
the edges of a relation transitively requires going back and forth between row and column indices
rather than simply following arrows along a path".

**Crossings.** Purchase (1997) found "reducing the number of edge crosses is by far the most
important aesthetic"; her 2001 UML study, with crossings held at zero, found bends mattered a
little and node distribution, edge length, symmetry, orthogonality not at all — and that diagrams
with most edges pointing *upward* read worse
(<https://courses.ischool.berkeley.edu/i247/f05/readings/Purchase_UML_OzIV01.pdf>). Kobourov,
Pupyrev and Saket (GD 2014, <https://www2.cs.arizona.edu/~kobourov/crossings.pdf>) found the
crossing effect "significant for small graphs but not significant for large graphs" — a 10–30 node
map is exactly where it counts.

**Levelisation.** Structure101's Levelized Structure Map puts items "into rows, or levels, so that
as far as possible every item depends on at least one item on the level immediately below it … and
items on the lowest level do not depend on any other items", and claims the payoff this map needs:
"This arrangement conveys a lot of dependency information so that the specific item-to-item
dependency arrows can be hidden without loss of context"
(<https://www.sonarsource.com/structure101/docs/java/studio5/Content/restructure101/lsm.html>).
Cycles become "feedback dependencies" drawn in red, after computing a minimum feedback set. The DSM
tradition says the same with a matrix: "the dependency relation should be acyclic … the matrix
should be lower triangular", a cycle "is literally a square block straddling the diagonal", and the
ordering pushes "elements with many inbound relationships (providers) … to the bottom, while the
elements with many outgoing relationships (consumers) have shifted to the top"
(<https://dsmsuite.github.io/dsm_overview.html>). NDepend puts a number on the alternative: its
box-and-arrow graph "was not able to show more than a few dozens boxes and then became
un-understandable" (<https://www.ndepend.com/docs/dependency-structure-matrix-dsm>).

**The dot rules worth borrowing.** Gansner et al. (1993, <https://www.graphviz.org/documentation/TSE93.pdf>)
break cycles by reversing DFS back edges rather than collapsing the cycle, because "even when a
procedure call graph has cycles, one still expects to see top-level functions near the top"; they
replace an edge spanning more than one rank by "chains of unit length edges between … virtual
nodes" so a long edge takes a slot in every rank it crosses; and `constraint=false` lets an edge be
drawn without ranking (<https://graphviz.org/docs/attrs/constraint/>). The dot guide's advice on
oversized graphs is surgery, not knobs: collapse, `tred`, then smaller separations
(<https://graphviz.org/pdf/dotguide.pdf>). dependency-cruiser's `archi` reporter "exists to make
high level dependency overviews" by collapsing modules to folders, and calls depth limits "typically
a terrible idea" because they drop edges silently
(<https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md>). Holten's
edge bundling needs waypoints this tool does not have, but its second benefit transfers: a collapsed
group "visualizes implicit adjacency edges between parent nodes"
(<https://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Holten06.pdf>).

**How many edge kinds.** Healey found five colours detected "rapidly and accurately", seven to nine
"mixed" (<https://www.cs.ubc.ca/sites/default/files/tr/1996/TR-96-10_0.pdf>); the Pearson guide warns
that more than six "can be especially problematic when marks are small"
(<https://accessibility.pearson.com/resources/dataviz/>). Connectors are small marks. madge,
dependency-cruiser and NDepend each ship one to three edge styles plus one highlight.

---

## 4. The numbers this tool actually produces

Decisions below were made against four scans on this machine, not against the literature's random
graphs. The measurements are from `jq` over `BatMetadata.json` and from `map --dry-run`.

**Everlong** (`audits/everlong/EverlongLabs-blockend-2`, scanned by 0.27.0): 61 in-scope names,
of which only **20 are `Contract`**; 25 are interfaces and 16 are libraries. The current map draws
98 relations with boundaries, 41 of them "leaves the code". Every `FLAMM*Lib` is used by exactly
one deployable contract, `FLAMM` (directly or through another `FLAMM*Lib`); `MMRouterLib` only by
`MMRouter`; `TrancheMathLib` only by `TrancheController`; `RouterDeployLib` and `PoolDeployLib` only
by `FLAMMFactory`. Folding libraries into their hosts turns "`FLAMMFlowLib` calls `MMRouter`" into
"`FLAMM` calls `MMRouter`", which is what the chain actually does. `interfaces[]` is **empty** in
this scan — interface collapsing can only come from `base_contracts` and `resolutions`. In-degree
after folding concentrates on four names: `FLAMM`, `MMRouter`, `EverlongCore`, `PriceFeed`.

**Ascend** (`audits/ascend/basedmd-ascend/contracts`): 38 in-scope names, **18 `Contract`**, 9
interfaces, 10 libraries; after the interface collapse, 13 boxes and 18 relations without
boundaries, 53 with. The 35 boundary edges are dominated by `IERC20`/`IERC20Metadata` (7 contracts)
and by four names that are not contracts at all — `PoolId` (a type alias), `CurveStep[]`,
`AccountingState`, `Limits` (structs) — which reach the map through `wired_at_construction`, whose
only filter is "starts with a capital letter". `IAscendLaunchpad` still fails to collapse because
`IListingLaunchpad`, an interface, also lists it as a base, so "exactly one implementer" counts two.
21 of the 34 in-scope relations are construction wiring; only 3 are typed calls.

**Berachain** and **evm-deps-poc**: 16 and 17 in-scope names once the flag is read under its old
serialised name, `external` (a first `jq` over `.vendored` missed the alias and reported 855 and
62). Berachain's six relations included `PendingAddress`, `PendingUint192` and
`mapping(Id => PendingUint192)` as "unresolved bindings" — struct and mapping types leaking through
the same gap as Ascend's; with the type check in place it has two relations. A scan that genuinely
predates the flag would show every `lib/` contract in scope, and that case is handled by refusal
(§13) even though no scan on this machine exhibits it.

Density, then, is nowhere near Ghoniem's 0.2: Ascend is 18 edges over 13 nodes (0.11 of n²),
Everlong after folding is on the order of 30 edges over 18 nodes (0.09). At that density and size,
node-link is the right primary artefact and the matrix is a companion for the lookups node-link is
bad at. A system of eighty deployable contracts does not appear in any scan on this machine; §10
says what to do when one does.

---

## 5. What a node is

**A node is a deployable contract**: `contract_type == Contract`, not vendored, not on the ignore
list. This is C4's container ("a deployable unit", <https://c4model.com/diagrams/container>) and
Diligence's "top-level/deployable contracts". Everything else is a part of some node or a fact
about the system's edge, and is drawn as such:

- **An interface collapses into its implementer** when that is a fact — a recorded resolution, or
  exactly one deployable (non-interface, non-abstract) contract declaring it as a base. This is
  what `relations()` does now, with one correction: implementers are counted among deployable
  contracts only, so an interface that extends another no longer blocks the collapse. Several
  deployable implementers and no resolution is a question, kept as such (§6).
- **A library folds into every deployable contract that uses it**, transitively through other
  libraries. A library has no address and runs in its caller's context; a relation it makes is a
  relation its host makes. Everlong's sixteen libraries become zero boxes and no information is
  lost, because the deploy already draws each library function in full inside its caller's cluster.
  A library that no deployable contract reaches (`EverlongStrategy`, `Mul512` via maths-only
  callers) is listed in the report as unused in scope and not drawn.
- **An abstract contract folds into every deployable contract that derives from it**, for the same
  reason: its code is the derived contract's bytecode. An abstract base nothing in scope derives
  from is listed, not drawn.
- **A contract with no source in the repository is not a node.** It is an entry in the outside
  panel (§8). The current `--with-boundaries` draws one box per interface and makes `IERC20` the
  best-connected thing on the map, which teaches nothing.
- **A proxy** (`FLAMMProxy is BeaconProxy`, empty body) is a deployable contract and gets a box;
  the scan records no delegation, so it has no edges and lands in the "not connected" strip (§7).
  The report says so rather than pretending it is unrelated to `FLAMM`.

The node is keyed by **(file path, name)**, not by name. Two in-scope contracts can share a name in
different files (a `Treasury` in `src/` and one in `test/`); the label carries the file's last
directory only when the name is ambiguous, the way `deploy` prints `from <file>` only when it has
to.

## 6. What an edge is, and how many kinds a reader can hold

An edge is a relation between two nodes, never a count — that rule from the current `map.rs`
stands. What changes is the encoding. The current four colours (blue call, green wiring, red
unresolved, orange boundary) are one too many, and the reason is not the number but that colour
was carrying two different questions at once: *what kind of thing is at the far end* and *how sure
is the scan*. Separating them gives three line styles in one colour, one colour exception, and
one node mark:

- **Solid, black: a call the scan can pin** — a `resolved_call`, an unresolved call whose
  interface collapsed (§5), a static `Contract.fn` call found in `function_dependencies`, or a
  call that reaches the target through a folded library or base. This is almost every edge.
- **Dashed, black, captioned `?`: a binding the auditor has not answered** — an interface with
  two or more deployable implementers and no `bat-cli resolve`. The caption is the one place a
  label earns its cost: the report prints the exact `bat-cli resolve IFace Contract` to run, and
  the dashed arrow is the picture of that open question. Drawn to *every* candidate, because the
  honest picture of "one of these three" is three dashed arrows, not a guess.
- **Dotted, grey, captioned `wired`: wired at construction and never called.** When a pair is
  both wired and called the call wins and the wiring is not drawn, exactly as today; the dotted
  edge survives only for the pair that is wired and *not* called — an immutable `oracle` no function
  ever uses, or a wiring the scan could not follow a call through. In Ascend 21 of 34 relations
  are wiring; most will disappear under a call once libraries fold, and the few that remain are
  worth a glance precisely because they are odd.
- **Red: an arrow that goes up.** A cycle, after cycle-breaking (§7). Structure101's "feedback
  dependency in red" and the DSM's above-diagonal cell. A red arrow is the one mark on the map that
  is itself a finding candidate — two contracts that each depend on the other is where re-entrancy
  and ordering bugs live — so it gets the only colour.
- **An amber border on a box: this contract calls past the audited code.** Boundary is a property
  of the node, not an edge kind, drawn in the amber the deploy already uses for "reaches outside"
  (`#fff7ec` frame fill, `diagram-deploy-design.md` §19). What it calls is in the outside panel.

That is three strokes, one colour, one border, which sits under Healey's five. What is deliberately
*not* an edge: inheritance (part of the node, §5), `using for` on vendored libraries
(`SafeERC20` is not a relation between two contracts), events, function names, and the number of
calls. The deploy answers all of those for one entry point; this map is not that.

## 7. The layout: levels from the bottom, arrows pointing down

**Top-down, not left-to-right.** Every layered tool and every DSM shares one visual contract: the
reader infers "A depends on B" from vertical position, and an upward arrow is the exception. The
deploy runs left-to-right because a screenshot is wide and short and a caller's call sites read top
to bottom; a map's nodes are small boxes and the thing being read is hierarchy, for which
"down" is the convention a reader already carries from every org chart and every Graphviz render.

**Levels by longest path to a sink** (Structure101's levelisation), not longest path from a root
as `layout_graph` does. A node that calls nothing in scope is on the bottom row; a node's level is
one more than the highest level among what it calls. This has three consequences that a from-root
layering does not deliver. Every sink is on the bottom row, so the row *is* the answer to "what
does everything depend on" — `EverlongCore`, `PriceFeed`, `DepositAllowlist`, `TrancheToken` in
Everlong; `Treasury`, `StakingQuoteOracle`, `StakerDistributor` in Ascend. The levelling is stable
under rescans: adding a new periphery contract never re-levels the core, whereas from-root layering
re-levels everything when a deeper entry appears. And there is no root to choose — the current code
picks "the first contract nothing points at", which is an arbitrary name on a map that has a dozen
entry contracts.

**Cycle breaking before levelling**, as dot does: a DFS over the whole graph, started from the
sources (in-degree 0) in name order and then from every remaining node, marks back edges; those
are excluded from levelling and drawn red, upward, with Miro routing them. `layout.rs` already has
this exact routine (`split_back_edges`, `layout.rs:313`), and it already seeds its DFS with every
node after the root, so it needs only to accept "no root". Starting from the sources is what makes
the reversed edge the natural one (`MMRouter → FLAMM` is the feedback in Everlong, not the other
way), which matches dot's argument that top-level nodes should stay on top.

**Within a row, order by barycentre with crossings counted honestly**, reusing `order_layers` and
`count_crossings` (`layout.rs:441`, `:518`); the call-line fraction becomes 0.5 for every edge since
a box has no call sites. Ties are broken by the node's group (§9) and then by name, so the picture
is deterministic across runs — ELK's `considerModelOrder` makes the same choice, a stable order
over a marginally better crossing count, because the auditor will look at this map many times.

**The bottom row is ordered by in-degree from the centre outward**: the most-called contract in the
middle, then alternating left and right. Arrows then converge on the hub from both sides with the
shortest average length, and the hub's position says what its in-degree says.

**Long edges get a corridor, and are drawn as a chain.** This is the only answer to "arrows cross
several boxes", and it is the deploy's own answer (§1 of `diagram-deploy-design.md`): Miro routes,
so a crossing is removed only by changing what is near what. An edge that skips a row reserves a
slot in every row it crosses — `insert_bend_points` (`layout.rs:1401`) already does this and
`GraphLayout.routes` already returns the bend points — and is uploaded as a chain of elbowed
connectors through invisible `create_anchor_marker` shapes at those points, with `ArrowEnd::None`
on every link but the last. The intermediate boxes shift sideways by one gutter to make room; the
arrow runs down the gap instead of over a box. Arrows leave the **bottom centre** of the caller and
enter the **top** of the callee (`RelativeAnchor(0.5, 1.0)` → `(x, 0.0)`), so Miro's elbow is a
vertical–horizontal–vertical Z whose horizontal leg lies in the gutter between rows. On a hub with
many callers, the landing x is spread along the callee's top edge in caller order, so eight arrow
heads do not stack on one point.

**Rows are centred, not top-left aligned.** The deploy top-aligns because the entry point must sit
in the corner with the room below it; a map has no entry point, and a centred pyramid of three to
five rows reads as one shape. Row gap large enough for the horizontal legs — on the order of two
box heights.

**A strip on the right for the unconnected.** A deployable contract with no edges either way
(`FLAMMProxy`, `AscendCoreToken`, `BurnSink`) is not in any level. It goes in a labelled column at
the right edge, "not connected in scope", so it is visibly present and visibly apart. Hiding it
would break the auditor's rule; levelling it would put it on the bottom row beside the hubs, where
it would read as a foundation.

## 8. The outside panel and the matrix: the two things rendered as text

Both are rendered with `silicon::create_figure` the way `storage.rs` renders its slot table —
lines of text to a PNG — because that is the thing this tool draws best and because both are
tables, not graphs.

**The outside panel** sits under the bottom row: one line per external system, listing which
contracts reach it. "External system" means an interface type with no deployable implementer in
scope — in scope or vendored, it makes no difference — with the ERC-20 family (`IERC20`,
`IERC20Metadata`, `IERC20Permit`, `IERC20Errors`) merged into one line, "ERC-20 tokens", because
seven contracts touching tokens is a fact about every DeFi system and no fact about this one. A
struct, enum or type alias (`file_items`) is never an external system. The ordering is by how many
contracts reach each system, so the panel reads as "where value can leave, most-shared first". This
is STRIDE's rule applied literally: "collapse similar elements within a single trust boundary into
a single element", and "you don't trust what's on the other side"
(<https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/november/uncover-security-design-flaws-using-the-stride-approach>).
Drawn by default; `--no-outside` leaves it off.

**The matrix** sits to the right of the diagram. Rows are callers and columns callees, both in the
diagram's order (top row first, left to right within a row), so the matrix reads in the same order
the diagram does; the legend states the resulting convention explicitly, since the DSM literature
warns it flips between fields: a mark **above** the diagonal is an arrow that goes down the
diagram; a mark **below** the diagonal is a red arrow. Cells are `●` call, `?` unresolved, `·`
wired-only. The last column is "called by" (column sums) and the last row "calls" (row sums); the
"called by" column is the exact, sortable answer to "everybody writes to this one". It is drawn
whenever there are at least four nodes — at three the diagram already is the matrix — and it is the
part of the map that still works at eighty nodes (§10).

## 9. Grouping, and what is left out

**Group = the first directory under the program path** (`src/core`, `src/periphery`,
`src/listing`, `src/yield`) — the author's own subsystem boundary, which is what Diligence's
per-subsystem figures draw and what dependency-cruiser's `archi` collapses to. Groups are a
categorical tint on the box fill (up to six, the Healey limit; beyond six, no tint) and a tie-break
in row ordering, never a cluster boundary that fights the levels. The alternative — clusters as
rectangles with rows inside them — is what Graphviz does and what makes its contract graphs wide
and tangled, because a cluster forces its members together even when the levels say they belong
apart.

**Deliberately left out of the picture**, each with its reason: actors and roles (the scan has
`entry_points[].access_control`, but a map of who-can-call-what is a different lens and belongs in
a `--lens roles` later, not on this frame); function names and call counts (the deploy's job);
inheritance edges (inside the node, §5); vendored libraries used through `using for`; `new X()`
creation — the scan does not record it (§13); and delegation through proxies, likewise. The frame
title says what it is: `map: <project> — who calls whom`.

## 10. One contract, eighty contracts

**One contract** is drawn: one box, the outside panel under it, no matrix, and the report says
"one deployable contract in scope; every relation is with the outside". It is not an error: the
auditor asked for the system's shape and that is the shape.

**Eighty contracts** does not fit on one node-link frame, by Ghoniem's data and NDepend's "few
dozen boxes". The rule, in order of what to try: first, the map as designed — levelling and the
corridor chains hold up longer than a force layout, and the crossing count from `count_crossings`
is printed in the report so the auditor knows what they are looking at. Above **thirty** deployable
nodes, the frame becomes one frame per group (§9) with the other groups collapsed to a single box
each carrying the implicit aggregated edges (Holten's parent edges; dependency-cruiser's
`collapsePattern`), plus the matrix of everything, which does not degrade. `--focus <Contract>`
draws one contract and its neighbours, levelled, for the reader who wants the shape around one box
(dependency-cruiser's `--focus`). What is never done is a depth cut or a node cap: the deploy
removed `--max-depth` and `--max-nodes` for hiding code, and the same rule holds here.

## 11. In what order the reader takes the information

The auditor's question — "no sé cómo priorizar la información" — gets an explicit answer, and the
legend states it in that order:

1. **The bottom row**: what everything depends on. These contracts hold the state the system is
   about; their storage frames (`bat-cli storage`) are the next thing to open.
2. **The top row**: what nothing in scope calls — the entry contracts, where the outside gets in.
3. **The arrows, read downward**: a path from an entry contract to a foundation is a flow; the
   `effects` report of that entry point is the detail.
4. **The red arrows**: the cycles. Each one is a pair to think about before reading either.
5. **The dashed arrows**: the questions. Each has a `bat-cli resolve` line in the report; the map
   is not finished until they are solid or acknowledged.
6. **The amber borders and the outside panel**: where value leaves the code, most-shared first.
7. **The matrix**, for the exact lookups a picture is bad at: is there an edge, who is called by
   the most, and the column that ranks it.

This is the same order the report prints to stdout, so the terminal and the board say one thing.

---

## 12. The implementation, against the code as it is

The current `map.rs` has three parts worth keeping — `relations()` as the source of relation
facts (with the §5 corrections), `report()` as the stdout shape, and the placement via
`ColumnLayout::open_left` — and two to replace: `layout_map`, which calls `layout_graph` with an
arbitrary root, and `draw`, which draws four legend shapes and auto-routed box-to-box connectors.
In stages:

### Stage 1 — the model, pure and tested (`map.rs`, about a day)

Split `relations()` into a model that owns the node set and a relation collector over it.

- `deployables(metadata) -> Vec<Node>`: `Contract`-typed, non-vendored, not ignored, keyed by
  (file, name), with `group` from the first directory under the program path and `reaches_outside`
  from any `unknown_external_calls` whose type is an interface with no deployable implementer.
- `hosts(metadata) -> BTreeMap<(file,name), BTreeSet<node>>`: for every library and abstract
  contract in scope, the deployable contracts whose code it is. Bases come from `base_contracts`,
  transitively through in-scope non-interface bases. Library use comes from
  `function_dependencies.callees` — the strings are `Lib.fn` for static calls, as
  `auto_deploy.rs:5542` documents and as the Everlong sample confirms (`FLAMMGateLib.anchor`,
  `FLAMMStore.grant`) — resolved with `contract_in_scope(target, caller.file)` and iterated to a
  fixpoint so a library used only by a library reaches its host. A library on the ignore list is
  still folded; ignoring hides a box, never a relation.
- `resolve_type(name, from_file) -> Resolved::{Node, Candidates(Vec), Outside, NotAType}`: the
  existing closure, with implementers counted among deployables, the `resolutions` map first, the
  `file_items` struct/alias/enum check that returns `NotAType`, and a vendored interface with no
  deployable implementer returning `Outside`. `implementations_of` (`auto_deploy.rs:6002`) encodes
  the same priorities; the cleanest move is to lift it into `EvmBatMetadata` and have both call it.
- `relations()` then records, per deployable node *and per node hosting each library or base*:
  `resolved_calls` (`Call`), `unresolved_calls` through `resolve_type` (`Call`, one `Unresolved`
  per candidate, or `Outside`), static `Contract.fn` callees from `function_dependencies` that
  resolve to a deployable (`Call`), `wired_at_construction` through `resolve_type` (`Wired`), and
  `unknown_external_calls` (`Outside`, into the panel). Self-relations drop; `Call` beats `Wired`
  on a pair as today; `Outside` never becomes an edge.
- The report gains sections in the §11 order, the `bat-cli resolve …` lines, the folded libraries
  ("FLAMMFlowLib → part of FLAMM"), the unused and the unconnected, and the crossing count.
- Tests, in `map_test`: the `IListingLaunchpad` case (an interface extending an interface does not
  block a collapse); a struct-typed immutable is not an edge; a library used by a library folds to
  the host; wired-and-called keeps the call; wired-only survives; two implementers and no
  resolution produce two dashed edges; a vendored interface with no implementer is outside.

`--dry-run` prints the report and the matrix as text and stops, as now.

### Stage 2 — the level layout (`layout.rs`, about a day)

Add `layout_levels(nodes, edges, config) -> GraphLayout`, top-down, rootless. It reuses
`split_back_edges` (generalised to take an optional root; it already seeds every node), a new
`assign_levels` (longest path to a sink over the forward edges), `insert_bend_points` (unchanged:
it works on any layer assignment), `order_layers` and `count_crossings` (unchanged; the
`from_line_fraction` is 0.5 for every edge), and returns `PlacedNode`s in frame-local coordinates
with rows centred. `back_edges` and `routes` come back exactly as `layout_graph` returns them, so
the drawing code consumes one shape for both. The bottom-row centre-out ordering is a final pass
over level 0. `LayoutConfig` gains nothing; the map passes its own `gutter_y`.

Tests: a chain levels into one node per row; a diamond puts the shared sink on the bottom; a cycle
yields one back edge and still levels; a source that reaches only a sink lands on level 1; the
unconnected are not in `nodes` (the caller strips them first); crossings on a known
two-row graph equal the hand count.

### Stage 3 — drawing (`map.rs` + two client helpers, about a day)

- `MiroClient::create_map_node(frame, name, subtitle, fill, border, href)`: a `create_label` with
  two text lines (`<p><b>name</b></p><p>called by N · calls M</p>`), a fill for the group tint, an
  amber border when it reaches outside, and an `<a href>` to the contract's column label on the
  board when `miro.auto.columns[]` has one (`ContractColumn.label_id`), so the map doubles as the
  board's index — the same `moveToWidget` trick the link cards use.
- Legend and outside panel: two `silicon::create_figure` PNGs, uploaded with
  `create_image_in_frame`; the legend text is the §11 list in the map's own words.
- Matrix: a third PNG, `.txt` so nothing is highlighted, column headers as numbers when names
  would not fit, every line parsing (the storage table's lesson about an unbalanced `(`).
- Connectors: for each forward edge, markers at `routes[i]` via `create_anchor_marker`, then a
  chain of `create_connector` (elbowed) through them with `ArrowEnd::End` on the last only; the
  landing anchor x spread along the callee's top edge in caller order. Back edges: one red
  elbowed connector from the caller's top to the callee's bottom, Miro-routed. Dashed and dotted
  through `ConnectorStroke`; captions through `ConnectorStyle.caption`.
- Frame: `create_frame`, `open_left` as now, no fill. Record `{frame_id, node_ids}` under a new
  `miro.map` so a later run can print the previous frame's URL; nothing is deleted, per the rule
  that just removed replacement from `storage` and `effects`.
- Concurrency as today: boxes and PNGs in one `JoinSet`, then markers, then connectors.

### Stage 4 — scale (later, when a scan needs it)

`--focus <Contract>` (neighbours only, same pipeline), and above thirty nodes one frame per group
with foreign groups collapsed to one box each. Both are the same model with a filter applied before
Stage 2; neither needs new drawing code.

## 13. The edge cases, and what each one does wrong by default

- **A scan without `vendored`**: every `lib/` contract is in scope, so the map would have `Vm` and
  `Test` as nodes. Detected as "nothing is vendored and some contract sits under a `lib/` segment"
  (`predates_vendored`) and refused with "run `bat-cli sonar`". The field is read under both
  `vendored` and its older name `external`, so a scan from 0.27.0 with the old spelling is not
  refused — it was never the problem.
- **Same-named contracts from `lib/`**: never a node (vendored), but a *target*: `resolve_type`
  must go through `contract_in_scope(name, caller.file)`, never `get_contract_by_name`; a vendored
  `ERC20` base of `AscendCoreToken` is part of that node, not a boundary, which is what the
  current dry-run draws (`AscendCoreToken → ERC20 leaves the code`).
- **Two in-scope contracts with one name**: keyed by (file, name); labelled with the directory;
  the report prints both paths.
- **A contract only wired, never called**: the dotted `wired` edge, and a line in the report.
- **An interface extended by another interface** (`IAscendLaunchpad`): count deployable
  implementers only.
- **A struct, enum or alias typed as a relation** (`Limits`, `PoolId`, `CurveStep[]`,
  `mapping(Id => PendingUint192)`): `NotAType`; strip `[]` and `mapping(…)` before the lookup.
- **A call into an inherited vendored base** (`super._mint`): the base folds into the node; the
  call is a self-call and is dropped.
- **Cycles**: broken from the sources, drawn red upward, listed in the report as pairs; a
  three-node cycle reverses one edge, not three, and the matrix shows the block.
- **A hub with twenty callers**: landing anchors spread along the top edge; if the edge count on
  one box exceeds what its width can space at the stroke width, the box widens — never fewer
  arrows.
- **A library used by nobody deployable**: listed, not drawn. **A library used by everyone**
  (`ReentrancyGuardLib`): folds into every host, producing no edge at all, which is correct — a
  guard is not a relation.
- **One contract**: one box plus the outside panel; no matrix. **Zero**: stop with "no deployable
  contract in scope".
- **Eighty contracts**: §10. Nothing is cut; the frame per group and the matrix carry it.
- **The ignore list**: a box is left out, its relations are not — calls *to* an ignored contract
  are dropped with the box (there is nothing to land on) and the report says what was skipped,
  matching `deploy`.
- **Interface collapse that lies**: a contract implementing an interface without `is IFace` is
  invisible to `base_contracts`; `bat-cli resolve` is the escape hatch, and the dashed arrow with
  its `resolve` line is the prompt.
- **`new X()` creation**: not in the metadata. A factory that only deploys (`FLAMMFactory` and
  its `RouterDeployLib`/`PoolDeployLib`) shows calls to what it configures and nothing for what it
  creates. Accepted as a gap and named in the legend until `sonar` records `new` targets.

## 14. What to measure before calling it done

Three dry runs and one real frame. The dry runs: Everlong (expect ~18 boxes, four hubs on the
bottom row, `MMRouter → FLAMM` red, zero struct-typed edges), Ascend (13 boxes, `IAscendLaunchpad`
gone, the outside panel with "ERC-20 tokens" first), and a scan with one contract. The real
frame: Everlong, checked for arrows that cross a box (should be none that skip a row), the
crossing count printed versus counted by eye, the matrix order matching the diagram, every href
landing on a column label, and the frame to the left of everything. Then a second run to confirm
nothing was deleted and the previous URL was printed.


---

## Frames, not one picture (2026-10-13)

Five passes were spent on colour, lanes, anchor spread and hub ordering, and the auditor's
verdict stayed the same: "no logro seguirlo". The defect was never the styling. One frame with
sixteen contracts and twenty relations is a mesh at any zoom, and the three things that fixed
exactly this for entry-point diagrams were sitting unused in `auto_deploy.rs`
(`docs/diagram-deploy-design.md` §4, §5, §13):

1. **Partition** — `map` now draws ONE FRAME PER TOP CALLER (a contract nothing in scope depends
   on). Its frame holds everything it reaches, so the frame answers one question: what does this
   contract talk to. A contract two frames both use is drawn in both. That repetition is the
   deliberate trade from §4: a repeated box is cheap, a crossing arrow is not.
2. **Localize** — a shared contract whose caller sits ≥2 columns back gets a local copy
   (`localize`, ids `Name#2`). §5's warning applies unchanged: the distance is only the heuristic
   for WHICH box to copy.
3. **The certificate** — `GraphLayout::crossings` is now returned by the layout engine and
   printed per frame by `--dry-run`. A copy that does not lower it is undone. This is the part
   that stops the guessing: a change to the picture is judged by a number before anyone looks.

Measured on ascend (13 in-scope contracts, 46 relations with the outside drawn): six frames,
crossings 2 / 0 / 0 / 0 / 0 / 0 — against one frame that no ordering brought under a tangle.

Two decisions that came with it, both from the auditor:

- **The outside is drawn**, grey and named. Telling someone a contract "calls past this
  repository" without naming what it calls is not information they can act on. `--no-boundaries`
  replaces `--with-boundaries` — the default inverted.
- **The matrix is gone.** It made the reader learn a number per contract. The picture is the
  deliverable; the terminal report carries what the picture cannot.


---

## Frames, not one picture (2026-10-13)

Five passes were spent on colour, lanes, anchor spread and hub ordering, and the auditor's
verdict stayed the same: "no logro seguirlo". The defect was never the styling. One frame with
sixteen contracts and twenty relations is a mesh at any zoom, and the three things that fixed
exactly this for entry-point diagrams were sitting unused in `auto_deploy.rs`
(`docs/diagram-deploy-design.md` §4, §5, §13):

1. **Partition** — `map` now draws ONE FRAME PER TOP CALLER (a contract nothing in scope depends
   on). Its frame holds everything it reaches, so the frame answers one question: what does this
   contract talk to. A contract two frames both use is drawn in both. That repetition is the
   deliberate trade from §4: a repeated box is cheap, a crossing arrow is not.
2. **Localize** — a shared contract whose caller sits >= 2 columns back gets a local copy
   (`localize`, ids `Name#2`). §5's warning applies unchanged: the distance is only the heuristic
   for WHICH box to copy.
3. **The certificate** — `GraphLayout::crossings` is now returned by the layout engine and
   printed per frame by `--dry-run`. A copy that does not lower it is undone. This is the part
   that stops the guessing: a change to the picture is judged by a number before anyone looks.

Measured on ascend (13 in-scope contracts, 46 relations with the outside drawn): six frames,
crossings 2 / 0 / 0 / 0 / 0 / 0 — against one frame that no ordering brought under a tangle.

Two decisions that came with it, both from the auditor:

- **The outside is drawn**, grey and named. Telling someone a contract "calls past this
  repository" without naming what it calls is not information they can act on. `--no-boundaries`
  replaces `--with-boundaries` — the default inverted.
- **The matrix is gone.** It made the reader learn a number per contract. The picture is the
  deliverable; the terminal report carries what the picture cannot.


---

## The frame is a tree, and there are no cards (2026-10-13)

The hub cut from earlier the same day did what it promised — no box drawn twice, 8 frames of at
most 6 boxes — and the auditor rejected it:

> "listing desk es intermedio, y tampoco es de alta jerarquía, entonces tantos frames confunden…
> esos 2 frames perfectamente podrían ir juntos en 1 solo frame… quiero menos frames"

The goal was never "fewest boxes". It is: **a frame per contract of high hierarchy, and that
frame self-contained**, so the set of frames IS the high-level view of the system.

So:

- **Roots** are the top callers (nothing in scope depends on them) plus any cycle nothing sits
  above (`StakingFeeConverter`). Nothing else gets a frame. A cycle has no top caller, so one
  member has to be chosen: it is **the one that calls the most**, ties by name — the member
  whose frame tells the most of the story, rather than whichever the relations happened to list
  first.
- **`unfold`** turns each root's reachable sub-graph into a TREE: a contract reached two ways is
  drawn once per branch, its chain copied with it. A node already on the current path is a
  cycle, and gets the one red arrow home instead of unfolding for ever.
- Nothing inside a frame is shared, so **nothing can cross** — `localize`, the lanes' lane
  assignment and the crossing certificate all agree at 0 on every frame of ascend.
- Bounded at 400 boxes per frame: unfolding a dense DAG can blow up in principle. On the systems
  this is pointed at (16 contracts, 20 relations) the worst frame is 23 boxes. Recycling a box
  when the bound is hit is intense logic for a problem nobody has yet.

Ascend: **6 frames**, 83 boxes, 0 crossings anywhere — against 8 frames and a card trail.

One trap worth recording: `covered` compares relations by ORIGINAL name on both ends. A drawn
edge carries the id of a copy (`IERC20@YieldRouter`, `Treasury#2`), the raw relation carries the
name, and comparing the two made every call leaving the repository look undrawn — which turned
every contract that makes one into a root of its own: fourteen frames for five systems.