Mímir lenses: the user guide
This is the one guide for running the Mímir graph lenses on a Rust workspace. It covers installing the tools, making a dump, running every lens, writing a target architecture, and reading the reports. The reference manual lenses.md has the full math, the code lines and the tests of each lens. This guide links to its sections as §4.x.
Every command below was run on build-1 on 2026-11-06, against Gunnar c17a6ef4 (the default build and the metered delivery build) and Holger h32/next 9dbe5cbf. The outputs are pasted as the tools printed them. A line that ends in … was cut for width.
Quick start
Five steps on a clean Debian / Ubuntu machine, from crates.io only. This was run in a bare debian:trixie-slim container on build-1 (fresh CARGO_HOME, no checkouts) on 2026-11-06:
sudo apt-get install -y build-essential pkg-config curl git libgraphblas-dev # GraphBLAS 7.x
rustup toolchain install nightly-2026-10-05 --profile minimal --component rustc-dev,llvm-tools-preview,rust-src
cargo +nightly-2026-10-05 install --locked cargo-mimir-dump # the dump tool (a rustc driver)
cargo install --locked dwarves-lens # the lenses: cargo lens <lens>
cargo lens graph-package --local && cargo lens all --limit 3 # dump this workspace, run every lens
The versions this guide was checked with:
| crate | version | what it is |
cargo-mimir-dump |
0.1.0 | the dump tool: cargo-mimir-dump, mimir-driver, mimir-capture, mimir-super (nightly-2026-10-05 only) |
nornir-mimir-schema |
0.1.0 | the dump's Arrow schema, under the dump tool |
dwarves-lens |
0.1.1 | the door: cargo-lens, dwarves-lens |
nornir-mimir-lens |
0.2.2 | the lens library under the door |
nornir-mimir-reach 0.1.1, nornir-mimir-graph 0.1.0, graphblas-sys 0.1.0 |
the layers under the library |
What the clean container printed (cut):
$ cargo install --locked cargo-mimir-dump # stable: the guard stops it
error: cargo-mimir-dump is a rustc driver: it builds only with nightly-2026-10-05 and its rustc-dev component.
This build uses `rustc 1.99.1 (58cf8f9c3 2026-10-13)`, not nightly-2026-10-05 (commit 282215592).
Install the toolchain once, then install with it:
rustup toolchain install nightly-2026-10-05 --profile minimal --component rustc-dev,llvm-tools-preview,rust-src
cargo +nightly-2026-10-05 install --locked cargo-mimir-dump
$ cargo +nightly-2026-10-05 install --locked cargo-mimir-dump
Installed package `cargo-mimir-dump v0.1.0` (executables `cargo-mimir-dump`, `mimir-capture`, `mimir-driver`, `mimir-super`)
$ cargo install --locked dwarves-lens
Installed package `dwarves-lens v0.1.1` (executables `cargo-lens`, `dwarves-lens`)
$ cargo lens --version
cargo-lens 0.1.1 (nornir-mimir-lens 0.2.2, git d67f0b7c)
$ git clone -q --depth 1 https://github.com/dtolnay/itoa && cd itoa
$ cargo lens graph-package --local
graph-package: 53 packages, 77 dependency edges, 50253 call edges (3830 into other packages)
wrote /work/itoa/target/dwarves/graph-package.znippy (sha256 5381d0e833757a0d18164020f5d0beced89580987980672bd92ce5b5450c0973)
$ cargo lens all --limit 3
size: 0 fns > 150 lines (0 > 300), 0 files > 5000 lines, of 10 fns in 3 files (529 lines, lower bound)
…
lens: nornir-mimir-lens 0.2.2 (git d67f0b7c)
twins: 0 twin groups over 10 fns (0 api pairs, 0 name+sig, 0 shape, 0 NEAR, 0 DIVERGENT = same name, different rule, 0 inlined; …
…
$ cargo lens all --mimir-dump /gdumps/gunnar-default --limit 1 # the Gunnar c17a6ef4 dump: 16 s
size: 81 fns > 150 lines (27 > 300), 5 files > 5000 lines, of 7246 fns in 403 files (301150 lines, lower bound)
twins: 304 twin groups over 7022 fns (344 api pairs, 121 name+sig, 229 shape, 150 NEAR, 30 DIVERGENT = same name, different rule, 5 inlined; …
verdict: crates: 0 RED / 1 AMBER / 20 GREEN; modules: 0 RED / 24 AMBER / 287 GREEN (77 small ones not banded); 3747 tests reach 5635 of 7022 …
fail-open: 0 decisions FAIL OPEN (a `Result<bool>` / `Option<bool>` decision collapsed with `unwrap_or(true)`: an error answers yes); 0 fail closed, 15 with the value unread; …
Without --src, fail-open cannot read the literals (15 values unread); give --src <checkout> as in §4. Install times in that container (12 jobs): the driver 39 s, the door 130 s, the itoa dump 12 s. Every report ends with its lens: line: compare numbers only between runs whose lens version and git hash match. The Gunnar numbers from crates.io are the same as the numbers of the build-1 source build of the same commit.
Contents:
- Quick start (above)
- What a lens is, and the dataflow
- Install
- Make a dump
- Run the lenses
- Every lens in one table
- The target architecture:
.nornir/architecture.ron - Reading
architecture-report/1 - The math, step by step
- Where to read more
1. What a lens is, and the dataflow
A lens asks one question of the code graph and answers with a report: a headline, summary counts, findings with file:line, and notes on what it cannot see. There are two kinds:
- Micro lenses find one shape each: a long fn, two fns that do the same thing, a
matchover another crate's enum, a decision that answers yes on an error. - Macro lenses judge the architecture: the layout the code wants, the layer order, the missing trait between two groups, who owns a concept, and how far the code is from a target you write down. They also price a refactoring before anyone does it.
All of them read one dump: the facts rustc knows after type checking, one set of Arrow IPC files per compiled crate. A dump holds one build, meaning one feature set.
your workspace
│ cargo mimir-dump (rustc driver, pinned nightly, after_analysis)
▼
dump dir: <crate>@<ver>-<hash>.{nodes,edges,spans,symbols,mir}.arrow
│ Graph::load (nornir-mimir-lens; exact cache .dwgc in $DWARVES_GRAPH_CACHE)
▼
Graph: nodes (fn, method, struct, enum, trait, impl, field, mir_block, …)
edges (CALLS, CONTAINS, IMPLEMENTS, FOR_TYPE, USES_TYPE, READS_FIELD, WRITES_FIELD, …)
│ GraphBLAS matrices (nornir-mimir-graph over SuiteSparse:GraphBLAS 7.x)
▼
lens products: reach (LOR.LAND), counts (PLUS.TIMES), shortest paths (MIN.PLUS)
│
▼
Report { headline, summary, findings[{text, json}], needs, notes }
│ door: dwarves lens <name> / cargo lens <name> (--json, --limit 0)
▼
text on stdout, or one JSON object
The code lives in nornir, crate nornir-mimir-lens (crates/mimir-lens/). dwarves is only the command-line door: dwarves lens <name> and cargo lens <name> are the same verb.
2. Install
2.1 System packages
The lenses link SuiteSparse:GraphBLAS 7.x from the system. No copy is bundled.
sudo apt-get install -y build-essential clang cmake pkg-config protobuf-compiler \
git libgraphblas-dev # Ubuntu / Debian: GraphBLAS 7.4
At run time a built binary needs libgraphblas7 and libgomp1 only.
2.2 The dump tool
cargo-mimir-dump and mimir-driver are a rustc driver, built against one pinned nightly (nightly-2026-10-05, commit 282215592) and its rustc-dev component. The crate's build script checks both and stops with the install line when either is missing:
rustup toolchain install nightly-2026-10-05 --profile minimal \
--component rustc-dev,llvm-tools-preview,rust-src
cargo +nightly-2026-10-05 install --locked cargo-mimir-dump # cargo-mimir-dump, mimir-driver and two helpers
From a nornir checkout the same is cd nornir/crates/mimir-driver && cargo install --locked --path . (its rust-toolchain.toml picks the nightly). Keep cargo-mimir-dump and mimir-driver in one directory: the first looks for the second beside itself. Your workspace keeps its own toolchain.
2.3 The door: cargo lens / dwarves lens
cargo install --locked dwarves-lens # cargo-lens (cargo lens <lens>) and dwarves-lens
dwarves-lens is the lens door on its own: the same verbs as dwarves lens <lens> (dwarves calls this crate), with only nornir-mimir-lens and clap under it. The full forge (dwarves, 0.1.6 on crates.io) is not the way to the lenses: its optional parts are path crates of the sibling repos, so its lens build stays a source build (lenses.md §1.3). The crates.io crate named cargo-lens (0.1.1) is another project's; do not install it.
2.4 The library from crates.io
To call the lenses from your own code, depend on the published crate:
[dependencies]
nornir-mimir-lens = "0.2"
anyhow = "1"
use mimir_lens::lenses::{self, size::SizeArgs};
fn main() -> anyhow::Result<()> {
let dump = std::env::args().nth(1).expect("usage: libcheck <mimir dump dir>");
// the graph of a Mímir v2 dump (cached by DWARVES_GRAPH_CACHE)
let g = lenses::all::loaded(std::path::Path::new(&dump))?;
let r = lenses::size::run(&g, &SizeArgs::default())?;
println!("{}", r.text(3));
Ok(())
}
Built from crates.io on build-1 (the lock resolved nornir-mimir-lens 0.2.0 and graphblas-sys 0.1.0) and run on the Gunnar default dump:
$ cargo build --release && ./target/release/libcheck /gdumps/gunnar-default
size: 81 fns > 150 lines (27 > 300), 5 files > 5000 lines, of 7246 fns in 403 files (301150 lines, lower bound)
fns: 7246
fns_without_lines: 0
fns_long: 81
fns_very_long: 27
files: 403
files_big: 5
lines: 301150
lines_exact: false
files_counted_exactly: 0
FILE 9822+ crates/gunnar-server/src/control_plane.rs
FILE 7582+ crates/gunnar-cli/src/control.rs
FILE 6500+ crates/gunnar-common/src/cli.rs
… 83 more (--limit 0 for all, --json for every row)
needs file line counts: the dump holds items, not files: 403 of 403 counts are max(end_line) of the file's items (a lower bound); pass --src <checkout> holding the files for the exact count
note: fn length = end_line − start_line + 1 of the fn node (signature and body; attributes excluded); files marked `+` are lower bounds
The needs line is how every lens says what it could not answer, and what input would answer it.
3. Make a dump
Two dumps cover every lens (the reasons are in lenses.md §1.5):
cd <your workspace>
# every crate with its dependencies: the graph lenses read this one
cargo lens graph-package --local # writes target/dwarves/mimir
# your own crates only: the folds and cfg-sprawl read this one
cargo mimir-dump --workspace-only --out target/dwarves/mimir-ws \
--target-dir target/dwarves/mimir-target-ws --repo <name>
Things that bite:
-
One dump is one build. Code behind a
cfgthe build did not turn on is not in the dump. Gunnar'saccount_room_at()(accounts.rs:323) is behind#[cfg(feature = "metered")]: the default dump cannot see its fail-open. Dump the delivery build too, and give it tosingle-implanddyn-to-enumas--also-dump DIR(§5). -
-pand--featureswith--workspace-only.-p Xdumps only the named packages. With several-p, a package-prefixed feature (--features gunnar-server/metered) silently turns on nothing; give the features unprefixed:
cargo mimir-dump --workspace-only -p gunnar-server-metered --features metered \
--out target/dwarves/mimir-metered --target-dir target/dwarves/mimir-target-metered
Then check that a known cfg-gated item is in the dump (for Gunnar, cargo lens fail-open lists accounts.rs:323).
-
The
mirfile. Since dump schema 8 every body of every crate is in<unit>.mir.arrow, complete (tablecode_mir). Themir_blocknodes andCFG_SUCCedges the lenses read are no longer written: the loader derives them from that file, the same bytes as before, so no lens result changed. A dump without the file (an older driver) is read as before. The format, the instance pass (cargo mimir-dump --exec) and the executor that reads them are inexec-graph.md. -
A new
--outon a warm--target-dirwrites nothing. Cargo has nothing to recompile, so the dump is empty. Give each dump its own target dir.
4. Run the lenses
4.1 Every lens on one load: lens all
export DWARVES_GRAPH_CACHE=~/.cache/dwarves-graph # the exact graph cache: the second load is fast
dwarves lens all --mimir-dump target/dwarves/mimir --src . --limit 0 # text
dwarves lens all --mimir-dump target/dwarves/mimir --src . --limit 0 --json # one object keyed by lens
The default set is the 21 lenses that need no input: size, twins, dispatch-erosion, cycles, single-impl, error-shape, cfg-sprawl, dsm, misplaced, taint, verdict, taxonomy-drift, dyn-to-enum, foreign-helper-sprawl, late-validation, asymmetric-seam, deferred-init-sprawl, typestate-candidate, pipeline-subclass-leak, monolithic-router and fail-open. --src adds history-comments; --sink with --choke adds gate-bypass; --entry with --sink adds chokepoints; --sink adds runtime-guard-erosion. Name others with --lenses a,b,c. Each lens prints exactly what its own verb prints.
Wall time and peak memory on build-1 (release door, graph cache warm, --src given):
| dump | wall | peak RSS |
| Holger h32 9dbe5cbf | 13.7 s | 4.6 GB |
| Gunnar c17a6ef4, default | 12.6 s | 4.5 GB |
| Gunnar c17a6ef4, metered | 6.6 s | 1.5 GB |
The headlines of lens all on the Gunnar metered dump (cut at 150 characters):
size: 57 fns > 150 lines (20 > 300), 5 files > 5000 lines, of 5829 fns in 309 files (242210 lines)
twins: 230 twin groups over 5598 fns (280 api pairs, 69 name+sig, 228 shape, 163 NEAR, 14 DIVERGENT = same name, different rule, 3 inlined; 15 STRONG …
dispatch-erosion: 45 ladders / 222 arms over an enum of another CRATE in 44 fns, 34 / 204 over an enum of another MODULE of its crate (83 ladders at h…
cycles: 9 module cycles holding 76 modules (largest 45), 0 crate cycles; 289 modules, 1123 module edges
single-impl: 23 traits with exactly one production impl and 0 with none, of 53 traits (122 impls seen)
…
asymmetric-seam: 0 trait methods in 0 traits are a capability of ≤ 20 % of their ≥ 5 impls (the rest refuse: by a refusing default or a refusing o…
fail-open: 3 decisions FAIL OPEN (a `Result<bool>` / `Option<bool>` decision collapsed with `unwrap_or(true)`: an error answers yes) — through gunna…
history-comments: 2357 comment lines narrate history (3.4 % of 69204 comment lines; comments are 28.8 % of 240173 lines in 297 files): 1401 dated, 93…
4.2 One lens
dwarves lens <name> --mimir-dump DIR [--src CHECKOUT] [--limit 0] [--json] \
[--crate NAME]… [--include-tests] [--include-deps] [--fail-on-findings]
| flag | meaning |
--mimir-dump DIR |
the dump; default ./target/dwarves/mimir |
--src CHECKOUT |
the source checkout, for what the MIR does not keep: literal values, exact line counts, comments |
--limit 0 |
list every finding (the default 50 cuts the list, not the headline counts) |
--json |
the whole report as one JSON object |
--crate NAME |
report on these crates only |
--include-tests |
count test code too (default: hand-written production code only) |
--fail-on-findings |
exit non-zero when anything is found: the CI gate mode |
--local |
run here even when $DWARVES_SERVER / $NORNIR_SERVER is set |
Lenses that take inputs (patterns are substrings of <crate> <short moniker>; name the trait method AND the impls, Store#put() and [Store]put()):
dwarves lens gate-bypass --sink 'GitOps#update_ref()' --sink 'PackIngest#ingest()' \
--choke '[PolicyEngine]evaluate()' --entry 'transport/serve_receive_pack()' --mimir-dump DIR
dwarves lens reach-set --entry 'gunnar-grpc service/mirror/' --must-reach '[AuthTable]may_in()' --mimir-dump DIR
dwarves lens single-impl --mimir-dump DIR --also-dump DIR-OF-THE-OTHER-BUILD
dwarves lens arch-report --spec .nornir/architecture.ron --mimir-dump DIR --top 3
With --spec, gate-bypass and verdict take their chokes from the spec: every production fn mapped to the gate component (gate: "<name>" in the spec, else the component named gate) joins the --choke patterns, and --choke becomes optional. A hand-written choke list goes stale when the gate moves; the spec moves with the code.
Every report ends with the lens build that made it (lens_version and lens_git in --json), so numbers from two runs are only compared when the build matches:
lens: nornir-mimir-lens 0.2.2 (git <8 hex>)
5. Every lens in one table
The example findings are real rows from Gunnar c17a6ef4 (default dump unless marked), each printed by dwarves lens <id> --mimir-dump … --limit 1. "Formula" is the rule in one line; the full math is in the § named.
5.1 Micro lenses
| id (§) | what it finds | formula | example finding |
size (4.6) |
long fns and big files | end − start + 1 > 150 (very long > 300); file > 5000 lines |
FILE 9822+ crates/gunnar-server/src/control_plane.rs |
twins (4.2) |
fns that do the same thing | cos(R_i, R_j) ≥ 0.8 on IDF-weighted reach R = (I + A + … + Aᵏ)·L under the rare-API mask; WL hash of the MIR dataflow; groups = components |
NEAR STRONG ×9 in 3 crates [NEAR+shape+inlined] |
dispatch-erosion (4.3) |
match ladders over another crate's enum, string compares, parallel enums, match self ladders, literal arguments |
ladder = switch(discr(_x)) over E, ` |
V(E) |
cycles (4.4) |
module and crate cycles | SCCs of Cᵀ · A_calls · C (CALLS rolled up by CONTAINS) |
MODULE CYCLE ×43 (1072 calls inside): gunnar_server, gunnar_server::account_audit_sweep, … |
single-impl (4.5) |
traits with 0 or 1 production impl | ` | I∪(t) |
error-shape (4.7) |
stringly errors | counts of anyhow! / bail! sites, typed error types, Result<_, String> fns |
error-shape: 727 anyhow!/bail!/Error::msg sites in 332 fns vs 42 typed error types; 376 fns return Result<_, String | Box<dyn Error>> |
cfg-sprawl (4.8) |
features declared vs used | per crate: declared, item gates, body gates, never-gating features | cfg-sprawl: 100 features declared in 15 crates, 504 item gates + 326 gates inside bodies (in 120 fns), 40 features declared but never gating anything |
taint (4.9) |
untrusted input that reaches an exec / fs / path / net sink | reach(seeds, V⟨¬sanitized⟩) ∩ sinks over the MIR value-flow graph (LOR.LAND) |
TAINT exec std process/impl#[Command]arg(). in gunnar-cli crypt/door_run(). (crates/gunnar-cli/src/crypt.rs:912 +44:15) |
chokepoints (4.10) |
where one gate could sit | common dominators of every entry → sink path (Cooper–Harvey–Kennedy); Brandes betweenness | chokepoints: no entry reaches a sink: nothing to mediate (for the two entries given) |
misplaced (4.11) |
fns in the wrong module | Leiden communities, Q of communities vs modules vs crates, MoJoFM |
MOVE 24 fn(s) gunnar_core::auth::sso_table → gunnar_core::auth |
dsm (4.12) |
how far a change spreads | `PC = | R |
runtime-guard-erosion (4.13) |
what guards each write: a type, a verdict, a config flag, nothing | control dependence of the sink call in the door's MIR CFG | VERDICT gunnar-core txlog/tee/impl#[LoggedGit][GitOps]update_ref(). (crates/gunnar-core/src/txlog_tee.rs:156) → GitOps#update_ref() … |
taxonomy-drift (4.14) |
enums that name the same things twice | J(V₁, V₂) ≥ θ; a translation fn = one switch over E₁ whose arms build E₂; DRIFT when arms > 5 |
PARALLEL gunnar-common cli/PrincipalKindArg# ≈ gunnar-core model/PrincipalKind# ≈ gunnar-policy context/PrincipalKind# J≤0.80, 10 translation arms |
dyn-to-enum (4.15) |
trait vs enum, one rule | advise_trait: 0 → DELETE, 1 → INLINE, open → TRAIT, test seam → KEEP, > 8 → TRAIT, no dyn → KEEP, else ENUM |
ENUM gunnar-forge driver/ForgeDriver# (crates/gunnar-forge/src/driver.rs:116) — 3 impls [Forgejo, GitHub, GitLab], 14 dyn uses, 8 call sites |
foreign-helper-sprawl (4.16) |
free fns over &str / &Path / std types called from many modules |
first param foreign ∧ no local state ∧ ≥ 3 calling modules |
HELPER gunnar-server budget/human(). (crates/gunnar-server/src/budget.rs:359) — first param u64, 5 modules, 18 call sites, 4 name twins |
late-validation (4.17) |
validators deep below the entry ("parse, don't validate") | validator over a primitive ∧ depth ≥ 4 below the nearest entry ∧ ≥ 2 subsystems |
LATE gunnar-grpc client_signer/is_loopback_target(). (crates/gunnar-grpc/src/client_signer.rs:237) — (&str) -> bool, depth 7, 3 callers in 3 subsystems |
asymmetric-seam (4.18) |
trait methods only a few impls can do | ` | Capable(T, m) |
deferred-init-sprawl (4.19) |
structs built in steps | ≥ 5 fields, ≥ 40 % Option / once-cell, read from ≥ 2 subsystems |
DEFERRED-INIT gunnar-ssh seam/ServiceRequest# (crates/gunnar-ssh/src/seam.rs:92) — 7/14 fields (50 %: 7 Option, 0 once-cell, 0 both), read from 5 subsystems |
typestate-candidate (4.20) |
a state machine kept in a flag | a self field that ≥ 3 methods check first and bail on |
typestate-candidate: 0 typestate candidates: … 464 structs with inherent methods, 2459 methods, 19 guards |
pipeline-subclass-leak (4.21) |
a core struct holding its leaves' state | fields used by < 10 % of its methods, named after ≥ 2 leaves of one taxonomy |
LEAK gunnar-server stores/ServerStores# (crates/gunnar-server/src/stores.rs:460) — 38 fields, 101 methods, 2 leaf fields |
monolithic-router (4.22) |
a route table written as code | fn ≥ 500 lines comparing ≤ 2 values against ≥ 20 string literals |
monolithic-router: 0 fns ≥ 500 lines decide by comparing ≤ 2 values against ≥ 20 string literals …; 5 long fns |
reach-set (4.23) |
what an entry reaches; entries that never reach a check | R(e) = reach(e, A) (LOR.LAND, dyn expanded); MISSING R(e) ∩ targets = ∅; SKIPPABLE: a path returns Ok before the next hop |
MISSING gunnar-grpc service/mirror/impl#[MirrorService]log_twin_flush(). (crates/gunnar-grpc/src/service/mirror.rs:762) — 470 fns (207 local) in 22 crates |
history-comments (4.24) |
comments that narrate history | a comment line matching dates, lane names, owner, tickets, change words | history-comments: 3003 comment lines narrate history (3.5 % of 85960 comment lines …): 1897 dated, 111 lane, 358 owner, 256 ticket, 902 change |
export-refactor-envelope (4.25) |
one bounded JSON per finding, for a coding agent | a finding + every node it names + the exact source lines | (run on a --json report: --report FILE --finding N) |
gate-bypass (4.1) |
write paths that skip the choke point | reach(E, A⟨¬choke⟩) ∩ sinks, then a matched call/return walk over the MIR ICFG |
BYPASS gunnar-wire receive_pack/impl#[ReceivePack]serve_after_preamble(). (crates/gunnar-wire/src/receive_pack.rs:980) |
fail-open (4.35) |
a yes/no decision that answers yes on an error | unwrap_or(k:bool) on a Result<bool> / Option<bool> call; ans = ¬lit under Not(…), false for if … { return; }; FAIL-OPEN ans = true |
FAIL-OPEN gunnar-server control_plane/impl#[CoreControlPlane]namespace_out(). (crates/gunnar-server/src/control_plane.rs:1995) — gunnar-core auth/impl#[AuthTable]holds_server_grant().(..).unwrap_or(true) |
verdict (5 of lenses.md) |
one band per crate and module | the worst of the lens scores per unit; a gate bypass is RED | AMBER 0.35 module gunnar_server::admin (40 fns): cov 0.90 (36 of 40 fns unreached by tests); cyc 1.00 … |
5.2 Macro lenses
| id (§) | what it finds | formula | example finding |
reflexion (4.27) |
how far the code is from the target | H = Mᵀ·A·M (PLUS.TIMES); divergent H⟨¬T⟩; κ = conv / (conv + div) |
DIVERGENT server → grpc w 1070 in 669 edges [layer] (the §6 spec) |
arch-recover (4.28) |
the layout the code wants | Leiden over U = Cᵀ·A·C at a γ sweep; consensus over seeds (ARI); Hungarian match to today's crates → MOVE |
arch-recover: 4296 units, 10557 unit edges; crate level γ 0.2: 20 groups … MoJoFM crates 71 %, modules 73 %; 1493 units to move |
refactor-price (4.29) |
the value of a refactoring before doing it | apply the plan rows to the model in memory: Δκ, closed divergent weight, ΔPC, ΔSCC, ΔQ, changed call sites |
(run with --plan FILE; see the ROW lines of §7) |
arch-report (4.30) |
all of the macro engine in one run | reflexion + arch-recover + the loop (α ≥ 0.85) + refactor-price → architecture-report/1 |
§7 |
layer-stack (4.31) |
the dependency direction | Tarjan SCCs; a minimum back-weight order per SCC; β = cut weight / total |
CUT gunnar_server::storage → gunnar_server::storage::znippy_engine w 14 (layer 6 → 7) |
seam-interface (4.32) |
the trait missing between two groups | callee set S of a cut; `ν = |
names(S) |
concept-scatter (4.33) |
who owns a concept | per concept: entropy H of its touches over the groups, spread g; owner = argmax |
CONCEPT #1 gunnar-grpc client_signer/CredentialSigner# [struct] H 2.149, g 5, 15 touches; owner gunnar-cli (defined in gunnar-grpc) |
ports-adapters (4.34) |
I/O inside domain code | I/O seeds S_io; R = reach(E, U); a violation = u ∈ S_io in a group with domain share ≥ 0.7 |
VIOLATION gunnar-core txlog/Log# (crates/gunnar-core/src/txlog.rs:936) in gunnar_core::txlog (domain 80 %): direct fs I/O … 177 domain units above |
6. The target architecture: .nornir/architecture.ron
The macro lenses compare the code with a target you write down. reflexion, layer-stack, seam-interface, concept-scatter, ports-adapters and arch-report read it from --spec FILE, else from <--src>/.nornir/architecture.ron. With no spec, every crate is a component and the layers come from the Cargo dependency DAG.
6.1 The format
(
layers: ["top", "middle", "bottom"], // a component may depend on every layer below its own
components: { "<name>": ["<glob>", …], … }, // which items belong to which component
allow: [("<from>", "<to>")], // adds pairs to the allowed matrix T
forbidden: [("<from>", "<to>")], // removes pairs; always wins over allow
expected: [("<from>", "<to>")], // an absent pair is a finding
allow_edges: [("fqn:<from>", "fqn:<to>")], // optional: one edge allowed past every pair rule
via: ["fqn:<glob>"], // optional: a decorator's forwarding methods
gate: "<component>", // optional: the gate layer (default: the component `gate`)
)
Globs, most specific first (fqn: over mod: over crate:; within a kind, the glob with more literal characters wins):
| glob | matches | example |
crate:<glob> |
the package name (- and _ alike) |
crate:holger-znippy-* |
mod:<a>::<b>::** |
the module path; * one segment, ** any number |
mod:gunnar_server::serve::** |
fqn:<glob> |
crate::path::Item#member(); * any run, ? one char |
fqn:gunnar_grpc::seam::ControlPlane#* |
6.2 A worked example
A four-component target for Gunnar, in .nornir/architecture.ron:
// A small target for Gunnar: four components, top to bottom.
(
layers: ["doors", "grpc", "server", "core"],
components: {
"doors": ["crate:gunnar-ssh", "crate:gunnar-http"],
"grpc": ["crate:gunnar-grpc"],
"server": ["crate:gunnar-server"],
"core": ["crate:gunnar-core", "crate:gunnar-store", "crate:gunnar-policy"],
},
allow: [],
forbidden: [("grpc", "server")], // the gRPC services must not reach into the server crate
expected: [("doors", "core")], // the doors should use core directly
)
The layers give T, the allowed pairs: each component may depend on every layer below it.
doors grpc server core
T = doors [ 1 1 1 1 ]
grpc [ 0 1 0* 1 ] * forbidden removes (grpc, server)
server[ 0 0 1 1 ]
core [ 0 0 0 1 ]
M maps each item to its component (the globs above). The lens builds the item graph A (CALLS, USES_TYPE, READS_FIELD, WRITES_FIELD, IMPLEMENTS, weighted by sites), rolls it up H = Mᵀ · A · M (PLUS.TIMES), and splits H by the mask: H⟨T⟩ off the diagonal is convergent, H⟨¬T⟩ is divergent.
$ dwarves lens reflexion --mimir-dump /gdumps/gunnar-default --spec .nornir/architecture.ron --limit 8
reflexion: architecture.ron: 4 components, 4 layers; 9256 items, 31057 edges (weight 51447); κ 0.687 (convergent 3119 / divergent 1418 in 2 pairs), 0 absences; 3088 unmapped items, 0 inferred
…
DIVERGENT server → grpc w 1070 in 669 edges [layer]
gunnar-server control_plane/impl#[CoreControlPlane]merge_attempt(). → gunnar-grpc seam/MergedRef# (WRITES_FIELD ×24) at crates/gunnar-server/src/control_plane.rs:9149
gunnar-server control_plane/impl#[CoreControlPlane][FirstBoot]bootstrap_state(). → gunnar-grpc seam/BootstrapState# (WRITES_FIELD ×17) at crates/gunnar-server/src/control_plane.rs:2550
…
DIVERGENT server → doors w 348 in 193 edges [layer]
gunnar-server serve/impl#[Server]start(). → gunnar-ssh config/SshConfig# (WRITES_FIELD ×16) at crates/gunnar-server/src/serve.rs:945
…
The conformance: κ = 3119 / (3119 + 1418) = 0.687. The two divergent pairs point up the layer order: the server crate writes the record types that gunnar-grpc defines (seam::MergedRef, BootstrapState, …) and the SSH config. The forbidden grpc → server pair has no edge, and the expected doors → core pair is present, so there are 0 absences. 3088 items match no glob (the gunnar CLI crate, xtask, the other crates): they stay out of κ.
7. Reading architecture-report/1
arch-report runs the whole macro engine and prints one report with the contract architecture-report/1:
$ dwarves lens arch-report --mimir-dump /gdumps/gunnar-default --spec .nornir/architecture.ron --top 3
arch-report: architecture-report/1: PC 12.4 % → 12.7 %, SCC 14 → 14, Q 0.454 → 0.455, divergences 1418 → 1354, κ 0.687 → 0.697, β 1.2 % → 1.2 %; 13 moves from 23 aligned Leiden groups (α ≥ 0.85), 2 seams
contract: architecture-report/1
score: {"beta":[0.011741,0.012149],"divergences":[1418,1354],"divergent_pairs":[2,3],"kappa":[0.687459,0.697295],"pc":[0.123741,0.126601],"q":[0.453991,0.455415],"scc":[14,14]}
…
MOVE gunnar-server grpc_gate/impl#[ChainLedger]append(). (crates/gunnar-server/src/grpc_gate.rs:451) gunnar_server::grpc_gate [server] → grpc — closes 13 divergent weight (group α 0.95)
SEAM-NO server → grpc: 147 call sites into 89 fns (68 methods) from 73 callers, ν 0.578, behind a trait 20 %
CUT gunnar_server::storage → gunnar_server::storage::znippy_engine w 14 (layer 6 → 7): gunnar-server storage/shutdown(). → gunnar-server storage/znippy_engine/shutdown(). (CALLS ×1) …
CONCEPT #2 gunnar-core model/Principal# [struct] H 1.551, g 3, 295 touches; owner core (defined in core); core 118+11, doors 77+8, server 76+5
LEAK doors ×77 direct: gunnar-http auth/principal_for(). (crates/gunnar-http/src/auth.rs:159), …
VIOLATION [layer] server → grpc: gunnar-server control_plane/impl#[CoreControlPlane]merge_attempt(). → gunnar-grpc seam/MergedRef# (WRITES_FIELD ×24) at …
UNMAPPED gunnar main().
ROW C1 rank 1: Δκ +0.000 (0.687 → 0.687), divergent w +0 (1418 → 1418), pairs 2 → 2, ΔPC -0.02 pp (12.4 % → 12.4 %), ΔSCC +0 (14 → 14), ΔQ +0.000, Δβ -0.07 pp; 14 changed call sites, …
Wall 10.5 s, peak 4.6 GB.
How to read it:
scoreholds[before, after]pairs. "After" is the model with the loop's MOVE rows applied in memory (nothing is edited).pcis the propagation cost,sccthe cyclic module groups,qthe modularity,divergencesthe divergent weight,kappathe conformance,betathe share of edge weight that points up the layer order.- The findings, one class per row (
--jsongives the fields):
| class (text) | JSON fields | meaning |
move (MOVE) |
symbol, at, from{component, module}, to, weight, alpha, reason |
an item whose aligned Leiden group lives in another component; moving it closes weight of divergence |
seam (SEAM / SEAM-NO) |
from_component, to_component, call_sites, callees, callers, methods, nu, behind_trait, proposed, proposed_trait, merged_pairs |
the interface across a cut; SEAM proposes a trait, SEAM-NO says why not (ν too wide or already behind a trait) |
cut (CUT) |
from, to, from_layer, to_layer, weight, edge_count, edges |
an edge that points up the layer order, from layer-stack |
concept (CONCEPT #n, LEAK) |
name, kind, rank, entropy, spread, touches, owner, home, per_group, members, leaks |
a type touched across groups; the owner and the direct touches that leak out of it |
violation (VIOLATION) |
rule, from, to, edge |
one divergent edge of the reflexion |
unmapped (UNMAPPED) |
symbol |
an item no glob of the spec matches |
row (ROW) |
id, title, rank, review_rank, kappa, divergent_weight, divergent_pairs, pc, q, beta, core, cyclic_groups, layers, changed_sites, moved_items, closed_weight, cut_weight, score |
one priced refactoring row (loop rows and --plan rows), ranked by gain per changed call site |
- The summary also counts
components, groups, aligned_groups, moves, unmapped, inferred, seams, proposed_seams, cuts, cut_weight, layers, concepts, top_concepts, violations, rowsand therankingof the rows.
8. The math, step by step
Each lens is a few matrix products on the graph, then counting. The semirings:
| step | used by | product | semiring |
| reach a set | gate-bypass, reach-set, taint, ports-adapters, late-validation | q⟨¬v⟩ = q · A until q is empty |
ANY.PAIR on bool (= LOR.LAND) |
| closure | dsm, cycles | R = A⁺ |
LOR.LAND |
| roll-up | cycles, reflexion, arch-recover, layer-stack | Cᵀ · A · C, Mᵀ · A · M |
PLUS.TIMES |
| API reach | twins | (I + A + … + Aᵏ) · L, then IDF cosine under the rare-API mask |
PLUS.TIMES |
| masks | reflexion, ports-adapters | H⟨T⟩, H⟨¬T⟩ |
element-wise |
| community | misplaced, arch-recover | Leiden on the weighted graph; Q = Σ_c (e_c / m − γ (d_c / 2m)²) |
— |
| order | layer-stack | Tarjan SCC; minimum back-weight order (exact ≤ 20 nodes, else Eades–Lin–Smyth) | — |
| one pass | size, single-impl, error-shape, cfg-sprawl, dispatch-erosion, fail-open, asymmetric-seam, … | over the nodes or the MIR blocks | — |
A worked example of the one-pass kind, fail-open on its test fixture:
room(): holds_grant(a).unwrap_or(true) lit = true, no Not, no early return → ans = true → FAIL-OPEN
closed(): holds_grant(a)\n .unwrap_or(false) lit = false (the 2nd line, inside the 12-line window) → fail-closed at :22
admits(): !shielded(a).unwrap_or(true) MIR _0 = Not(^_6), dest = _6 → ans = ¬true = false → fail-closed, negated
guard(): if holds_grant(a).unwrap_or(true) { return; } lit = true, a bare early return → ans = false → fail-closed
tally(): count(a).unwrap_or(0) Result<u64, _> is no decision → no row
On Gunnar's metered dump this gives the review's three FAIL-OPEN rows: accounts.rs:323, entitlement.rs:1655 and control_plane.rs:1995.
9. Where to read more
lenses.md: every lens with its math, semirings, dataflow, worked example, Holger and Gunnar rows, bench and tests.macro-lenses-plan.md: the plan behind the macro lenses (M1–M7) and why each was built so.compiler-to-graph.md: from rustc to the tables to the GraphBLAS matrices, one fn traced end to end.tables.md: every column of the dump.- The crate:
crates/mimir-lens/README.md.