Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

docs/v2/mimir/overview.md

Mímir v2: what it is, for reviewers

This page describes Mímir as it is on nornir main on 2026-11-04. It is written for a reviewer who has not read the code. Every claim was checked against the code; file:line references are extras for those who want to look. Paths are in nornir unless they start with dw: (dwarves, crates/dwarves/). The graph lenses and the folds are nornir's crate crates/mimir-lens/ (moved from dwarves on 2026-11-05); dwarves is their CLI door. The detailed manuals are README.md, tables.md, compiler-to-graph.md and lenses.md.

0. Design stance (owner, 2026-11-04)

Mímir is meant to hold the whole Rust code universe: every workspace crate, every dependency at its locked version, std, and the system calls below them, across history. Large memory use is accepted as the price of that. The machines it runs on have hundreds of gigabytes of RAM. Reviewers should therefore weigh proposals by coverage, exactness and answer time on the full universe, not by peak memory. A change that saves memory but drops facts, approximates an exact answer or narrows the universe is not wanted. A change that keeps every fact and makes the full universe faster or cheaper to rebuild is.

1. Purpose

Mímir turns the whole code universe of a Rust workspace into one typed graph. "Whole" means the workspace's own crates, every dependency crate at the exact version the lockfile resolves, the toolchain's std, and, below them, the kernel's system calls. The facts come from the compiler itself, not from a parser of ours.

Questions it answers, with evidence and named gaps:

Who calls it:

caller door
dwarves CLI dwarves lens <name>, cargo lens <name> (offline, on a dump directory; the code is crates/mimir-lens); dwarves mimir zombie|risk|bloat (the nornir lens, or a local dump through the same crate)
dwarves-gui the Coderoom tab and the graph source pane, through CodeRoom.Answer on the forge proxy (:7900)
nornir CLI nornir mimir analyze <op> --server <url>
AI agents MCP tools mimir_<lens> in src/mcp_stdio.rs (gRPC when NORNIR_SERVER is set, in process otherwise)
Heimdall tooling the "possible system calls" Graph JSON (mimir-room <lake> possible …), read by dwarves-tune-graph
Holger serves the dependency sources: with MIMIR_CRATE_MIRROR every registry crate the dump compiles comes from Holger's sparse index (README §10.3); Holger's own workspace is the main analysed example in lenses.md §7

2. The pipeline end to end

 repo at commit C
   │  cargo mimir-dump  (cargo check --workspace --all-targets --keep-going,
   │                     RUSTC_WRAPPER=mimir-driver, nightly-2026-10-05)
   ▼
 mimir-driver: rustc in process, after_analysis hook walks TyCtxt + MIR
   │  per compiled unit (member, dependency, std):
   │    <crate>@<ver>-<hash>.{nodes,edges,spans,symbols}.arrow + .stats.json
   │  (Arrow IPC file format, one RecordBatch, temp name + rename)
   │
   ├──────────────► nornir-mimir-lens (offline; door: dwarves lens):
   │                Graph::from_dump → adjacency lists
   │                → GraphBLAS matrices (crates/mimir-lens/src/lenses/grb.rs) → 12 graph lenses,
   │                  verdict, zombie/risk/bloat/coverage folds → Report
   ▼
 nornir consumer (src/code_room/consumer_loop.rs, crates/mimir-ingest)
   │  read_out_dir → delta vs the repo's open set → +/− events
   │  ONE skade atomic release per (repo, sha): events + offset + ledger
   ▼
 lake: skade (Iceberg catalog in redb, Parquet zstd-3, 256k-row groups)
   │  code_{node,edge,span}_events, universe_*_events, code_symbol_events,
   │  bin_*; consumer_offsets (repo → seq, sha); READ-FAST interval tables
   │  code_{nodes,edges,spans}_v  [valid_from_seq, valid_to_seq)
   ▼
 mimir-room: fold at a Scope {repo → seq, universe on/off} → Snapshot
   │  sorted monikers = dense u32 ordinals; one boolean n×n GrB_Matrix per
   │  edge kind; unions, transposes, the Jikes matrix built on first use, kept
   ▼
 readers: CodeRoom.Answer surfaces (Viz.CrateGraph, Knowledge.Source, …)
          mimir_lens::answer (26 table-fed lenses + 2 source lenses)
   ▼
 answer envelope: worst[…], feeds[], evidence_note (MISSING / LOWER BOUND),
                  lens_meta.state (complete | partial | empty_fact | empty_gap | …)

The producer is the only source of graph facts. It is a clippy-shaped rustc driver (crates/mimir-driver/src/main.rs). It does not write the lake. It writes files, and the nornir consumer is the only writer of the lake (README §5). dwarves reads the same files and never the lake.

Other feeds ride the consumer's offsets and write the warehouse beside the lake: git_heat, sbom, dep_graph, code_index, binary, clone_shapes and embeddings (src/code_room/feeds.rs:13-21).

3. The data model

Nodes (code_nodes, tables.md §1). Kinds: crate mod fn method struct enum union trait trait_alias impl const static type macro ffi foreign_type field variant param ret local generic assoc_type assoc_const mono_instance ffi_import feature, plus the MIR parts mir_body and mir_block, and the boundary's syscall nodes in the universe. Each node has a level (workspace · repo · crate · module · item · part), its span, cfg, visibility, effective_visibility, has_doc, is_test, defined_here (false = a reference stub of another crate's item) and origin:

Edges (code_edges). Kinds: CONTAINS DEPENDS_ON CALLS IMPLEMENTS FOR_TYPE USES_TYPE READS_FIELD EXPANDS GATED_BY TESTS TOUCHES_TABLE COMPATIBLE INSTANCE_OF ENABLES ENABLES_WEAK BUILT_FROM SYSCALLS CFG_SUCC WRITES_FIELD (the last since schema 6: a body → each field it writes; READS_FIELD is every mention, so a read-only use is READS_FIELD without WRITES_FIELD). An edge has a site count; positions live in code_spans under the key from ␞ to ␞ KIND, so moving code changes spans and never an edge.

Ids. Every id is a SCIP moniker string, rust-analyzer cargo <package> <version> <descriptors>, made by one formatter (crates/mimir-schema/src/moniker.rs). Only the grammar is SCIP's; there is no index.scip and no SCIP tooling. A "stable" form drops the version, for identity across bumps (moniker::stable).

MIR payloads (compiler-to-graph.md §6). The driver reads mir_promoted. Members get full facts: one mir_body per fn, one mir_block per basic block, CFG_SUCC edges. Dependencies get the body fingerprint only (MIMIR_MIR changes this). A block's payload holds its statements, terminator, the locals it touches with their types, and the data-flow pairs a>b (local a read where b is written). Constants are recorded by type, never by value. The block fingerprint is FNV-1a 64 over the block text with locals renamed by first use, then mixed with the stable callee moniker. So two blocks that do the same thing to the same types share one fingerprint.

Caps. No block cap per body since schema 6 (every block is a row; MIMIR_MIR_BLOCKS / cargo mimir-dump --mir-blocks N sets one), and STMTS_PER_BLOCK = 128 statements per block (crates/mimir-schema/src/mir.rs:62). Ordinals are 32-bit. The room drops mir_block rows and CFG_SUCC unless MIMIR_ROOM_MIR_BLOCKS=in; lenses read block payloads from the lake per repo. Pane lists are capped at 200 entries.

4. Time and history

The time axis is the consumer sequence number seq, per repo: the ordinal of the commits the consumer has walked, first-parent order. Each event row carries repo, seq, sha, op (+/−), payload_hash. consumer_offsets maps a repo to its current (seq, sha).

As of a commit. A Scope names repo → seq (and universe on or off). A sha is not a scope key: Coderoom.SnapshotDiff takes repo@seq and refuses a sha (src/code_room/reader/wired/tests.rs:433). The room at a scope is built in one of two ways:

  1. The fold. For each key, the rows with a + at or before seq and no later − (the open set). This scans the repo's event history.
  2. The READ-FAST zone (crates/mimir-ingest/src/readfast.rs). Per source kind there is a derived interval table, code_nodes_v, code_edges_v, code_spans_v, partitioned by repo. Each row is one version with [valid_from_seq, valid_to_seq); valid_to_seq = NULL means open. A table property mimir.readfast.anchor.<repo> says how far the zone is complete. A read takes the interval rows live at min(seq, anchor) plus the event tail after the anchor. The event tables stay the truth; a repo without an anchor falls back to the fold. The server refreshes the zone as a lake_readfast job after each consume (src/server_grpc.rs:6439).

Diffs.

History. A member's first consume starts at its tip. Older commits come from a backfill job that consumes one commit per turn (src/code_room/backfill.rs). Each turn is a full cargo mimir-dump of that commit, with cargo's incremental fingerprints deciding what recompiles. A past commit is built against today's sibling repos; when it does not load, it is recorded as error and skipped.

Cost, as measured (sources and dates in §6): a tip consume of one member with 608 crates took 299 s (dump 156 s, delta 121 s, commit 22 s); the next commit with two bodies changed took 58 s (58 crates re-dumped). A room for the whole workspace took 28 s on dwarf. There is no measured number for the READ-FAST read against the fold.

5. Vectors and similarity

All of these exist in code today. "Real" means a producer writes the input in the normal flow.

signal what producer state
raw function embedding one vector per function node: the driver's span of the fn, cut from the checkout; key content_hash (SHA-256 of the text) → vector, so identical bodies share one row; table embeddings, links in embedding_node_events embeddings feed, deferred to the feeds lane (src/vector/fnvec.rs) real
normalised-body embedding the same model over the name-free normalised body; table embeddings_norm, keyed by the exact key clone_shapes + embeddings feeds (src/clone_signals/feed.rs) real
exact key hash of the normalised body (every workspace-chosen name replaced by its role) clone_shapes (fn_shape_events) real
MinHash + LSH 5-token shingles of role classes, PERMS = 64, BANDS = 16 × ROWS = 4; candidate probability 1 − (1 − J⁴)¹⁶; near clone when the estimated J ≥ 0.80 (src/clone_signals/minhash.rs) clone_shapes real
structural features p: param type, r: return type, t: used type, f: field read, c: callee, k: caller; idf = ln(1 + N/df); score = weighted Jaccard Σ idf(shared) / Σ idf(either), floor 0.5, candidates only from rare features (df ≤ 64) (src/clone_signals/structure.rs) computed from the room real
FastRP role vectors 128 dims, a sparse ±1 projection with 4 non-zeros per feature; X0 = Σ idf(f)·P(f); X1 = ·X0, X2 = ·X1 over row-normalised CALLS (both directions), USES_TYPE and READS_FIELD; role = normalise(n(X0) + n(X1) + ½·n(X2)); role_neighbours at cosine ≥ 0.90 computed from the room, stored as space fastrp-v1 real
WL shapes (dwarves) dwarves lens twins: per-fn PDG of MIR blocks (control CFG_SUCC + data edges); name-free start labels; h Weisfeiler–Lehman rounds, each one mxm B·onehot(labels) with PLUS.PAIR; equal label multisets = shape twin; Jaccard in [near, 1) = NEAR twin; API reach R = (I + A + … + Aᵏ)·L, IDF cosine under a rare-API mask (crates/mimir-lens/src/lenses/twins.rs:1-30) the dump real
dwarves dupes embeddings embeddings over the driver's spans for dwarves none not wired: refuses in about 10 ms

Model and backends. The default model is jina-v2-base-code (768 dimensions). The registry also lists a q8 variant, minilm-l6-v2, bge-base-en-v1.5 and the static potion-base-8m (src/vector/embed_registry.rs:113-255). Backends: tract on CPU, onnxruntime on CPU, CUDA or ROCm, picked at run time (src/vector/select.rs), or a remote GPU worker ($NORNIR_EMBED_REMOTE, src/vector/remote.rs). Vectors from two models never mix (model_profile key).

The pair index (src/vector/pair_index.rs). similar_code (cosine 0.92) and semantic_clones (0.80) need every pair above a bar. The index is an exact cosine self-join over the distinct vectors, kept in the process: FLOOR = 0.80, used from MIN_ROWS = 2,048 rows. The cold build is n²/2 dots in AVX2 register tiles (src/vector/tiled.rs; an int8 prefilter gives the same pairs and floats); a commit adds k × n dots for k new vectors. There is no ANN, and the index runs on the CPU only. Pairs are held as Vec<(u32, u32, f32)>. Clone lenses then apply mutual top-k (MUTUAL_K = 8) and union-find classes. semantic_twins uses TWIN_COSINE = 0.882, the operating point calibrated for jina-v2-base-code on 3,222 nornir functions (ROC-AUC 0.920, src/mimir_lens/room_feed/clones.rs:2044).

6. Compute

GraphBLAS (SuiteSparse 7, through crates/graphblas-sys and the safe layer crates/mimir-graph). The operations, with A = boolean adjacency:

op formula semiring
reach f_{k+1}⟨¬v_k⟩ = f_k · A (vxm) GxB_ANY_PAIR_BOOL (= LOR.LAND on booleans)
batched reach (reach_each, reach_sets) F⟨¬V, struct, replace⟩ = F · A (mxm), F is B × n, B = NORNIR_BFS_BATCH = 64 GxB_ANY_PAIR_BOOL
reverse the same walk on a cached Aᵀ (GrB_transpose) GxB_ANY_PAIR_BOOL
dependents (Jikes) W = (CALLS ∨ USES_TYPE ∨ IMPLEMENTS ∨ FOR_TYPE)ᵀ ∨ diag(impls)·CONTAINS GrB_LOR, GxB_ANY_SECOND_BOOL
scope A_s = diag(s) · A · diag(s) GxB_ANY_SECOND_BOOL, GxB_ANY_FIRST_BOOL
bottleneck (trust) widest-path relaxation over node weights GxB_MAX_FIRST_UINT64, MIN, MAX
propagate (FastRP) y = A·x over a weighted A GrB_PLUS_TIMES_SEMIRING_FP64
diff A_y⟨¬A_x⟩ GrB_IDENTITY_BOOL with GrB_DESC_RSC

(crates/mimir-graph/src/ops.rs; table with lines in lenses.md §3.4.) Matrices are packed from sorted from·2³² + to pairs, then set to GxB_AUTO_SPARSITY so GraphBLAS can store a mostly empty matrix hypersparse (crates/mimir-graph/src/matrix.rs:207). The only global option set is the thread count (GxB_GLOBAL_NTHREADS, crates/mimir-graph/src/runtime.rs). The graph lenses (crates/mimir-lens) add components, Leiden (misplaced), closure by repeated squaring (dsm), dominators and betweenness (chokepoints), and a CFPQ walk (gate-bypass). The possible-system-calls walk is a plain breadth-first walk with parents over edge reads, not a matrix product (crates/mimir-room/src/possible.rs:461).

Caches.

jera lanes (src/server_grpc.rs:205-262): backfill, feeds (deferred embeddings), rooms (room warm-up, the ring's embed), five lenses lanes (LENS_LANES = 5), panes (pane answers, high priority) and lake (compaction, READ-FAST refresh). A Mimir.* door answers a kept result at once, or queues the lens and answers queued / running with a job id.

Numbers (only what a file records; one run each unless said):

what number where it comes from
graph-load of a dump (read + index, no lens) Holger 32.2 s / 2.16 GB, dwarves 18.6 s / 1.35 GB, Gunnar 40.3 s / 2.76 GB lens plan §G, build-1 (32 cores, 128 GB), debug build, 2026-11-04; lenses.md §8
dwarves lenses twins 39.0 s / 3.12 GB, zombie 28.9 s / 2.17 GB, risk 62.6 s / 2.35 GB (Holger) same
the dump (cargo mimir-dump --workspace-only) Holger 127.2 s / 4.29 GB, dwarves 126.9 s / 2.39 GB, Gunnar 111.9 s / 2.48 GB same
study lenses (container memory.peak) taint 79 s / 3.7 GB, chokepoints 86 s / 4.3 GB, dsm 115 s / 3.7 GB (Holger) lens plan §F.1, 2026-11-04
Holger dump size 291 crates, 1,795 Arrow files, 1.6 GB, about 1.1 M nodes and 8 M edge ends lens plan §D round 8 and §G
room build, whole workspace 28 s on dwarf src/mimir_lens/queued.rs:3, 2026-10-16
room vs ring view room 360 MB, ring view 1.9 GB (dwarf's 21 repos) crates/mimir-room/src/lib.rs:490, 2026-10-16
reading the whole ring before the closure fix 15.7 M nodes / 37.7 M edges, 22 GB peak; now only the DEPENDS_ON closure is read (6.3 M nodes kept) crates/mimir-room/src/snapshot.rs:1786-1792, 2026-10-16
folding the dependency universe 8.2 M nodes in 951 s src/mimir_lens/room_feed/lenses.rs:1147, 2026-10-19
consume of one member (608 crates) 299 s, +3,242,285 events; server peak 4.4 GB .nornir/mimir-v2-land.md lane I, 2026-10-13
vector lenses on 4,628 fns similar_code 1.0–2.3 s, semantic_clones 1.0–2.7 s; embedding 0.77 fn/s on tract CPU .nornir/mimir-v2-land.md lane J, 2026-10-13
a lens queue before the lane split the last of 21 lenses waited 860 s on one lane src/server_grpc.rs:224-229, 2026-10-21

7. What is NOT there

8. The old review vs today

The old review was written against an older description. Each row was checked against the code on 2026-11-04.

# the claim state today evidence still worth doing?
W1 FastRP measures proximity, not role (role_neighbours) FIXED, the default since 2026-11-05 engine=wl (src/clone_signals/role_wl.rs, space role-wl-v1), the default, compares what the neighbours ARE per edge kind and direction; engine=fastrp stays available and stays the 3D code map's vector producer. Fixtures: role twins in separate components and two impls behind dyn are missed by FastRP and found by WL; a dispatcher and a leaf with one caller, type and field are paired by FastRP and kept apart by WL (room_feed/role_wl_tests.rs) Default flipped by the owner after the comparison (~/plan/handback/wl-role.md); FastRP retirement waits for the code-map vectors (D5)
W2 Interval tables in Iceberg/Parquet are an OLAP mismatch; world_impact folds two full rooms PARTLY The interval tables exist (code_*_v, crates/mimir-ingest/src/readfast.rs:1-17). world_impact reads the commit window from them without folding (crates/mimir-room/src/window.rs:1-14) and folds two rooms only as a fallback (src/mimir_lens/room_feed/lenses.rs:1266-1349) Measure the zone read against the fold first; no number exists
W3 Two jina pipelines, raw and normalised, while the normalised body already has MinHash 64 + LSH 16×4 STILL TRUE embeddings and embeddings_norm both exist (src/code_room/feeds.rs:21, src/clone_signals/mod.rs:17-21); MinHash PERMS = 64, BANDS = 16 (src/clone_signals/minhash.rs:17-24) Yes: measure what embeddings_norm adds to semantic_clones and semantic_twins beyond MinHash and structure
W4 The driver writes lake rows directly; no content-addressed cache NOT APPLICABLE ANY MORE (first half), PARTLY (second half) The driver writes per-unit Arrow IPC files; only the consumer writes the lake (README §3.4, §5). Caches: the mirror is keyed by cargo's unit hash (crates/mimir-ingest/src/driver.rs:35-44); the ring ledger is keyed by an FNV-1a 64 semhash of package@version, manifest dir, crate, types, cfgs, code options, target, resolved extern versions and feature dependencies (crates/mimir-driver/src/lib.rs:221, :268, :290). The key has no source hash and no driver version, and it covers dependency units only Yes for member history: a re-projection from kept dumps would avoid re-dumping commits after an ingest change
W5 Single-source BFS loops in weak_tests and data_reach ALREADY DONE Both use reach_each: F⟨¬V⟩ = F ANY.PAIR A, 64 walks per frontier matrix (lenses.md weak_tests step 3, data_reach step 4; crates/mimir-graph/src/ops.rs:672-729) No
W6 symbol_identity and undoc_api run as GraphBLAS lenses but could come from Cargo.lock / missing_docs NOT APPLICABLE ANY MORE Neither uses GraphBLAS. symbol_identity checks the one-definition-per-moniker invariant from Room::duplicate_keys, which Cargo.lock cannot see. undoc_api reads the driver's has_doc and effective_visibility (the same rustc facts missing_docs reads) and ranks by churn (lenses.md symbol_identity, undoc_api) No
I1 Batched multi-source BFS F(k+1) = ⟨¬V_k⟩(F_k × A), ANY.PAIR, tiles of 64/128 ALREADY DONE reach_each / reach_sets, GxB_ANY_PAIR_BOOL, B = NORNIR_BFS_BATCH default 64 (crates/mimir-room/src/query.rs:203-206) Only tuning B
I2 Angular centroid pruning (spherical k-means K=512) for similar_code + semantic_twins (35 s → < 5 s) STILL TRUE (not done) No k-means in the pair path. The pair index is an exact, incremental self-join with AVX2 tiles and an int8 prefilter (src/vector/pair_index.rs:1-37, src/vector/tiled.rs). The 35 s figure has no source in the code or the plans; the recorded figure is 1.0–2.7 s on 4,628 fns (2026-10-13) Only after a measurement at 10⁵ vectors shows the cold build hurts; pruning must keep the join exact
I3 Roaring bitmap snapshots every 64 commits + deltas; world_impact as a bit diff + reverse reach on a cached Aᵀ PARTLY No bitmaps; the READ-FAST zone and the commit window play that role (§4). Reverse reach on a cached transpose / Jikes matrix is done (crates/mimir-room/src/snapshot.rs:446-488) Not now; measure the zone first
I4 Split the driver into a content-addressed dump (.mimir-cache/<BLAKE3(…)>.arrow.zst) and a projector into the lake PARTLY The split exists: driver → Arrow files → consumer. The cache key is not BLAKE3 of sources: cargo unit hash (mirror) and FNV-1a semantic key (ring, dependencies only); files are not zstd Yes, for members across history (see W4)
I5 Directed, edge-typed WL refinement instead of FastRP for role_neighbours DONE as engine=wl Shared pieces in crates/mimir-graph/src/ops.rs (label_counts PLUS.PAIR, wl_round, stable_hash, multiset_min_masked PLUS.MIN); the item graph, tclass, IMPL_OF/SELF_OF, the hub rule and the kept labels (fn_role_labels) in role_wl.rs; the graph lens twins (crates/mimir-lens) can move onto ops::wl_round later (same answers only if it keeps its hash) Default flipped 2026-11-05; FastRP retirement waits for the code-map vectors
I6 GxB_HYPERSPARSE on sparse kinds, GxB_GLOBAL_POOL false, bounded top-k heaps instead of Vec<(u32,u32,f32)>, to cut a 17.2 GB peak (graph < 800 MB for 103k fns) PARTLY Hypersparse: AUTO sparsity on every packed matrix (crates/mimir-graph/src/matrix.rs:207, arrow.rs:158). Pool: not set; only GxB_GLOBAL_NTHREADS (runtime.rs:55). Pair buffers are still Vec<(u32, u32, f32)> (src/vector/pair_index.rs:68, tiled.rs:77). The 17.2 GB peak has no source in nornir, dwarves or the plans; it is unmeasured now. Recorded peaks: 22 GB for the whole ring read before the closure fix, 1.9 GB ring view, 360 MB room (2026-10-16); dwarves lenses 1.4–4.3 GB (2026-11-04) Top-k heaps would change answers (the lenses ask for every pair above a bar), so no. Measure the pair buffers at 10⁵ vectors first
I7 Drop normalised-body embeddings; symbol_identity from cargo-metadata; undoc_api from -W missing-docs STILL TRUE (first part); NOT APPLICABLE ANY MORE (second and third) See W3 and W6 First part: after the W3 measurement