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

nordisk/dwarves

dwarves-gui/src/mimir_lens.rs

//! **The 🔍 Lenses tab — the six Mimir lenses.**
//!
//! The UI surface of the Mimir lenses: the generic bloat tax, zombie cargo features,
//! change-risk scoring, weak (cannot-go-red) tests, un-benched hot paths, and the
//! relocated similar-code query. One pane with six views rather than six tabs, because
//! they are the same shape — *a ranked list of findings with its evidence beside it* —
//! and the shell already carries 20 tabs. Six near-identical tabs would be a twin of
//! the same renderer six times (L5), which is the thing this constellation keeps
//! deleting.
//!
//! The QUERY half of every lens now lives in **nornir** as the `Mimir.{GenericCost,
//! ZombieFeatures,ChangeRisk,SimilarCode,WeakTests,BenchGaps}` RPCs (L5a) — so korp's
//! UI and nornir's own viz can reach them too. This pane reads nornir's warehouse over
//! the thin `Warehouse.Scan` gRPC seam and folds it with the IDENTICAL engine code the
//! RPCs run (the fold was lifted into nornir verbatim), so the tab and the RPC can
//! never disagree about what a finding is. Re-pointing this pane's reads to call the
//! `Mimir.*` RPCs directly is a follow-up that needs a trimmed `mimir.proto` in
//! `nornir-warehouse-embed` (the drift-guarded stub crate) — the numbers are already
//! identical either way.
//!
//! The pane owns **no query logic**. Every finding comes from the `dwarves::mimir`
//! engine, which `dwarves mimir {bloat,zombie,risk}` drives through the identical calls,
//! so the tab and the command line can never disagree about what a finding is.
//!
//! ## ARCHITECTURE (L5a) — surfaced IN THE UI, not just in a doc comment
//! The QUERY half of each lens belongs in **nornir**, which owns the warehouse, exposed
//! as an RPC so korp's UI and nornir's own viz can use it too. What belongs in dwarves is
//! the GATE and the PROPOSAL. The pane renders
//! [`dwarves::mimir::PROPOSED_RPCS`] verbatim in its **where this belongs** view, so the
//! staging decision is visible to whoever opens the tab rather than buried in a module
//! doc nobody reads.
//!
//! ## What the pane must never do
//! **Show a composite score alone.** The risk view prints `callers`, `churn` and
//! `coverage` as their own columns beside the score, and carries Tornhill's citation on
//! screen — a number nobody can trace is the failure this constellation keeps retracting.
//!
//! **Render an empty table as a clean result.** Every view leads with the engine's
//! `evidence_note`, which names MISSING FEEDS by table; today that means
//! `scip_occurrences`, `coverage_fn` and `semver_diffs` are called out on sight.

use std::collections::BTreeMap;

use dwarves::coverage::CoverageIndex;
use dwarves::mimir::{bloat, collide, risk, zombie, Sources, PROPOSED_RPCS};
use facett_core::Facet;

/// Row cap per table on the thin read. `symbol_sizes` is the big one (1.35M rows for a
/// dozen analysed artifacts).
const SCAN_LIMIT: u32 = 2_000_000;

/// Where the pane's warehouse rows come from. Injected so the robot test drives the
/// pane over a deterministic fixture with no server.
pub trait LensSource: Send {
    fn load(&self) -> Result<Sources, String>;
    fn describe(&self) -> String;
    /// Manifest facts per repo (declared vs built features) — needed by the zombie lens.
    fn manifests(&self) -> BTreeMap<String, zombie::ManifestFacts> {
        BTreeMap::new()
    }

    /// **The THIN answer.** A remote source folds on the SERVER: it calls nornir's six
    /// `Mimir.*` lens RPCs and returns each lens's ranked findings + evidence_note as
    /// the RPC produced them (the report's `state_json`). `None` (the default) means
    /// this source supplies RAW warehouse `Sources` via [`load`](Self::load) and the
    /// pane folds them LOCALLY — the fat/embedded path. `Some` short-circuits the local
    /// fold: the pane renders exactly what the server computed.
    fn remote(&self) -> Option<Result<RemoteReports, String>> {
        None
    }

    /// **Does this source fold on the server?** A question about the source, answered
    /// without asking anything: [`remote`](Self::remote) runs every lens RPC in series
    /// and must never be called to find this out (2026-10-14: the pane called it from
    /// `error_code`, i.e. from every `state_json`, i.e. every frame while an error was
    /// held — on the real workspace `world_impact` takes minutes and the window froze).
    fn folds_on_the_server(&self) -> bool {
        false
    }

    /// **The same remote fold, OFF the caller's thread, ONE LENS AT A TIME.** A
    /// source that folds on the server returns a receiver of per-lens updates the
    /// pane polls once per frame: every lens is asked on its own worker, each answer
    /// lands as it comes, and a lens the server answers `queued` / `running` (a jera
    /// job) is asked again until it lands — so no lens waits for, or is blanked by,
    /// another (2026-10-16: one failed RPC or the 20 s client bound blanked the whole
    /// tab). The channel closes when every lens has its last answer. `None` = this
    /// source has no remote fold (the local-fold sources).
    fn remote_spawn(&self) -> Option<std::sync::mpsc::Receiver<LensUpdate>> {
        None
    }

    /// **Set the Churn frequency-filter params** the change/anomaly view drives
    /// (band = high/low/band, and the low/high cutoffs). Default no-op — only a remote
    /// source (which serves `Mimir.Churn`) honours it.
    fn set_churn_params(&mut self, _band: &str, _cutoff_low: i64, _cutoff_high: i64) {}

    /// **Set the subclone repo scope** — the ONE repo `Mimir.Subclone` folds
    /// (the lens is per-repo by construction; `""` asks for its refusal, which
    /// names the askable repos). Default no-op — only a remote source honours it,
    /// exactly like the Churn params above.
    fn set_subclone_repo(&mut self, _repo: &str) {}

    /// **Set the group_twins repo scope** — the second SOURCE-door lens is per-repo
    /// by construction too (`""` asks for its refusal naming the askable repos).
    /// Its OWN knob, not subclone's: a click in one lens never re-scopes the other.
    fn set_group_twins_repo(&mut self, _repo: &str) {}

    /// **Re-ask ONE lens, off the caller's thread** — the same per-lens worker
    /// [`remote_spawn`](Self::remote_spawn) uses, for a single lens (what a
    /// subclone repo click drives: one re-ask, never a whole re-scan). `None` =
    /// this source has no remote fold.
    fn remote_spawn_one(
        &self,
        _lens: &'static str,
    ) -> Option<std::sync::mpsc::Receiver<LensUpdate>> {
        None
    }
}

/// The shipped source: nornir's warehouse over the thin gRPC read seam.
pub struct WarehouseLens {
    pub endpoint: String,
    pub token: String,
    pub workspace: String,
}

impl WarehouseLens {
    pub fn new(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        Self {
            endpoint: endpoint.into(),
            token: token.into(),
            workspace: workspace.into(),
        }
    }
}

impl LensSource for WarehouseLens {
    fn load(&self) -> Result<Sources, String> {
        let mut tables = BTreeMap::new();
        let mut first_err = None;
        for t in dwarves::mimir::TABLES
            .iter()
            .chain(dwarves::coverage::TABLES.iter())
        {
            // A table nothing writes since Mímir v2 is not asked for: it is an empty
            // feed here, and the folds' evidence names it as NOT READ.
            if dwarves::retired::is_retired(t) {
                tables.insert((*t).to_string(), Default::default());
                continue;
            }
            match nornir_warehouse_embed::scan_warehouse_table(
                &self.endpoint,
                &self.token,
                &self.workspace,
                t,
                SCAN_LIMIT,
            ) {
                Ok(s) => {
                    tables.insert(
                        (*t).to_string(),
                        dwarves::coverage::rows::Table::new(s.columns, s.rows),
                    );
                }
                Err(e) => {
                    first_err.get_or_insert(e.to_string());
                    tables.insert((*t).to_string(), Default::default());
                }
            }
        }
        // A server that answered nothing at all is UNREACHABLE, not an empty warehouse
        // — two different diagnoses, and conflating them is how an outage reads as a
        // clean bill of health.
        if tables
            .values()
            .all(|t: &dwarves::coverage::rows::Table| t.is_empty())
        {
            if let Some(e) = first_err {
                return Err(e);
            }
        }
        let mut s = Sources::default();
        s.add_root(self.describe(), tables);
        Ok(s)
    }

    fn describe(&self) -> String {
        format!("nornir warehouse @ {} / {}", self.endpoint, self.workspace)
    }
}

/// The never-blank demo source. Its shape is the real 2026-07-22 reading: one artifact
/// whose tax is mostly unactionable glue plus one named generic that IS actionable, one
/// feature declared-and-never-built, one repo whose build space is unknown, and a
/// churning uncovered file.
#[cfg(any(test, feature = "fixtures"))]
pub struct FixtureLens;

#[cfg(any(test, feature = "fixtures"))]
impl FixtureLens {
    fn t(cols: &[&str], rows: &[&[&str]]) -> dwarves::coverage::rows::Table {
        dwarves::coverage::rows::Table::new(
            cols.iter().map(|s| s.to_string()).collect(),
            rows.iter()
                .map(|r| r.iter().map(|s| s.to_string()).collect())
                .collect(),
        )
    }
}

#[cfg(any(test, feature = "fixtures"))]
impl LensSource for FixtureLens {
    fn load(&self) -> Result<Sources, String> {
        let mut tables = BTreeMap::new();
        tables.insert(
            "binary_facts".to_string(),
            Self::t(
                &["content_sha256", "repo", "artifact_path", "total_bytes"],
                &[&["c1", "znippy", "/t/release/znippy", "1000000"]],
            ),
        );
        tables.insert(
            "symbol_sizes".to_string(),
            Self::t(
                &[
                    "content_sha256",
                    "crate_name",
                    "symbol_demangled_no_generics",
                    "symbol_demangled",
                    "size_bytes",
                    "kind",
                    "is_monomorphization",
                ],
                &[
                    &[
                        "c1",
                        "core",
                        "core::ptr::drop_in_place",
                        "core::ptr::drop_in_place::<A>",
                        "90000",
                        "Text",
                        "true",
                    ],
                    &[
                        "c1",
                        "core",
                        "core::ptr::drop_in_place",
                        "core::ptr::drop_in_place::<B>",
                        "90000",
                        "Text",
                        "true",
                    ],
                    &[
                        "c1",
                        "znippy",
                        "znippy::parse_stream",
                        "znippy::parse_stream::<u8>",
                        "20000",
                        "Text",
                        "true",
                    ],
                    &[
                        "c1",
                        "znippy",
                        "znippy::parse_stream",
                        "znippy::parse_stream::<u16>",
                        "20000",
                        "Text",
                        "true",
                    ],
                    &[
                        "c1",
                        "znippy",
                        "znippy::parse_stream",
                        "znippy::parse_stream::<u32>",
                        "20000",
                        "Text",
                        "true",
                    ],
                ],
            ),
        );
        tables.insert(
            "feature_gate_facts".to_string(),
            Self::t(
                &[
                    "repo",
                    "crate_name",
                    "module_path",
                    "item_name",
                    "cfg_expr",
                    "file",
                    "line",
                ],
                &[
                    &[
                        "korp",
                        "korp",
                        "korp::spark",
                        "spark_ingest",
                        "feature = \"spark41\"",
                        "src/spark.rs",
                        "12",
                    ],
                    &[
                        "knut",
                        "knut",
                        "knut::g",
                        "gated",
                        "feature = \"grpc\"",
                        "src/g.rs",
                        "3",
                    ],
                ],
            ),
        );
        tables.insert(
            "call_edges".to_string(),
            Self::t(
                &[
                    "snapshot_id",
                    "repo",
                    "caller_path",
                    "callee_ident",
                    "call_kind",
                ],
                &[
                    &["s1", "znippy", "znippy::tests::t", "parse_stream", "call"],
                    &["s1", "znippy", "znippy::hot::drive", "parse_stream", "call"],
                    &["s2", "knut", "knut::x", "gated", "call"],
                ],
            ),
        );
        tables.insert(
            "symbol_facts".to_string(),
            Self::t(
                &[
                    "snapshot_id",
                    "ts_micros",
                    "repo",
                    "crate_name",
                    "module_path",
                    "item_kind",
                    "item_name",
                    "visibility",
                    "file",
                    "line",
                ],
                &[
                    &[
                        "s1",
                        "100",
                        "znippy",
                        "znippy",
                        "znippy::hot",
                        "fn",
                        "parse_stream",
                        "pub",
                        "src/hot.rs",
                        "10",
                    ],
                    // A CROSS-REPO name collision: two repos each hand-roll `nornir_escape::esc::escape`,
                    // so the call graph (which binds by name) cannot tell whose it is — the graph-soundness
                    // hazard the name-collision lens surfaces.
                    &[
                        "s2",
                        "200",
                        "korp",
                        "nornir_escape",
                        "esc",
                        "fn",
                        "escape",
                        "pub",
                        "src/esc.rs",
                        "5",
                    ],
                    &[
                        "s2",
                        "200",
                        "knut",
                        "nornir_escape",
                        "esc",
                        "fn",
                        "escape",
                        "pub",
                        "src/esc.rs",
                        "9",
                    ],
                ],
            ),
        );
        tables.insert(
            "git_heat_facts".to_string(),
            Self::t(
                &["repo", "file", "commits_90d"],
                &[&["znippy", "src/hot.rs", "37"]],
            ),
        );
        let mut s = Sources::default();
        s.add_root(self.describe(), tables);
        Ok(s)
    }

    fn describe(&self) -> String {
        "fixture (demo) — a mostly-glue tax with one actionable generic, a never-built \
         feature, and a churning uncovered file"
            .to_string()
    }

    fn manifests(&self) -> BTreeMap<String, zombie::ManifestFacts> {
        [
            (
                "korp".to_string(),
                zombie::ManifestFacts {
                    declared: ["spark41".to_string()].into(),
                    built: Default::default(),
                    build_space_known: true,
                },
            ),
            (
                // knut declares no build space, so its `grpc` feature must NOT be
                // called never-built — the false-positive guard, visible in the demo.
                "knut".to_string(),
                zombie::ManifestFacts {
                    declared: ["grpc".to_string()].into(),
                    built: Default::default(),
                    build_space_known: false,
                },
            ),
        ]
        .into()
    }
}

/// **The six lenses as nornir's `Mimir.*` RPCs return them** — each is the lens
/// report's `state_json` (the observable surface: ranked findings + an
/// `evidence_note` naming missing feeds by table), carried verbatim as a
/// [`serde_json::Value`]. The pane RENDERS these in the thin path and adds no numbers
/// of its own, so a thin reading is exactly what the server computed.
#[derive(Clone, Debug, Default)]
pub struct RemoteReports {
    pub bloat: serde_json::Value,
    pub zombie: serde_json::Value,
    pub risk: serde_json::Value,
    pub weak_tests: serde_json::Value,
    pub bench_gaps: serde_json::Value,
    pub similar_code: serde_json::Value,
    /// `Mimir.Villa` — impure / shell-out heresy over the CPG (the new lens).
    pub villa: serde_json::Value,
    /// `Mimir.Churn` — the frequency filter (high/low/band-pass) over the change-series.
    pub churn: serde_json::Value,
    /// `Mimir.Anomaly` — statistical outliers on the change-series.
    pub anomaly: serde_json::Value,
    /// `Mimir.WorldImpact` — changes ranked by world + constellation downstream.
    pub world_impact: serde_json::Value,
    /// `Mimir.BinaryDrift` — source(AST) vs binary(DWARF) attestation: symbols/edges
    /// in the compiled binary not derivable from the audited source.
    pub binary_drift: serde_json::Value,
    /// `Mimir.SemanticClones` — the Type-4 twin of SimilarCode: independent
    /// RE-IMPLEMENTATIONS (high cosine, LOW structural agreement), the exact
    /// complement of the pair space SimilarCode keeps. Lens 12 on the chain since
    /// 2026-08-25; before that it was foldable only on demand, which is why this
    /// pane had no view for it at all.
    pub semantic_clones: serde_json::Value,
    /// `Mimir.DataReach` — which code touches which warehouse table, who reaches it.
    pub data_reach: serde_json::Value,
    /// `Mimir.Trust` — bottleneck trust from the entry surface (act / hold / refuse).
    pub trust: serde_json::Value,
    /// `Mimir.NameCollision` — CALLS landing on merged ids (call-graph soundness).
    pub name_collision: serde_json::Value,
    /// `Mimir.SymbolIdentity` — own definitions whose id the fold merged.
    pub symbol_identity: serde_json::Value,
    /// `Mimir.UndocApi` — undocumented public API ranked by churn.
    pub undoc_api: serde_json::Value,
    /// `Mimir.ReachableVuln` — advisories gated on whether own code calls the crate.
    pub reachable_vuln: serde_json::Value,
    /// `Mimir.Subclone` — SUB-ITEM clone regions of ONE repo's source: nornir's
    /// SOURCE-door lens (it folds source text with the room's item spans, not a
    /// warehouse table — `nornir::mimir_lens::subclone` module docs). PER-REPO by
    /// construction: `repo = ""` is the lens's own REFUSAL naming the askable repos
    /// (`repos_available`); the pane renders that refusal and makes each named repo
    /// a click — never a "clean constellation".
    pub subclone: serde_json::Value,
    /// `Mimir.SemanticTwins` — v1's semantic twins on v2: Type-4 twins (cosine ≥
    /// 0.882 AND structural ≤ 0.35 AND a body written differently AND mutual kNN
    /// k = 8) and Type-3 copies, each pair with both members and a `why`.
    pub semantic_twins: serde_json::Value,
    /// `Mimir.GroupTwins` — the same semantic GROUP of statements in two functions
    /// of ONE repo (nornir's second SOURCE-door lens). PER-REPO like subclone:
    /// `repo = ""` is its REFUSAL naming `repos_available`, rendered as a refusal
    /// with each repo a click — never as "no twins".
    pub group_twins: serde_json::Value,
    /// `Mimir.ReimplementsShared` — a crate that imports crate B and holds a copy
    /// or twin of a B function: one row per function to replace, both sides.
    pub reimplements_shared: serde_json::Value,
}

/// **Every lens the thin pane asks**: nornir's lens name (what the server
/// dispatches and every answer's `kind` says) beside the key the pane's state
/// carries it under. 19 of `nornir::mimir_lens::LENSES` (the 18 plus
/// `semantic_twins`), plus the two SOURCE-door lenses `subclone` and `group_twins` —
/// the SOURCE-door lens nornir's `answer()` dispatches beside them (it is not in
/// `LENSES` because it has no table feed and, so far, no `Mimir.*` verb on
/// nornir's own proto; dwarves' trimmed proto declares the verb so the row works
/// the day it lands, and until then a live server's `Unimplemented` is shown as
/// this one lens's error).
pub const THIN_LENSES: [(&str, &str); 22] = [
    ("generic_cost", "bloat"),
    ("zombie_features", "zombie"),
    ("change_risk", "risk"),
    ("weak_tests", "weak_tests"),
    ("bench_gaps", "bench_gaps"),
    ("similar_code", "similar_code"),
    ("semantic_clones", "semantic_clones"),
    ("villa", "villa"),
    ("churn", "churn"),
    ("anomaly", "anomaly"),
    ("world_impact", "world_impact"),
    ("binary_drift", "binary_drift"),
    ("data_reach", "data_reach"),
    ("trust", "trust"),
    ("name_collision", "name_collision"),
    ("symbol_identity", "symbol_identity"),
    ("undoc_api", "undoc_api"),
    ("reachable_vuln", "reachable_vuln"),
    ("subclone", "subclone"),
    // The twins rows (2026-10-20): v1's semantic twins on v2, and the group twins
    // of ONE repo's source (per-repo, its refusal names the askable repos).
    ("semantic_twins", "semantic_twins"),
    ("group_twins", "group_twins"),
    // A library re-implemented by a crate that imports it (2026-10-22).
    ("reimplements_shared", "reimplements_shared"),
];

/// One lens's answer, landing on its own. `last` is false while the server says
/// the lens is `queued` / `running` on jera: its worker asks again.
#[derive(Clone, Debug)]
pub struct LensUpdate {
    pub lens: &'static str,
    pub answer: serde_json::Value,
    pub last: bool,
}

/// A lens that has no answer yet: asked, nothing back (`pending`).
pub fn pending_envelope(lens: &str) -> serde_json::Value {
    serde_json::json!({
        "kind": lens,
        "worst": [],
        "feeds": [],
        "evidence_note": format!("{lens}: asked — waiting for the server's answer"),
        "lens_meta": { "state": "pending", "lens": lens },
    })
}

/// A lens whose call FAILED (transport, auth, `Unimplemented`, a refusal by status):
/// this lens's error, never the tab's. The message is the call's own.
pub fn error_envelope(lens: &str, msg: &str) -> serde_json::Value {
    serde_json::json!({
        "kind": lens,
        "worst": [],
        "feeds": [],
        "evidence_note": format!("{lens}: the call failed — {msg}"),
        "lens_meta": { "state": "error", "lens": lens, "error": msg },
    })
}

impl RemoteReports {
    /// The answer of `lens` (nornir's name).
    pub fn slot(&self, lens: &str) -> Option<&serde_json::Value> {
        Some(match lens {
            "generic_cost" => &self.bloat,
            "zombie_features" => &self.zombie,
            "change_risk" => &self.risk,
            "weak_tests" => &self.weak_tests,
            "bench_gaps" => &self.bench_gaps,
            "similar_code" => &self.similar_code,
            "semantic_clones" => &self.semantic_clones,
            "villa" => &self.villa,
            "churn" => &self.churn,
            "anomaly" => &self.anomaly,
            "world_impact" => &self.world_impact,
            "binary_drift" => &self.binary_drift,
            "data_reach" => &self.data_reach,
            "trust" => &self.trust,
            "name_collision" => &self.name_collision,
            "symbol_identity" => &self.symbol_identity,
            "undoc_api" => &self.undoc_api,
            "reachable_vuln" => &self.reachable_vuln,
            "subclone" => &self.subclone,
            "semantic_twins" => &self.semantic_twins,
            "group_twins" => &self.group_twins,
            "reimplements_shared" => &self.reimplements_shared,
            _ => return None,
        })
    }

    pub fn slot_mut(&mut self, lens: &str) -> Option<&mut serde_json::Value> {
        Some(match lens {
            "generic_cost" => &mut self.bloat,
            "zombie_features" => &mut self.zombie,
            "change_risk" => &mut self.risk,
            "weak_tests" => &mut self.weak_tests,
            "bench_gaps" => &mut self.bench_gaps,
            "similar_code" => &mut self.similar_code,
            "semantic_clones" => &mut self.semantic_clones,
            "villa" => &mut self.villa,
            "churn" => &mut self.churn,
            "anomaly" => &mut self.anomaly,
            "world_impact" => &mut self.world_impact,
            "binary_drift" => &mut self.binary_drift,
            "data_reach" => &mut self.data_reach,
            "trust" => &mut self.trust,
            "name_collision" => &mut self.name_collision,
            "symbol_identity" => &mut self.symbol_identity,
            "undoc_api" => &mut self.undoc_api,
            "reachable_vuln" => &mut self.reachable_vuln,
            "subclone" => &mut self.subclone,
            "semantic_twins" => &mut self.semantic_twins,
            "group_twins" => &mut self.group_twins,
            "reimplements_shared" => &mut self.reimplements_shared,
            _ => return None,
        })
    }

    /// Every lens asked, none answered yet.
    pub fn all_pending() -> Self {
        let mut r = Self::default();
        for (lens, _) in THIN_LENSES {
            if let Some(v) = r.slot_mut(lens) {
                *v = pending_envelope(lens);
            }
        }
        r
    }

    /// The lenses whose answer is still to come (asked, queued or running).
    pub fn waiting(&self) -> Vec<(&'static str, String)> {
        THIN_LENSES
            .iter()
            .filter_map(|(lens, _)| {
                let v = self.slot(lens)?;
                let w = lens_state_word(v);
                matches!(w, "pending" | "queued" | "running").then(|| (*lens, w.to_string()))
            })
            .collect()
    }

    /// `Some(reason)` when EVERY lens failed — a server nobody reached, not 21
    /// separate findings: the pane says UNAVAILABLE once, with the first reason.
    pub fn all_failed(&self) -> Option<String> {
        let mut first = None;
        for (lens, _) in THIN_LENSES {
            let v = self.slot(lens)?;
            if lens_state_word(v) != "error" {
                return None;
            }
            first.get_or_insert_with(|| {
                v.pointer("/lens_meta/error")
                    .and_then(|e| e.as_str())
                    .unwrap_or("")
                    .to_string()
            });
        }
        first
    }
}

/// How long a lens the server keeps `queued` / `running` is asked again before
/// the tab stops asking (the job itself runs on).
const RE_ASK_BOUND: std::time::Duration = std::time::Duration::from_secs(20 * 60);

/// **The THIN lens source: nornir's six `Mimir.*` RPCs over gRPC.**
///
/// The shipped source when dwarves runs THIN against a nornir-server (t14s against
/// oden): the server HAS the warehouse and the fresh data, so a thin pane asks IT to
/// fold the lenses rather than trying to fold locally over a warehouse it does not
/// own. The QUERY half of every lens lives in nornir (ARCHITECTURE L5a); this is the
/// client for the `Mimir.{GenericCost,ZombieFeatures,ChangeRisk,SimilarCode,
/// WeakTests,BenchGaps}` reads. The fold code is IDENTICAL on both sides (it was
/// lifted verbatim into nornir), so the thin finding-set equals the local fold for
/// the same warehouse.
#[derive(Clone)]
pub struct RemoteLensSource {
    pub endpoint: String,
    pub token: String,
    pub workspace: String,
    /// Optional repo filter (""=all repos), forwarded to every lens RPC.
    pub repo: String,
    /// The `Mimir.Subclone` repo scope — its OWN knob, not [`repo`](Self::repo):
    /// subclone is per-repo by construction, and `""` deliberately asks for the
    /// lens's refusal (which names the askable repos) while every other lens
    /// reads `""` as "all repos".
    pub subclone_repo: String,
    /// The `Mimir.GroupTwins` repo scope — its own knob, as subclone's.
    pub group_twins_repo: String,
    /// Result cap per lens (0 = each lens's server default).
    pub limit: u32,
    /// GenericCost instantiation floor (0 = the server default of 2).
    pub min_instantiations: u32,
    /// `Mimir.Churn` band: "high" / "low" / "band" (default "high").
    pub churn_band: String,
    /// `Mimir.Churn` low cutoff (band-/low-pass change-frequency threshold).
    pub churn_cutoff_low: i64,
    /// `Mimir.Churn` high cutoff (band-/high-pass change-frequency threshold).
    pub churn_cutoff_high: i64,
}

impl RemoteLensSource {
    pub fn new(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        Self {
            endpoint: endpoint.into(),
            token: token.into(),
            workspace: workspace.into(),
            repo: String::new(),
            subclone_repo: String::new(),
            group_twins_repo: String::new(),
            limit: 0,
            min_instantiations: 0,
            churn_band: "high".to_string(),
            churn_cutoff_low: 2,
            churn_cutoff_high: 5,
        }
    }
}

impl LensSource for RemoteLensSource {
    /// A remote source folds on the SERVER — it has no local `Sources` to hand back.
    /// The pane never calls this (it takes the [`remote`](LensSource::remote) path); it
    /// returns an honest error rather than a silent empty in case something does.
    fn load(&self) -> Result<Sources, String> {
        // CODED `dwarves-mimir-3`: a remote (server-folding) source has no local warehouse.
        Err(dwarves::dwarves_err!(
            mimir,
            3,
            "RemoteLensSource folds on the server via Mimir.* RPCs — it has no local \
             warehouse to load"
        )
        .to_string())
    }

    fn describe(&self) -> String {
        format!(
            "nornir Mimir.* RPCs @ {} / {}",
            self.endpoint, self.workspace
        )
    }

    fn folds_on_the_server(&self) -> bool {
        true
    }

    fn remote(&self) -> Option<Result<RemoteReports, String>> {
        // Every lens on its own: one that fails or waits does not take the others
        // with it. Re-asks a `queued` / `running` lens until it lands.
        let mut r = RemoteReports::all_pending();
        for (lens, _) in THIN_LENSES {
            self.ask_until_answered(lens, |u| {
                if let Some(slot) = r.slot_mut(u.lens) {
                    *slot = u.answer;
                }
                true
            });
        }
        Some(match r.all_failed() {
            Some(why) => Err(why),
            None => Ok(r),
        })
    }

    fn set_churn_params(&mut self, band: &str, cutoff_low: i64, cutoff_high: i64) {
        self.churn_band = band.to_string();
        self.churn_cutoff_low = cutoff_low;
        self.churn_cutoff_high = cutoff_high;
    }

    fn set_subclone_repo(&mut self, repo: &str) {
        self.subclone_repo = repo.to_string();
    }

    fn set_group_twins_repo(&mut self, repo: &str) {
        self.group_twins_repo = repo.to_string();
    }

    /// One lens again, on its own worker — a CLONE of this source, exactly as
    /// [`remote_spawn`](LensSource::remote_spawn) spawns each of its workers.
    fn remote_spawn_one(
        &self,
        lens: &'static str,
    ) -> Option<std::sync::mpsc::Receiver<LensUpdate>> {
        let (tx, rx) = std::sync::mpsc::channel();
        let me = self.clone();
        std::thread::Builder::new()
            .name(format!("mimir-lens-{lens}-again"))
            .spawn(move || me.ask_until_answered(lens, |u| tx.send(u).is_ok()))
            .ok()?;
        Some(rx)
    }

    /// One worker per lens over a CLONE of this source (strings and knobs —
    /// nothing shared): each sends its lens's answers as they land, re-asking a
    /// queued one, and the channel closes when the last worker is done.
    fn remote_spawn(&self) -> Option<std::sync::mpsc::Receiver<LensUpdate>> {
        let (tx, rx) = std::sync::mpsc::channel();
        for (lens, _) in THIN_LENSES {
            let me = self.clone();
            let tx = tx.clone();
            std::thread::Builder::new()
                .name(format!("mimir-lens-{lens}"))
                .spawn(move || me.ask_until_answered(lens, |u| tx.send(u).is_ok()))
                .ok()?;
        }
        Some(rx)
    }
}

impl RemoteLensSource {
    /// Ask `lens` once. A failed call is THIS lens's error envelope.
    pub fn ask_once(&self, lens: &str) -> serde_json::Value {
        use nornir_warehouse_embed::mimir as m;
        let (e, t, w, repo, lim) = (
            self.endpoint.as_str(),
            self.token.as_str(),
            self.workspace.as_str(),
            self.repo.as_str(),
            self.limit,
        );
        let r = match lens {
            "generic_cost" => m::generic_cost(e, t, w, repo, self.min_instantiations, lim),
            "zombie_features" => m::zombie_features(e, t, w, repo, lim),
            "change_risk" => m::change_risk(e, t, w, repo, lim),
            "weak_tests" => m::weak_tests(e, t, w, repo, lim),
            "bench_gaps" => m::bench_gaps(e, t, w, repo, lim),
            "similar_code" => m::similar_code(e, t, w, repo, lim),
            "semantic_clones" => m::semantic_clones(e, t, w, repo, lim),
            "villa" => m::villa(e, t, w, repo, lim),
            "churn" => m::churn(
                e,
                t,
                w,
                repo,
                &self.churn_band,
                self.churn_cutoff_low,
                self.churn_cutoff_high,
                lim,
            ),
            "anomaly" => m::anomaly(e, t, w, repo, 0.0, lim),
            "world_impact" => m::world_impact(e, t, w, repo, lim),
            "binary_drift" => m::binary_drift(e, t, w, repo, lim),
            "data_reach" => m::data_reach(e, t, w, repo, lim),
            "trust" => m::trust(e, t, w, repo, 0.0, 0.0, lim),
            "name_collision" => m::name_collision(e, t, w, repo, lim),
            "symbol_identity" => m::symbol_identity(e, t, w, repo, lim),
            "undoc_api" => m::undoc_api(e, t, w, repo, 0, lim),
            "reachable_vuln" => m::reachable_vuln(e, t, w, repo, lim),
            // The SOURCE-door lens rides its OWN repo knob: per-repo by
            // construction, and "" asks for the refusal that names the repos.
            "subclone" => m::subclone(e, t, w, &self.subclone_repo, lim),
            "semantic_twins" => m::semantic_twins(e, t, w, repo, lim),
            // The second SOURCE-door lens, on its own repo knob like subclone.
            "group_twins" => m::group_twins(e, t, w, &self.group_twins_repo, lim),
            "reimplements_shared" => m::reimplements_shared(e, t, w, repo, lim),
            other => return error_envelope(other, "not a lens this pane asks"),
        };
        r.unwrap_or_else(|x| error_envelope(lens, &x.to_string()))
    }

    /// Ask `lens` until its answer is not `queued` / `running`, handing every
    /// answer to `send` (which returns false when nobody listens any more).
    pub fn ask_until_answered(&self, lens: &'static str, send: impl FnMut(LensUpdate) -> bool) {
        drive_until_answered(lens, || self.ask_once(lens), send, RE_ASK_BOUND);
    }
}

/// **A failed connect is not the lens's answer.** Every ask opens its own channel
/// with a 3 s connect timeout; over tailscale, with 21 lenses polling a loaded
/// server, one connect in many misses it. Such an ask is asked again within the
/// bound, so one dropped connect no longer pins the lens at ERROR for good.
fn is_transport_failure(v: &serde_json::Value) -> bool {
    lens_state_word(v) == "error"
        && v.pointer("/lens_meta/error")
            .and_then(|e| e.as_str())
            .is_some_and(|e| {
                e.contains("transport error") || e.contains("Unavailable") || e.contains("connect")
            })
}

/// The ask loop behind [`RemoteLensSource::ask_until_answered`]: `queued` / `running`
/// and transport failures are asked again (pause 300 ms growing to 3 s) until
/// `bound`; a transport failure past the bound is handed on as it came.
fn drive_until_answered(
    lens: &'static str,
    mut ask: impl FnMut() -> serde_json::Value,
    mut send: impl FnMut(LensUpdate) -> bool,
    bound: std::time::Duration,
) {
    let t0 = std::time::Instant::now();
    let mut pause = std::time::Duration::from_millis(300);
    loop {
        let answer = ask();
        let word = lens_state_word(&answer).to_string();
        let waiting = matches!(word.as_str(), "queued" | "running");
        let retry = is_transport_failure(&answer) && t0.elapsed() <= bound;
        if waiting && t0.elapsed() > bound {
            let msg = format!(
                "still {word} on the server after {}s — the tab stopped asking; the job runs on, ask again later",
                t0.elapsed().as_secs()
            );
            send(LensUpdate { lens, answer: error_envelope(lens, &msg), last: true });
            return;
        }
        if !retry && (!send(LensUpdate { lens, answer, last: !waiting }) || !waiting) {
            return;
        }
        std::thread::sleep(pause);
        pause = (pause * 3 / 2).min(std::time::Duration::from_secs(3));
    }
}

/// Which lens the pane is showing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
    Bloat,
    Zombie,
    Risk,
    /// Tests that cannot go red (`Mimir.WeakTests`).
    WeakTests,
    /// Hot paths with no bencher (`Mimir.BenchGaps`).
    BenchGaps,
    /// The relocated dupes query (`Mimir.SimilarCode`).
    SimilarCode,
    /// Impure / shell-out heresy (`Mimir.Villa`) — the new purity lens.
    Villa,
    /// Drift between successive Build Thing artifacts of one variant
    /// (`Mimir.BinaryDrift`): what changed in the bytes, and whether the
    /// provenance (commit, toolchain) explains it.
    BinaryDrift,
    /// Cross-repo qualified-path collisions (`Mimir.NameCollision`) — call-graph
    /// soundness, folded LOCALLY over `symbol_facts` (the fat/embedded leg).
    NameCollision,
    /// The Brokkr software-criteria scorecard — the software-as-a-graph quality
    /// view: one factor per lens, folded into a grade.
    Quality,
    /// `Mimir.Churn` — the frequency filter (high/low/band-pass) over the change-series.
    Churn,
    /// `Mimir.Anomaly` — statistical outliers on the change-series.
    Anomaly,
    /// `Mimir.WorldImpact` — changes ranked by world + constellation downstream.
    WorldImpact,
    /// Independent re-implementations (`Mimir.SemanticClones`) — SimilarCode's
    /// AND-gate inverted. A pair belongs to one lens or the other, never both.
    SemanticClones,
    /// `Mimir.DataReach` — which code reaches which warehouse table.
    DataReach,
    /// `Mimir.Trust` — bottleneck trust: act / hold / refuse.
    Trust,
    /// `Mimir.SymbolIdentity` — own definitions whose id the fold merged.
    SymbolIdentity,
    /// `Mimir.UndocApi` — undocumented public API ranked by churn.
    UndocApi,
    /// `Mimir.ReachableVuln` — advisories gated on reachability.
    ReachableVuln,
    /// `Mimir.Subclone` — SUB-ITEM clone regions of ONE repo's source (nornir's
    /// SOURCE-door lens). With no repo chosen it renders the lens's REFUSAL and
    /// each named repo is a click; with one, one row per finding, clickable
    /// through to its source.
    Subclone,
    /// `Mimir.SemanticTwins` — Type-4 twins + Type-3 copies, one row per pair with
    /// both members; a pair click opens both members' source side by side.
    SemanticTwins,
    /// `Mimir.GroupTwins` — the same statement group in two functions of ONE repo
    /// (per-repo: its refusal names the repos, each a click).
    GroupTwins,
    /// `Mimir.ReimplementsShared` — a function to replace with a call to the
    /// library its crate already imports, one row per pair (both sides).
    ReimplementsShared,
    /// The L5a staging note + the specified nornir RPCs.
    Belongs,
}

impl View {
    fn as_str(self) -> &'static str {
        match self {
            View::Bloat => "bloat",
            View::Zombie => "zombie",
            View::Risk => "risk",
            View::WeakTests => "weak_tests",
            View::BenchGaps => "bench_gaps",
            View::SimilarCode => "similar_code",
            View::SemanticClones => "semantic_clones",
            View::Villa => "villa",
            View::BinaryDrift => "binary_drift",
            View::NameCollision => "name_collision",
            View::Quality => "quality",
            View::Churn => "churn",
            View::Anomaly => "anomaly",
            View::WorldImpact => "world_impact",
            View::DataReach => "data_reach",
            View::Trust => "trust",
            View::SymbolIdentity => "symbol_identity",
            View::UndocApi => "undoc_api",
            View::ReachableVuln => "reachable_vuln",
            View::Subclone => "subclone",
            View::SemanticTwins => "semantic_twins",
            View::GroupTwins => "group_twins",
            View::ReimplementsShared => "reimplements_shared",
            View::Belongs => "belongs",
        }
    }
    fn parse(s: &str) -> Option<Self> {
        match s {
            "bloat" => Some(View::Bloat),
            "zombie" => Some(View::Zombie),
            "risk" => Some(View::Risk),
            "weak_tests" => Some(View::WeakTests),
            "bench_gaps" => Some(View::BenchGaps),
            "similar_code" => Some(View::SimilarCode),
            "semantic_clones" => Some(View::SemanticClones),
            "villa" => Some(View::Villa),
            "binary_drift" => Some(View::BinaryDrift),
            "name_collision" => Some(View::NameCollision),
            "quality" => Some(View::Quality),
            "churn" => Some(View::Churn),
            "anomaly" => Some(View::Anomaly),
            "world_impact" => Some(View::WorldImpact),
            "data_reach" => Some(View::DataReach),
            "trust" => Some(View::Trust),
            "symbol_identity" => Some(View::SymbolIdentity),
            "undoc_api" => Some(View::UndocApi),
            "reachable_vuln" => Some(View::ReachableVuln),
            "subclone" => Some(View::Subclone),
            "semantic_twins" => Some(View::SemanticTwins),
            "group_twins" => Some(View::GroupTwins),
            "reimplements_shared" => Some(View::ReimplementsShared),
            "belongs" => Some(View::Belongs),
            _ => None,
        }
    }
}

/// **The Mímir menu — the ONE grouped set of code-graph / quality operations.**
///
/// The owner: *"alla operationer för att hitta dubbletter och liknande skall ligga
/// under Mimir-meny, nu ligger de utspridda."* This const is the single source for
/// the pane's in-pane selector AND the top-level **Mímir** menubar group (dwarves-gui
/// `tabbed.rs`), so the consolidation cannot drift between the two surfaces. Each
/// entry is `(view_id, label)` where `view_id` matches [`View::as_str`] — the menubar
/// fires `mimir:<view_id>` to jump to the Lenses tab and select that lens.
pub const MIMIR_MENU: &[(&str, &str)] = &[
    ("quality", "⚖ Quality (Brokkr)"),
    ("similar_code", "🔁 Similar Code"),
    ("semantic_twins", "👯 Semantic twins"),
    ("reimplements_shared", "♻ Re-implemented libraries (call the library)"),
    ("zombie", "🧟 Zombie Features"),
    ("risk", "🎯 Change Risk"),
    ("weak_tests", "🧪 Weak Tests"),
    ("bench_gaps", "⏱ Bench Gaps"),
    ("bloat", "📦 Generic Cost"),
    ("villa", "⚠ Villa (heresy)"),
    // The change/anomaly operations over the temporal graph — first-class entries under
    // the ONE Mímir menu, labelled by the OPERATION (no "lens" word — Mímir explains it).
    ("churn", "📈 Churn (high/low/band-pass)"),
    ("anomaly", "⚡ Anomalies"),
    ("world_impact", "🌍 World impact"),
    ("binary_drift", "🧬 Binary drift (build ↔ build)"),
    // Cross-repo qualified-path collisions — call-graph soundness, folded locally.
    ("name_collision", "🧭 Name collisions (graph soundness)"),
    ("symbol_identity", "🪪 Symbol identity (merged ids)"),
    ("data_reach", "🗄 Data reach (who touches which table)"),
    ("trust", "🛡 Trust (act / hold / refuse)"),
    ("undoc_api", "📝 Undocumented public API"),
    ("reachable_vuln", "🐞 Reachable vulnerabilities"),
    // The SOURCE-door dedup operation: shared runs SMALLER than the item they sit
    // in, which no chunk-keyed tier can state — one repo at a time by construction.
    ("subclone", "🧩 Sub-item clones (one repo)"),
    ("group_twins", "🪢 Group twins (one repo)"),
    ("belongs", "① Where this belongs"),
];

/// One Brokkr software-criteria factor, scored from a lens's finding count.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokkrFactor {
    /// The lens this factor reads (`villa`, `similar_code`, …).
    pub lens: &'static str,
    /// The human name of the factor.
    pub name: &'static str,
    /// The Brokkr software-criterion this factor upholds.
    pub criterion: &'static str,
    /// The finding count from the lens — `None` when the lens did not compute
    /// (`lens_meta.state` is not complete / partial / empty_fact): there is no count.
    pub count: Option<u64>,
    /// `pass` (0) · `warn` (1..=amber) · `fail` (> amber) · [`NOT_MEASURED`].
    pub status: &'static str,
    /// The lens's own `lens_meta.state` word, verbatim (`""` when it sent none).
    pub state: String,
}

/// The status of a factor whose lens did not compute. Never `pass`.
pub const NOT_MEASURED: &str = "not measured";

fn grade(count: u64, amber: u64) -> &'static str {
    if count == 0 {
        "pass"
    } else if count <= amber {
        "warn"
    } else {
        "fail"
    }
}

/// Count a lens's ranked findings — the array under `key` (falling back to a scalar
/// count field). The software-as-a-graph view is, at minimum, counts-per-lens.
fn lens_count(v: &serde_json::Value, key: &str, scalar: &str) -> u64 {
    if let Some(n) = v.get(scalar).and_then(|x| x.as_u64()) {
        return n;
    }
    v.get(key)
        .and_then(|a| a.as_array())
        .map(|a| a.len() as u64)
        .unwrap_or(0)
}

/// **The Brokkr scorecard — software AS A GRAPH, graded by its lenses.**
///
/// Each lens is one factor of Brokkr's software criteria (`brokkr-software-criteria.md`):
/// the new Villa lens scores PURITY (baremetal — no shelling out to OS tooling), the
/// others score duplication, dead code, test strength and bench coverage. A pure fold
/// over the lens `state_json`s (it adds no numbers of its own — counts come from the
/// lenses), returned as the observable summary the Quality view renders.
pub fn brokkr_scorecard(r: &RemoteReports) -> serde_json::Value {
    let heresies = lens_count(&r.villa, "findings", "heresies");
    let bench_never = r
        .bench_gaps
        .get("per_repo")
        .and_then(|a| a.as_array())
        .map(|a| {
            a.iter()
                .filter(|rp| {
                    rp.get("never_benched")
                        .and_then(|b| b.as_bool())
                        .unwrap_or(false)
                })
                .count() as u64
        })
        .unwrap_or(0);
    // A factor is graded only when its lens COMPUTED (`lens_meta.state` complete /
    // partial / empty_fact). A lens that is not wired, refused, or empty because an
    // input is missing has no count: grading its empty list would turn a missing
    // producer into a "pass" — the fake zero this scorecard must never print.
    let factor = |lens: &'static str,
                  report: &serde_json::Value,
                  name: &'static str,
                  criterion: &'static str,
                  count: u64,
                  amber: u64| {
        let answer = LensAnswer::of(report);
        BrokkrFactor {
            lens,
            name,
            criterion,
            count: answer.is_measured().then_some(count),
            status: if answer.is_measured() {
                grade(count, amber)
            } else {
                NOT_MEASURED
            },
            state: lens_state_word(report).to_string(),
        }
    };
    let factors = [
        factor(
            "villa",
            &r.villa,
            "Purity (baremetal, no shell-out)",
            "No dependence on OS distribution / package managers and their tooling — \
             stay pure Rust, don't shell out.",
            heresies,
            0, // any heresy is a fail — this is the point of the lens
        ),
        factor(
            "similar_code",
            &r.similar_code,
            "Minimal duplication (REUSE)",
            "Single or minimal dependencies — the fewer, the merrier; no copy-paste twins.",
            lens_count(&r.similar_code, "top_clusters", "clusters"),
            3,
        ),
        factor(
            "zombie",
            &r.zombie,
            "No dead features",
            "Lean — no gated code no caller can reach.",
            // `zombies` is the lens's TOTAL (its `worst` is only the page it sent).
            lens_count(&r.zombie, "worst", "zombies"),
            2,
        ),
        factor(
            "weak_tests",
            &r.weak_tests,
            "Tests can go red",
            "Superb functionality is PROVEN — a test that cannot fail proves nothing.",
            lens_count(&r.weak_tests, "worst", "weak"),
            2,
        ),
        factor(
            "bench_gaps",
            &r.bench_gaps,
            "Hot paths benched",
            "Superb functionality shown by zero-copy · multi-core · SIMD — measured, not asserted.",
            bench_never,
            1,
        ),
    ];

    let fails = factors.iter().filter(|f| f.status == "fail").count();
    let warns = factors.iter().filter(|f| f.status == "warn").count();
    let unmeasured = factors.iter().filter(|f| f.status == NOT_MEASURED).count();
    let overall = if fails > 0 {
        "heresy" // at least one criterion is broken — true whatever the others say
    } else if unmeasured > 0 {
        "unknown" // nothing failed, but not every criterion was measured: not "clean"
    } else if warns > 0 {
        "warn"
    } else {
        "clean"
    };
    // A blunt 0..=100 score: full marks minus a graded penalty per factor. A SORT KEY
    // for the scorecard, never a lone verdict — the factors are the columns. No score
    // at all while a factor is unmeasured: full marks minus nothing is not a score.
    let score: Option<i64> = (unmeasured == 0).then(|| {
        (100 - factors
            .iter()
            .map(|f| match f.status {
                "fail" => 20,
                "warn" => 8,
                _ => 0,
            })
            .sum::<i64>())
        .max(0)
    });

    serde_json::json!({
        "kind": "brokkr_quality",
        "overall": overall,
        "score": score,
        "unmeasured": unmeasured,
        "heresies": LensAnswer::of(&r.villa).is_measured().then_some(heresies),
        "factors": factors.iter().map(|f| serde_json::json!({
            "lens": f.lens,
            "name": f.name,
            "criterion": f.criterion,
            "count": f.count,
            "status": f.status,
            "state": f.state,
        })).collect::<Vec<_>>(),
        "attribution": "Brokkr's software criteria (dwarves/brokkr-software-criteria.md) — \
                        the law the forge builds by. Each factor is one Mímir lens over the CPG.",
    })
}

/// The **🔍 Lenses** pane.
pub struct LensPane {
    title: String,
    source: Box<dyn LensSource>,
    bloat: Option<bloat::BloatReport>,
    zombie: Option<zombie::ZombieReport>,
    risk: Option<risk::RiskReport>,
    /// The coverage index — carries the weak-tests report and the per-repo
    /// bench-coverage verdicts (the two widened views), computed in the SAME
    /// `scan()` so all six lenses read one warehouse read.
    idx: Option<CoverageIndex>,
    /// The name-collision reading — cross-repo qualified-path clashes over `symbol_facts`,
    /// folded LOCALLY in the SAME `scan()` (it reads a table the pane already loads).
    collide: Option<collide::CollisionReport>,
    error: Option<String>,
    view: View,
    /// Show only the ACTIONABLE (named) generic groups in the bloat view.
    actionable_only: bool,
    /// The THIN reading: the six lenses as nornir's `Mimir.*` RPCs returned them.
    /// `Some` ⇒ the source folded on the server and the pane renders its findings;
    /// `None` ⇒ the fat/local path (the typed reports above).
    remote: Option<RemoteReports>,
    /// A server fold in flight (the worker's receiver + when it started). The frame
    /// polls it; `wait()` blocks on it for the robot seam.
    pending: Option<(std::sync::mpsc::Receiver<LensUpdate>, std::time::Instant)>,
    /// The Churn frequency-filter controls (Change view): band + low/high cutoffs.
    /// Threaded into the source before a thin scan (`Mimir.Churn`).
    churn_band: String,
    churn_cutoff_low: i64,
    churn_cutoff_high: i64,
    /// **Show the find→fix block** — the actionable next step per finding. On by default
    /// (the fix IS the point of a lens); a read-only suggestion, never an applied edit.
    show_fixes: bool,
    /// Whether the read-on-open has already fired. Reset by
    /// [`forget`](Self::forget) so a workspace change reads the new workspace.
    auto_read: bool,
    /// The **Similar Code sub-view** — the former top-level Dupes tab, folded in here
    /// under Lenses (LAW 5 / the owner 2026-08-22: "IGEN dublett, skall inte ligga i top
    /// menyn"). It READS `lens_similar_code` and carries the local-fold APPLY seam the
    /// generic lens renderer has no place for. `None` on the fixture/robot constructors.
    dupes: Option<Box<crate::dupes::DupesPane>>,
    /// **The clone-source door** — the source reader a semantic-clone member pick
    /// fills: `Knowledge.Source` on the member's moniker (the owner, 2026-10-18:
    /// "make sure if click we see source code or at least its url"). Room-less on
    /// the bare constructors (a pick then says so); `lens_pane` wires the forge.
    code: crate::graph_source::GraphSourcePane,
    /// **The subclone repo scope** — which ONE repo `Mimir.Subclone` folds (the
    /// lens is per-repo by construction). `""` = none chosen: the lens answers its
    /// refusal, which NAMES the askable repos, and each name rendered is a click
    /// that writes this filter and re-asks the one lens.
    subclone_repo: String,
    /// One-lens RE-ASKS in flight (a subclone / group_twins repo click): their own
    /// receivers, because the full scan's may still be draining other lenses when
    /// the click lands.
    reask_pending: Vec<std::sync::mpsc::Receiver<LensUpdate>>,
    /// **The group_twins repo scope** — as [`subclone_repo`](Self::subclone_repo),
    /// for the second SOURCE-door lens.
    group_twins_repo: String,
    /// **The twin pair's source, side by side** — the owner, 2026-10-20: every
    /// twin member is a pointer to source. A pair click asks the room's
    /// `Knowledge.Source` for EACH member (the server reads the text; the UI reads
    /// no file) and the two answers are drawn beside each other.
    twin_a: crate::graph_source::GraphSourcePane,
    twin_b: crate::graph_source::GraphSourcePane,
    /// Which pair is open: (lens, row of [`twin_rows`]).
    twin_pick: Option<(&'static str, usize)>,
}

impl LensPane {
    pub fn live(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        Self::with_source(Box::new(WarehouseLens::new(endpoint, token, workspace)))
    }

    /// **The THIN pane** — folds on the server over nornir's six `Mimir.*` RPCs. The
    /// shipped choice when dwarves runs thin against a nornir-server (t14s → oden): the
    /// server owns the warehouse, so it does the fold and the pane renders the result.
    pub fn thin(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        Self::with_source(Box::new(RemoteLensSource::new(endpoint, token, workspace)))
    }

    pub fn with_source(source: Box<dyn LensSource>) -> Self {
        Self {
            title: "Lenses".to_string(),
            source,
            bloat: None,
            zombie: None,
            risk: None,
            idx: None,
            collide: None,
            error: None,
            view: View::Bloat,
            actionable_only: false,
            remote: None,
            pending: None,
            churn_band: "high".to_string(),
            churn_cutoff_low: 2,
            churn_cutoff_high: 5,
            show_fixes: true,
            auto_read: false,
            dupes: None,
            code: crate::graph_source::GraphSourcePane::new(),
            subclone_repo: String::new(),
            reask_pending: Vec::new(),
            group_twins_repo: String::new(),
            twin_a: crate::graph_source::GraphSourcePane::new(),
            twin_b: crate::graph_source::GraphSourcePane::new(),
            twin_pick: None,
        }
    }

    /// Give the clone-source door a room to ask `Knowledge.Source` of. The shipped
    /// pane gets the forge ([`lens_pane`]); tests hand a canned room.
    pub fn set_code_room(&mut self, room: std::sync::Arc<dyn crate::room::RoomSource>) {
        self.code = crate::graph_source::GraphSourcePane::new().with_room(room.clone());
        self.twin_a = crate::graph_source::GraphSourcePane::new().with_room(room.clone());
        self.twin_b = crate::graph_source::GraphSourcePane::new().with_room(room);
    }

    /// **The click on a twin pair**: row `i` of [`twin_rows`] for `lens` — ask the
    /// room for BOTH members' source (scoped to each member's repo; a group twin's
    /// statement lines highlighted) and show them side by side.
    pub fn pick_twin_pair(&mut self, lens: &str, i: usize) {
        let Some(r) = &self.remote else { return };
        let Some(lens) = TWIN_LENSES.iter().copied().find(|l| *l == lens) else { return };
        let Some(v) = r.slot(lens) else { return };
        let Some(row) = twin_rows(lens, v).into_iter().nth(i) else { return };
        for (pane, side) in [(&mut self.twin_a, &row.a), (&mut self.twin_b, &row.b)] {
            pane.select_scoped(side.node(), (!side.repo.is_empty()).then(|| side.repo.clone()), line_ranges(&side.group_lines));
        }
        self.twin_pick = Some((lens, i));
    }

    /// **Both members' source, side by side** — under the pair list of `view`'s
    /// lens when a pair of THAT lens is open: each column is a source pane that
    /// asked the room for its member, headed `repo/file:start-end @ commit`.
    fn twin_view(&mut self, ui: &mut egui::Ui, view: View) {
        let Some((lens, _)) = self.twin_pick else { return };
        if lens != view.as_str() {
            return;
        }
        ui.separator();
        ui.label(egui::RichText::new("⇆ both members' source").strong().color(TEAL));
        let (a, b) = (&mut self.twin_a, &mut self.twin_b);
        ui.columns(2, |cols| {
            cols[0].push_id("twin-source-a", |ui| a.view(ui));
            cols[1].push_id("twin-source-b", |ui| b.view(ui));
        });
    }

    /// **The click on a repo the group_twins refusal names** — as
    /// [`pick_subclone_repo`](Self::pick_subclone_repo): write the filter, re-ask
    /// the ONE lens.
    pub fn pick_group_twins_repo(&mut self, repo: &str) {
        self.group_twins_repo = repo.to_string();
        self.source.set_group_twins_repo(repo);
        if let Some(rx) = self.source.remote_spawn_one("group_twins") {
            if let Some(s) = self.remote.as_mut().and_then(|r| r.slot_mut("group_twins")) {
                *s = pending_envelope("group_twins");
            }
            // a pair of the old reading no longer names this answer's rows
            if matches!(self.twin_pick, Some(("group_twins", _))) {
                self.twin_pick = None;
            }
            self.reask_pending.push(rx);
        }
    }

    /// **The click on a semantic clone member**: member `m` of ranked finding `c`
    /// (the thin `findings` order). Asks the room for THAT member's source by its
    /// moniker — never by name — off the UI thread; the graph-source pane lands it.
    pub fn pick_clone_member(&mut self, c: usize, m: usize) {
        let Some(r) = &self.remote else { return };
        let member = &r.semantic_clones["findings"][c]["members"][m];
        let Some(id) = member["id"].as_str() else { return };
        let label = match (member["module"].as_str(), member["ident"].as_str()) {
            (Some(md), Some(i)) => format!("{md}::{i}"),
            (None, Some(i)) => i.to_string(),
            _ => id.to_string(),
        };
        self.code.select(crate::graph_source::NodeRef {
            label,
            moniker: Some(id.to_string()),
            kind: crate::graph_source::NodeKind::Symbol,
        });
    }

    /// **The click on a repo the subclone refusal names**: write the pane's
    /// subclone repo filter and re-ask the ONE lens with it — never a whole
    /// re-scan (every other lens's answer is about the same warehouse still).
    pub fn pick_subclone_repo(&mut self, repo: &str) {
        self.subclone_repo = repo.to_string();
        self.source.set_subclone_repo(repo);
        if let Some(rx) = self.source.remote_spawn_one("subclone") {
            // The slot says `pending` while the re-ask flies, so the row never
            // shows the old refusal as if it were the new repo's answer.
            if let Some(s) = self.remote.as_mut().and_then(|r| r.slot_mut("subclone")) {
                *s = pending_envelope("subclone");
            }
            self.reask_pending.push(rx);
        }
    }

    /// **The click on a subclone finding**: ranked finding `i` of the thin `worst`
    /// list. The answer carries file+lines (`location`), never a moniker, and
    /// `Knowledge.Source` resolves a NODE (`moniker`, or `name`: a path / path
    /// suffix) — never a file — so the ask goes through the SAME
    /// [`GraphSourcePane`](crate::graph_source::GraphSourcePane) door as a
    /// semantic-clone member, by the enclosing item's path derived from the lead
    /// file (`src/util/esc.rs` → `util::esc`, a suffix the room resolves). A lead
    /// file with no derivable path (`src/lib.rs`, `src/main.rs`) asks by the file
    /// name and the room's answer says, honestly, that it names no node.
    pub fn pick_subclone_finding(&mut self, i: usize) {
        let Some(r) = &self.remote else { return };
        let f = &r.subclone["worst"][i];
        let lead = f["lead_file"].as_str().unwrap_or("");
        if lead.is_empty() {
            return;
        }
        let label = enclosing_path_of(lead).unwrap_or_else(|| lead.to_string());
        self.code.select(crate::graph_source::NodeRef {
            label,
            moniker: None,
            kind: crate::graph_source::NodeKind::Symbol,
        });
    }

    /// **Drop the current reading** so the pane reads again on its next frame —
    /// what a workspace change calls. Deliberately NOT a re-fold: the chain has
    /// already folded the new workspace's lenses, or is about to.
    pub fn forget(&mut self) {
        self.remote = None;
        self.bloat = None;
        self.zombie = None;
        self.risk = None;
        self.idx = None;
        self.collide = None;
        self.error = None;
        self.auto_read = false;
    }

    #[cfg(any(test, feature = "fixtures"))]
    pub fn fixture() -> Self {
        Self::with_source(Box::new(FixtureLens))
    }

    /// Run all three lenses. A failure keeps the previous reading and records why.
    pub fn scan(&mut self) {
        // Thread the Change view's Churn frequency-filter params into the source (a
        // remote source forwards them to Mimir.Churn; a local source ignores them).
        let (band, clo, chi) = (
            self.churn_band.clone(),
            self.churn_cutoff_low,
            self.churn_cutoff_high,
        );
        self.source.set_churn_params(&band, clo, chi);
        // Thread the subclone repo scope the same way (a remote source forwards it
        // to Mimir.Subclone; a local source ignores it — the lens is thin-only).
        let subclone_repo = self.subclone_repo.clone();
        self.source.set_subclone_repo(&subclone_repo);
        let group_twins_repo = self.group_twins_repo.clone();
        self.source.set_group_twins_repo(&group_twins_repo);
        // THIN, off-thread: the source folds on the server (Mimir.* RPCs) on a worker;
        // the frame keeps painting and `poll()` lands the result. A scan pressed while
        // one is in flight is not a second round trip.
        if self.pending.is_some() {
            return;
        }
        if let Some(rx) = self.source.remote_spawn() {
            self.pending = Some((rx, std::time::Instant::now()));
            return;
        }
        // THIN, synchronous fallback: a remote source without a spawn — render its
        // returned findings, and DO NOT fold locally over a warehouse this host does
        // not own.
        if let Some(res) = self.source.remote() {
            self.apply_remote(res);
            return;
        }
        let sources = match self.source.load() {
            Ok(s) => s,
            Err(e) => {
                self.error = Some(e);
                return;
            }
        };
        self.bloat = Some(bloat::analyze(&sources, 2, 25));
        self.zombie = Some(zombie::analyze(&sources, &self.source.manifests(), 25));

        // The risk lens's coverage input is the SAME test-reachability the coverage
        // index reports — computed once, never a second opinion.
        let mut cov_tables = BTreeMap::new();
        for t in dwarves::coverage::TABLES {
            cov_tables.insert(t.to_string(), sources.get(t).clone());
        }
        let borrowed: dwarves::coverage::Tables =
            cov_tables.iter().map(|(k, v)| (k.as_str(), v)).collect();
        let idx = dwarves::coverage::build(
            "mimir lens",
            &borrowed,
            // the local fold reads warehouse tables; the code graph is nornir's to answer
            &dwarves::coverage::GraphFeed::not_asked(LOCAL_FOLD_HAS_NO_GRAPH),
            &[],
            &Default::default(),
            0,
            1,
            1,
        );
        self.risk = Some(risk::analyze(&sources, &idx.covered_by_repo, 25));
        // The weak-tests report and the per-repo bench-coverage verdicts ride on the
        // SAME index — the widened views add no second warehouse read.
        self.idx = Some(idx);
        // The name-collision lens folds the SAME `symbol_facts` this scan already loaded
        // (graph soundness) — one more local fold, no extra warehouse read.
        self.collide = Some(collide::analyze(&sources, 25));
        self.error = None;
        self.remote = None;
    }

    /// Land a server fold: the thin reading replaces the typed (fat) reports.
    fn apply_remote(&mut self, res: Result<RemoteReports, String>) {
        match res {
            Ok(r) => {
                self.remote = Some(r);
                // The typed (fat) reports are not this reading's source of truth.
                self.bloat = None;
                self.zombie = None;
                self.risk = None;
                self.idx = None;
                self.collide = None;
                self.error = None;
            }
            Err(e) => self.error = Some(e),
        }
    }

    /// Land ONE lens's answer: the reading exists from the first answer on, and
    /// every other lens keeps what it had (its answer, or `pending`).
    fn land(&mut self, u: LensUpdate) {
        if self.remote.is_none() {
            self.remote = Some(RemoteReports::all_pending());
            // The typed (fat) reports are not this reading's source of truth.
            self.bloat = None;
            self.zombie = None;
            self.risk = None;
            self.idx = None;
            self.collide = None;
        }
        self.error = None;
        if let Some(slot) = self.remote.as_mut().and_then(|r| r.slot_mut(u.lens)) {
            *slot = u.answer;
        }
    }

    /// Every lens worker is done: a reading in which EVERY lens failed is a
    /// server nobody reached — UNAVAILABLE once, with the reason.
    fn settle(&mut self) {
        match self.remote.as_ref().map(RemoteReports::all_failed) {
            Some(Some(why)) => {
                self.remote = None;
                self.error = Some(why);
            }
            Some(None) => {}
            None => {
                self.error = Some("the lens workers ended before any lens answered".into())
            }
        }
    }

    /// Non-blocking: land every lens answer that has arrived. Called once per
    /// frame by `ui()`.
    pub fn poll(&mut self) {
        loop {
            let Some((rx, _)) = &self.pending else { break };
            match rx.try_recv() {
                Ok(u) => self.land(u),
                Err(std::sync::mpsc::TryRecvError::Empty) => break,
                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                    self.pending = None;
                    self.settle();
                    break;
                }
            }
        }
        // A one-lens RE-ASK (a repo click) lands on its own receiver: one lens,
        // so its end is no reason to settle the whole reading.
        let mut landed = Vec::new();
        self.reask_pending.retain(|rx| loop {
            match rx.try_recv() {
                Ok(u) => landed.push(u),
                Err(std::sync::mpsc::TryRecvError::Empty) => break true,
                Err(std::sync::mpsc::TryRecvError::Disconnected) => break false,
            }
        });
        for u in landed {
            self.land(u);
        }
    }

    /// Blocking: wait until every lens has its last answer. The ROBOT seam
    /// (`update_json`) uses this so a scripted `scan` is complete when the call
    /// returns; the window never does.
    pub fn wait(&mut self) {
        if let Some((rx, _)) = self.pending.take() {
            for u in rx.iter() {
                self.land(u);
            }
            self.settle();
        }
        for rx in std::mem::take(&mut self.reask_pending) {
            for u in rx.iter() {
                self.land(u);
            }
        }
    }

    /// Is a server fold in flight? (The full scan, or a subclone re-ask.)
    pub fn is_pending(&self) -> bool {
        self.pending.is_some() || !self.reask_pending.is_empty()
    }

    pub fn status(&self) -> String {
        if let Some((_, since)) = &self.pending {
            let waiting = self
                .remote
                .as_ref()
                .map(RemoteReports::waiting)
                .unwrap_or_else(|| THIN_LENSES.iter().map(|(l, _)| (*l, "pending".to_string())).collect());
            let head = format!(
                "{} of {} lenses answered · {:.0}s — waiting on: {} — each lens lands on its own; \
                 the window is not blocked",
                THIN_LENSES.len() - waiting.len(),
                THIN_LENSES.len(),
                since.elapsed().as_secs_f64(),
                waiting
                    .iter()
                    .map(|(l, w)| format!("{l} ({w})"))
                    .collect::<Vec<_>>()
                    .join(", ")
            );
            return match &self.remote {
                Some(r) => format!("{head}\n{}", answer_status(r)),
                None => head,
            };
        }
        if let Some(e) = &self.error {
            return format!("UNAVAILABLE — {e}");
        }
        if let Some(r) = &self.remote {
            // The ANSWERS header first — what each lens rests on is the question an
            // operator has before "how many findings", because a list from a lens
            // that did not compute is worse than no list.
            return format!("{}\n{}", answer_status(r), thin_status(r));
        }
        match (&self.bloat, &self.zombie, &self.risk) {
            (Some(b), Some(z), Some(r)) => format!(
                "{} artifact(s) taxed · {} zombie feature(s) ({} severe) · {} file(s) scored for risk",
                b.artifacts.len(),
                z.features.len(),
                z.severe().len(),
                r.files_scored
            ),
            // There is no Scan button to press any more (768983f) — the pane reads
            // itself on open and the folds are jera chain jobs. A status line that
            // names a control the window does not carry sends the operator looking
            // for it.
            _ => "no reading yet — the pane reads itself on open".to_string(),
        }
    }

    fn scanned(&self) -> bool {
        self.bloat.is_some() || self.remote.is_some()
    }

    /// **The stable error CODE for a failed scan** ([`dwarves::errcode`]) — `None` when the
    /// scan succeeded. A failed read is classified onto the code the underlying layer
    /// already raised (the proxy's `dwarves-proxy-2` for a dead upstream travels up in the
    /// error string), falling back to `dwarves-mimir-1` for a lens fold that did not answer
    /// and `dwarves-warehouse-1` for a raw warehouse read that could not reach a server.
    ///
    /// This is the field the robot suite asserts instead of matching `"UNAVAILABLE — …"`.
    pub fn error_code(&self) -> Option<&'static str> {
        let e = self.error.as_ref()?;
        // A code raised further down (e.g. the proxy's dead-upstream dwarves-proxy-2)
        // survives in the message — prefer it, it is the more precise diagnosis.
        if let Some(found) = dwarves::errcode::parse(e) {
            return Some(found.code);
        }
        // Otherwise classify by which leg failed: the thin (server-folded) lens path is
        // mimir-1; the fat/local warehouse read is warehouse-1.
        Some(if self.source.folds_on_the_server() {
            "dwarves-mimir-1"
        } else {
            "dwarves-warehouse-1"
        })
    }

    /// The full dual-channel scan error — the code AND the human reason. `None` when OK.
    pub fn error_object(&self) -> Option<dwarves::errcode::DwarvesError> {
        let reason = self.error.as_ref()?;
        let code = self.error_code()?;
        let e = dwarves::errcode::lookup(code)?;
        Some(dwarves::errcode::DwarvesError::new(
            e.subsystem,
            e.n,
            e.code,
            reason.clone(),
        ))
    }

    /// The per-lens `state_json` for one view — the SAME value `state_json` emits and the
    /// pane renders, whether folded on the server (thin) or locally (fat). The find→fix
    /// layer reads this so a fix is derived from EXACTLY the finding on screen.
    fn view_json(&self, view: View) -> serde_json::Value {
        use serde_json::Value::Null;
        if let Some(r) = &self.remote {
            return match view {
                View::Bloat => r.bloat.clone(),
                View::Zombie => r.zombie.clone(),
                View::Risk => r.risk.clone(),
                View::WeakTests => r.weak_tests.clone(),
                View::BenchGaps => r.bench_gaps.clone(),
                View::SimilarCode => r.similar_code.clone(),
                View::SemanticClones => r.semantic_clones.clone(),
                View::Villa => r.villa.clone(),
                View::BinaryDrift => r.binary_drift.clone(),
                View::Churn => r.churn.clone(),
                View::Anomaly => r.anomaly.clone(),
                View::WorldImpact => r.world_impact.clone(),
                View::Quality => brokkr_scorecard(r),
                View::NameCollision => r.name_collision.clone(),
                View::DataReach => r.data_reach.clone(),
                View::Trust => r.trust.clone(),
                View::SymbolIdentity => r.symbol_identity.clone(),
                View::UndocApi => r.undoc_api.clone(),
                View::ReachableVuln => r.reachable_vuln.clone(),
                View::Subclone => r.subclone.clone(),
                View::SemanticTwins => r.semantic_twins.clone(),
                View::GroupTwins => r.group_twins.clone(),
                View::ReimplementsShared => r.reimplements_shared.clone(),
                View::Belongs => Null,
            };
        }
        match view {
            View::Bloat => self
                .bloat
                .as_ref()
                .map(bloat::BloatReport::state_json)
                .unwrap_or(Null),
            View::Zombie => self
                .zombie
                .as_ref()
                .map(zombie::ZombieReport::state_json)
                .unwrap_or(Null),
            View::Risk => self
                .risk
                .as_ref()
                .map(risk::RiskReport::state_json)
                .unwrap_or(Null),
            View::WeakTests => self
                .idx
                .as_ref()
                .and_then(|i| serde_json::to_value(&i.weak).ok())
                .unwrap_or(Null),
            View::BenchGaps => self
                .idx
                .as_ref()
                .map(|i| {
                    i.repos
                        .iter()
                        .filter_map(|r| serde_json::to_value(&r.bench).ok())
                        .collect::<Vec<_>>()
                })
                .map(serde_json::Value::from)
                .unwrap_or(Null),
            // The name-collision lens folds LOCALLY (fat leg) — it reads `symbol_facts`,
            // which this host already loaded, so a local reading DOES carry it.
            View::NameCollision => self
                .collide
                .as_ref()
                .map(collide::CollisionReport::state_json)
                .unwrap_or(Null),
            // Villa / Churn / Anomaly / WorldImpact / SimilarCode / BinaryDrift / Quality
            // fold on nornir (thin-only) — a local reading has no value for them.
            _ => Null,
        }
    }

    /// The find→fix list for the current view — the actionable next step per finding.
    fn current_fixes(&self) -> Vec<Fix> {
        fixes_for(self.view, &self.view_json(self.view))
    }

    /// **The Change view's frequency-filter controls** — the signal-processing band
    /// selector (high/low/band-pass) + the low/high change-frequency cutoffs, over the
    /// warehouse graph's temporal churn. Changing a control re-runs the thin scan so
    /// `Mimir.Churn` re-folds with the new band.
    fn change_controls(&mut self, ui: &mut egui::Ui) {
        let mut apply = false;
        ui.horizontal_wrapped(|ui| {
            ui.label(
                egui::RichText::new("Frequency filter:")
                    .strong()
                    .color(TEAL),
            );
            for (id, lbl) in [
                ("high", "high-pass · volatile hotspots"),
                ("low", "low-pass · stable core"),
                ("band", "band-pass"),
            ] {
                if ui.selectable_label(self.churn_band == id, lbl).clicked() {
                    self.churn_band = id.to_string();
                    apply = true;
                }
            }
        });
        ui.horizontal(|ui| {
            ui.label("low cutoff");
            // NO "Apply filter" button (owner 2026-08-25: SCAN is not a user action).
            // Changing a cutoff IS the new question — the pane re-asks it directly
            // rather than making the operator confirm that they meant what they just
            // typed. `.changed()` fires on the edit, not on every frame the widget is
            // on screen, so an untouched control costs nothing.
            if ui
                .add(egui::DragValue::new(&mut self.churn_cutoff_low))
                .changed()
            {
                apply = true;
            }
            ui.label("high cutoff");
            if ui
                .add(egui::DragValue::new(&mut self.churn_cutoff_high))
                .changed()
            {
                apply = true;
            }
            ui.label(
                egui::RichText::new("commits_90d thresholds; hotspot rank = churn × complexity")
                    .small(),
            );
        });
        if apply {
            self.scan();
        }
    }
}

/// Why a LOCAL lens fold carries no test-reachability: it folds warehouse tables, and
/// the code graph is not a table — nornir's `Mimir.*` lenses answer it from the room.
const LOCAL_FOLD_HAS_NO_GRAPH: &str =
    "the local lens fold reads warehouse tables only: the code graph (what a test reaches) is asked of nornir's `Mimir.*` lenses, not folded here";

fn chip(ui: &mut egui::Ui, text: &str, color: egui::Color32) {
    ui.label(
        egui::RichText::new(format!(" {text} "))
            .small()
            .strong()
            .color(egui::Color32::WHITE)
            .background_color(color),
    );
}

const RED: egui::Color32 = egui::Color32::from_rgb(0xc6, 0x28, 0x28);
const TEAL: egui::Color32 = egui::Color32::from_rgb(0x00, 0x69, 0x5c);
const AMBER: egui::Color32 = egui::Color32::from_rgb(0x8d, 0x6e, 0x00);

fn kb(b: u64) -> String {
    if b >= 1_048_576 {
        format!("{:.1} MB", b as f64 / 1_048_576.0)
    } else {
        format!("{:.1} KB", b as f64 / 1024.0)
    }
}

/// The MISSING-FEED callout every LOCAL-fold view leads with (the fold's own note).
fn evidence(ui: &mut egui::Ui, note: &str) {
    let bad = note.contains("MISSING FEEDS");
    ui.horizontal_wrapped(|ui| {
        if bad {
            chip(ui, "MISSING FEED", RED);
        }
        ui.label(egui::RichText::new(note).small());
    });
}

/// **What one `Mimir.*` answer says about itself** — `lens_meta.state`, as nornir's
/// Mímir v2 stamps every lens envelope. The pane reads THIS before it reads a single
/// finding: a ranked list means something only in the first three states.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LensAnswer {
    /// `complete` — computed, every input present.
    Complete,
    /// `partial` — computed with an input missing: every figure is a LOWER bound.
    Partial,
    /// `empty_fact` — computed, every input present, nothing found. A fact.
    EmptyFact,
    /// `empty_gap` — empty BECAUSE an input is missing. Not a verdict.
    EmptyGap,
    /// `not_wired` — the lens waits for a producer (named in the note).
    NotWired,
    /// The lens refused by state: `not_consumed`, `lake_unavailable`,
    /// `driver_not_available`, `no_room`, `not_linked`, `invalid_argument`, `internal`.
    Refused,
    /// No `lens_meta.state`, or a word this client does not know — the answer says
    /// nothing about itself, so nothing in it is read as a verdict.
    Unstated,
    /// `pending` (asked, nothing back yet), `queued` / `running` (a jera job on the
    /// server): the answer is still to come. Not a verdict, and holds no other lens.
    Waiting,
    /// `error` — this lens's call failed. Its own error; the others are unaffected.
    Failed,
}

impl LensAnswer {
    /// Read the state off a lens envelope.
    pub fn of(v: &serde_json::Value) -> Self {
        match lens_state_word(v) {
            "complete" => Self::Complete,
            "partial" => Self::Partial,
            "empty_fact" => Self::EmptyFact,
            "empty_gap" => Self::EmptyGap,
            "not_wired" => Self::NotWired,
            "not_consumed"
            | "lake_unavailable"
            | "driver_not_available"
            | "no_room"
            | "not_linked"
            | "invalid_argument"
            | "internal" => Self::Refused,
            "pending" | "queued" | "running" => Self::Waiting,
            "error" => Self::Failed,
            _ => Self::Unstated,
        }
    }

    /// Did the lens COMPUTE — is its ranked list (also an empty one) a measurement?
    pub fn is_measured(self) -> bool {
        matches!(self, Self::Complete | Self::Partial | Self::EmptyFact)
    }
}

/// The raw `lens_meta.state` word (`""` when the envelope carries none).
pub fn lens_state_word(v: &serde_json::Value) -> &str {
    v.get("lens_meta")
        .and_then(|m| m.get("state"))
        .and_then(|x| x.as_str())
        .unwrap_or("")
}

/// The chip a thin view leads with for a state that is not `complete`, and the
/// sentence to print when the answer carries no evidence text of its own.
fn answer_chip(v: &serde_json::Value) -> Option<(String, egui::Color32, &'static str)> {
    let word = lens_state_word(v);
    match LensAnswer::of(v) {
        LensAnswer::Complete => None,
        LensAnswer::Partial => Some((
            "PARTIAL — LOWER BOUND".into(),
            AMBER,
            "computed with an input missing: every figure below is a lower bound",
        )),
        LensAnswer::EmptyFact => Some((
            "NOTHING FOUND".into(),
            TEAL,
            "computed with every input present: nothing found (a fact, not a gap)",
        )),
        LensAnswer::EmptyGap => Some((
            "EMPTY — INPUT MISSING".into(),
            RED,
            "empty because an input is missing: this is NOT a verdict",
        )),
        LensAnswer::NotWired => Some((
            "NOT WIRED".into(),
            RED,
            "this lens waits for a producer: a missing producer, not an absence of findings",
        )),
        LensAnswer::Refused => Some((
            word.to_uppercase().replace('_', " "),
            RED,
            "the lens refused: nothing was computed",
        )),
        LensAnswer::Waiting => Some((
            match word {
                "queued" => "QUEUED ON JERA".into(),
                "running" => "RUNNING ON JERA".into(),
                _ => "ASKING".into(),
            },
            TEAL,
            "still being computed on the server: it lands on its own and holds no other lens",
        )),
        LensAnswer::Failed => Some((
            "ERROR".into(),
            RED,
            "this lens's call failed: the other lenses are not affected",
        )),
        LensAnswer::Unstated => Some((
            if word.is_empty() {
                "NO STATE".into()
            } else {
                format!("UNKNOWN STATE `{word}`")
            },
            RED,
            "the server did not say what this answer rests on (no Mímir v2 `lens_meta.state`): \
             nothing below is read as a verdict",
        )),
    }
}

/// The callout every THIN view leads with: the answer's state as a chip, then the
/// answer's OWN evidence text (`evidence_note`) verbatim — or, when it sent none, the
/// sentence of its state. An empty ranked list is never drawn without one of the two.
fn thin_evidence(ui: &mut egui::Ui, v: &serde_json::Value) {
    let note = thin_note(v);
    let chipped = answer_chip(v);
    ui.horizontal_wrapped(|ui| {
        if LensAnswer::of(v) == LensAnswer::Waiting {
            ui.spinner();
        }
        if let Some((label, color, _)) = &chipped {
            chip(ui, label, *color);
        }
        let text = match (&chipped, note.is_empty()) {
            (Some((_, _, fallback)), true) => (*fallback).to_string(),
            _ => note,
        };
        ui.label(egui::RichText::new(text).small());
    });
}

/// A whole-number field off a lens Value (0 when absent) — the thin status counters.
fn ju64(v: &serde_json::Value, key: &str) -> u64 {
    v.get(key).and_then(|x| x.as_u64()).unwrap_or(0)
}

/// The one-line status for a THIN reading, built from the RPC Values (no numbers of
/// the pane's own).
/// The lenses of a thin reading, by the name nornir dispatches them under.
fn remote_lenses(r: &RemoteReports) -> Vec<(&'static str, &serde_json::Value)> {
    THIN_LENSES
        .iter()
        .filter_map(|(lens, _)| Some((*lens, r.slot(lens)?)))
        .collect()
}

/// **The answers header** — how many of the lenses COMPUTED, and for every one that
/// did not, which state it is in, by name:
/// `lens 9/12 answered · 7 complete · 1 partial (lower bound) · 1 empty: nothing found ·
/// EMPTY, INPUT MISSING: generic_cost, binary_drift · NOT WIRED: similar_code ·
/// feeds missing: …`.
///
/// Read off the `lens_meta.state` every `Mimir.*` answer carries since Mímir v2 (a
/// lens is computed on the request; there is no chain, no snapshot and no cache to
/// report), and off the `feeds` each fold recorded. An answer WITHOUT a state — a
/// server that is not Mímir v2 — is named as such and counted as not answered: its
/// list is not a verdict this client can vouch for.
pub fn answer_status(r: &RemoteReports) -> String {
    let lenses = remote_lenses(r);
    let (mut complete, mut partial, mut empty_fact) = (0usize, 0usize, 0usize);
    let mut empty_gap: Vec<&str> = Vec::new();
    let mut not_wired: Vec<&str> = Vec::new();
    let mut refused: Vec<String> = Vec::new();
    let mut unstated: Vec<String> = Vec::new();
    let mut waiting: Vec<String> = Vec::new();
    let mut failed: Vec<&str> = Vec::new();
    let mut missing: std::collections::BTreeSet<String> = Default::default();
    let total = lenses.len();

    for (name, v) in lenses {
        match LensAnswer::of(v) {
            LensAnswer::Complete => complete += 1,
            LensAnswer::Partial => partial += 1,
            LensAnswer::EmptyFact => empty_fact += 1,
            LensAnswer::EmptyGap => empty_gap.push(name),
            LensAnswer::NotWired => not_wired.push(name),
            LensAnswer::Refused => refused.push(format!("{name} [{}]", lens_state_word(v))),
            LensAnswer::Waiting => waiting.push(format!("{name} [{}]", lens_state_word(v))),
            LensAnswer::Failed => failed.push(name),
            LensAnswer::Unstated => unstated.push(match lens_state_word(v) {
                "" => name.to_string(),
                word => format!("{name} [`{word}`]"),
            }),
        }
        // The MISSING FEEDS come from each lens's own feed record — a missing
        // producer is a fact about the warehouse, whatever the lens made of it.
        if let Some(feeds) = v.get("feeds").and_then(|f| f.as_array()) {
            for f in feeds {
                if f.get("missing").and_then(|m| m.as_bool()) == Some(true) {
                    if let Some(t) = f.get("table").and_then(|t| t.as_str()) {
                        missing.insert(t.to_string());
                    }
                }
            }
        }
    }

    let mut parts: Vec<String> = vec![format!(
        "lens {}/{} answered",
        complete + partial + empty_fact,
        total
    )];
    if complete > 0 {
        parts.push(format!("{complete} complete"));
    }
    if partial > 0 {
        parts.push(format!("{partial} partial (lower bound)"));
    }
    if empty_fact > 0 {
        parts.push(format!("{empty_fact} empty: nothing found"));
    }
    if !empty_gap.is_empty() {
        parts.push(format!("EMPTY, INPUT MISSING: {}", empty_gap.join(", ")));
    }
    if !not_wired.is_empty() {
        parts.push(format!("NOT WIRED: {}", not_wired.join(", ")));
    }
    if !refused.is_empty() {
        parts.push(format!("REFUSED: {}", refused.join(", ")));
    }
    if !waiting.is_empty() {
        parts.push(format!("WAITING: {}", waiting.join(", ")));
    }
    if !failed.is_empty() {
        parts.push(format!("ERROR: {}", failed.join(", ")));
    }
    if !unstated.is_empty() {
        parts.push(format!(
            "NO STATE (not a Mímir v2 answer): {}",
            unstated.join(", ")
        ));
    }
    parts.push(if missing.is_empty() {
        "feeds missing: none".to_string()
    } else {
        format!(
            "feeds missing: {}",
            missing.iter().cloned().collect::<Vec<_>>().join(", ")
        )
    });
    parts.join(" · ")
}

/// The three headline counts — each only where its lens COMPUTED. A lens that did not
/// says its state instead of a number: `0 zombie feature(s)` from a lens that is not
/// wired would be a zero nobody measured.
fn thin_status(r: &RemoteReports) -> String {
    let count = |v: &serde_json::Value, n: u64, what: &str| -> String {
        if LensAnswer::of(v).is_measured() {
            format!("{n} {what}")
        } else {
            let word = lens_state_word(v);
            format!(
                "{what}: not measured [{}]",
                if word.is_empty() { "no state" } else { word }
            )
        }
    };
    // `zombies` is the TOTAL the lens found; `worst` is the page it sent.
    let shown = thin_arr(&r.zombie, "worst").len() as u64;
    let zombies = r
        .zombie
        .get("zombies")
        .and_then(|z| z.as_u64())
        .unwrap_or(shown);
    let zombie_what = if zombies > shown {
        format!("zombie feature(s) ({shown} shown)")
    } else {
        "zombie feature(s)".to_string()
    };
    format!(
        "{} · {} · {} (thin — folded on nornir via Mimir.*)",
        count(&r.bloat, ju64(&r.bloat, "artifacts"), "artifact(s) taxed"),
        count(&r.zombie, zombies, &zombie_what),
        count(
            &r.risk,
            ju64(&r.risk, "files_scored"),
            "file(s) scored for risk"
        ),
    )
}

// ── the THIN renderer ────────────────────────────────────────────────────────────
// One renderer over the JSON the `Mimir.*` RPCs returned. It upholds BOTH
// prohibitions off the wire shape: every view LEADS with the `evidence_note` (so an
// empty ranked list never draws like an unfed table), and risk prints the three
// inputs (callers/churn/coverage) beside the score — never a composite number alone.

fn thin_note(v: &serde_json::Value) -> String {
    v.get("evidence_note")
        .and_then(|n| n.as_str())
        .unwrap_or("")
        .to_string()
}
fn thin_arr<'a>(v: &'a serde_json::Value, key: &str) -> &'a [serde_json::Value] {
    v.get(key)
        .and_then(|a| a.as_array())
        .map(|a| a.as_slice())
        .unwrap_or(&[])
}
fn thin_s(v: &serde_json::Value, k: &str) -> String {
    match v.get(k) {
        Some(serde_json::Value::String(s)) => s.clone(),
        Some(other) => other.to_string(),
        None => String::new(),
    }
}
fn thin_n(v: &serde_json::Value, k: &str) -> f64 {
    v.get(k).and_then(|x| x.as_f64()).unwrap_or(0.0)
}

/// The enclosing item's module path a source file implies — the handle a subclone
/// finding (file+lines, no moniker) asks `Knowledge.Source` by. The lead file is
/// checkout-relative (`crates/x/src/a/b.rs`); everything after its LAST `src/`
/// maps `/`→`::` with `.rs` and a trailing `/mod` dropped, so `src/util/esc.rs` →
/// `util::esc` — a path SUFFIX the room resolves. `None` for a crate root
/// (`src/lib.rs`, `src/main.rs`): there is no sub-path to name.
fn enclosing_path_of(file: &str) -> Option<String> {
    let norm = file.replace('\\', "/");
    let rest: &str = match norm.rfind("/src/") {
        Some(i) => &norm[i + 5..],
        None => norm.strip_prefix("src/")?,
    };
    let rest = rest.strip_suffix(".rs")?;
    let rest = rest.strip_suffix("/mod").unwrap_or(rest);
    if rest.is_empty() || rest == "lib" || rest == "main" {
        return None;
    }
    Some(rest.replace('/', "::"))
}

/// What a frame of the thin view was clicked on (applied after the render, which
/// only borrows the reading).
#[derive(Default)]
struct Clicks {
    /// A semantic-clone member: (finding, member).
    pick: Option<(usize, usize)>,
    /// A repo the subclone refusal named.
    sub_repo: Option<String>,
    /// A subclone finding.
    sub_find: Option<usize>,
    /// A repo the group_twins refusal named.
    gt_repo: Option<String>,
    /// A twin pair: (lens, row of [`twin_rows`]).
    pair: Option<(&'static str, usize)>,
}

/// Render ONE lens's thin reading from the JSON its `Mimir.*` RPC returned.
fn render_thin(ui: &mut egui::Ui, view: View, r: &RemoteReports, clicks: &mut Clicks) {
    ui.horizontal_wrapped(|ui| {
        chip(ui, "thin", TEAL);
        ui.label(
            egui::RichText::new(
                "folded on nornir-server via Mimir.* — the pane renders the server's \
                 finding-set verbatim (it never folds a warehouse it does not own)",
            )
            .small(),
        );
    });
    match view {
        View::Bloat => {
            let v = &r.bloat;
            thin_evidence(ui, v);
            egui::ScrollArea::vertical()
                .max_height(320.0)
                .id_salt("thin-bloat")
                .show(ui, |ui| {
                    for a in thin_arr(v, "worst_artifacts") {
                        ui.label(
                            egui::RichText::new(format!(
                                "{} / {}   tax {} ({:.1}%)   actionable {}",
                                thin_s(a, "repo"),
                                thin_s(a, "artifact"),
                                kb(thin_n(a, "tax_bytes") as u64),
                                thin_n(a, "tax_pct"),
                                kb(thin_n(a, "actionable_tax_bytes") as u64),
                            ))
                            .strong()
                            .monospace(),
                        );
                        for g in thin_arr(a, "top_named").iter().take(6) {
                            ui.monospace(format!(
                                "    {:>6}x  {:>9}  [{}] {}",
                                thin_n(g, "instantiations") as u64,
                                kb(thin_n(g, "tax_bytes") as u64),
                                thin_s(g, "crate"),
                                thin_s(g, "symbol"),
                            ));
                        }
                    }
                });
        }
        View::Zombie => {
            let v = &r.zombie;
            thin_evidence(ui, v);
            egui::ScrollArea::vertical()
                .max_height(320.0)
                .id_salt("thin-zombie")
                .show(ui, |ui| {
                    for f in thin_arr(v, "worst") {
                        ui.monospace(format!(
                            "{:<14} {:<24} items {:>4}  calls {:>5}",
                            thin_s(f, "repo"),
                            thin_s(f, "feature"),
                            thin_n(f, "gated_items") as u64,
                            thin_n(f, "inbound_calls") as u64,
                        ));
                    }
                });
        }
        View::Risk => {
            let v = &r.risk;
            if let Some(a) = v.get("model_attribution").and_then(|x| x.as_str()) {
                ui.label(egui::RichText::new(a).small().italics());
            }
            thin_evidence(ui, v);
            ui.label(
                egui::RichText::new(
                    "The score is a SORT KEY, not a verdict — the three inputs are the columns.",
                )
                .small()
                .strong(),
            );
            ui.label(
                egui::RichText::new(format!(
                    "{:<10} {:<40} {:>8} {:>8} {:>6} {:>10}",
                    "REPO", "FILE", "CALLERS", "CHURN90", "COV", "RISK"
                ))
                .small()
                .monospace()
                .strong(),
            );
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-risk")
                .show(ui, |ui| {
                    for u in thin_arr(v, "worst") {
                        let file = thin_s(u, "file");
                        let file = if file.len() > 40 {
                            file[file.len() - 40..].to_string()
                        } else {
                            file
                        };
                        ui.label(
                            egui::RichText::new(format!(
                                "{:<10} {:<40} {:>8} {:>8} {:>5.0}% {:>10.0}",
                                thin_s(u, "repo"),
                                file,
                                thin_n(u, "downstream_callers") as u64,
                                thin_n(u, "churn_90d") as u64,
                                thin_n(u, "coverage_frac") * 100.0,
                                thin_n(u, "risk"),
                            ))
                            .monospace()
                            .small(),
                        );
                    }
                });
        }
        View::WeakTests => {
            let v = &r.weak_tests;
            thin_evidence(ui, v);
            egui::ScrollArea::vertical()
                .max_height(320.0)
                .id_salt("thin-weak")
                .show(ui, |ui| {
                    for t in thin_arr(v, "worst") {
                        ui.monospace(format!(
                            "{:<12} {:<32} {:<12} runs {:>3}  versions {:>2}  churn90 {:>4}",
                            thin_s(t, "repo"),
                            thin_s(t, "test_name"),
                            thin_s(t, "verdict"),
                            thin_n(t, "runs") as u64,
                            thin_n(t, "versions") as u64,
                            thin_n(t, "churn_commits_90d") as u64,
                        ));
                    }
                });
        }
        View::BenchGaps => {
            let v = &r.bench_gaps;
            thin_evidence(ui, v);
            egui::ScrollArea::vertical()
                .max_height(320.0)
                .id_salt("thin-bench")
                .show(ui, |ui| {
                    for rp in thin_arr(v, "per_repo") {
                        ui.horizontal(|ui| {
                            if rp
                                .get("never_benched")
                                .and_then(|b| b.as_bool())
                                .unwrap_or(false)
                            {
                                chip(ui, "NEVER BENCHED", RED);
                            }
                            ui.monospace(format!(
                                "{:<12} {:>3} unbenched / {:>3} hot paths · {} run(s)",
                                thin_s(rp, "repo"),
                                thin_n(rp, "unbenched") as u64,
                                thin_n(rp, "hot_paths") as u64,
                                thin_n(rp, "bench_runs") as u64,
                            ));
                        });
                        for h in thin_arr(rp, "worst").iter().take(4) {
                            ui.label(
                                egui::RichText::new(format!(
                                    "    {:<28} fan-in {:>4}  churn90 {:>4}  cov {:>4.0}%",
                                    thin_s(h, "module"),
                                    thin_n(h, "fan_in") as u64,
                                    thin_n(h, "commits_90d") as u64,
                                    thin_n(h, "test_coverage") * 100.0,
                                ))
                                .monospace()
                                .small(),
                            );
                        }
                    }
                });
        }
        View::SimilarCode => {
            let v = &r.similar_code;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.SimilarCode — the relocated dupes query \
                         (embeddings × symbol_facts × scip_occurrences).",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            render_pair_rows(ui, "similar_code", v, clicks);
        }
        View::SemanticClones => {
            let v = &r.semantic_clones;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.SemanticClones — SimilarCode's AND-gate INVERTED: high \
                         cosine, LOW structural agreement, i.e. independent \
                         RE-implementations. The two lenses PARTITION the high-cosine \
                         pair space; a pair is one's finding or the other's, never both.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            // The ranked array is `findings` — NOT SimilarCode's `top_clusters`. Read
            // off `SemanticCloneReport::state_json`, because the two folds are twins in
            // purpose and NOT in wire shape, and guessing the sibling's key is exactly
            // the mistake this comment exists to stop.
            egui::ScrollArea::vertical()
                .max_height(280.0)
                .id_salt("thin-semclones")
                .show(ui, |ui| {
                    let mut pairs_seen = 0usize;
                    for (ci, c) in thin_arr(v, "findings").iter().enumerate() {
                        let this_pair = (thin_arr(c, "members").len() >= 2).then(|| {
                            pairs_seen += 1;
                            pairs_seen - 1
                        });
                        ui.horizontal_wrapped(|ui| {
                            chip(ui, &format!("score {:.2}", thin_n(c, "score")), TEAL);
                            if c.get("cross_scope").and_then(|b| b.as_bool()).unwrap_or(false) {
                                chip(ui, "cross-repo", AMBER);
                            }
                            ui.monospace(format!(
                                "{:<40} ×{}  cos {:.2}  struct {:.2}",
                                thin_s(c, "canonical"),
                                thin_n(c, "size") as u64,
                                thin_n(c, "avg_cosine"),
                                thin_n(c, "avg_structural"),
                            ));
                            if let Some(k) = this_pair {
                                if ui.link(OPEN_BOTH).clicked() {
                                    clicks.pair = Some(("semantic_clones", k));
                                }
                            }
                        });
                        for (mi, m) in thin_arr(c, "members").iter().enumerate() {
                            ui.horizontal(|ui| {
                                ui.add_space(14.0);
                                // THE DOOR: a member names its source, and clicking it
                                // reads the code (`Knowledge.Source` on the member's
                                // moniker) — the owner, 2026-10-18: "make sure if click
                                // we see source code or at least its url".
                                let row = format!(
                                    "{:<10} {}:{}  {}::{}",
                                    thin_s(m, "repo"),
                                    thin_s(m, "file"),
                                    thin_n(m, "line") as u64,
                                    thin_s(m, "module"),
                                    thin_s(m, "ident"),
                                );
                                if ui
                                    .link(egui::RichText::new(row).monospace().small())
                                    .clicked()
                                {
                                    clicks.pick = Some((ci, mi));
                                }
                            });
                        }
                    }
                });
        }
        View::Villa => {
            let v = &r.villa;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.Villa — IMPURE / shell-out heresy over the CPG. Every \
                         std::process::Command, raw exec/system escape and process::exit \
                         lights up; a site in a known offender names the tool being replaced.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            ui.horizontal_wrapped(|ui| {
                for (k, n) in v
                    .get("by_kind")
                    .and_then(|b| b.as_object())
                    .into_iter()
                    .flatten()
                {
                    let severe = k.as_str() == "external-tool" || k.as_str() == "shell";
                    chip(ui, &format!("{k} {n}"), if severe { RED } else { AMBER });
                }
            });
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-villa")
                .show(ui, |ui| {
                    for h in thin_arr(v, "findings") {
                        ui.horizontal(|ui| {
                            let kind = thin_s(h, "kind");
                            chip(
                                ui,
                                &kind,
                                if thin_s(h, "severity") == "high" {
                                    RED
                                } else {
                                    AMBER
                                },
                            );
                            let tool = h.get("known_tool").and_then(|t| t.as_str()).unwrap_or("?");
                            ui.monospace(format!(
                                "{:<10} {:<40} {:<22} [{}]",
                                thin_s(h, "repo"),
                                thin_s(h, "loc"),
                                thin_s(h, "symbol"),
                                tool,
                            ));
                        });
                    }
                });
        }
        View::BinaryDrift => {
            let v = &r.binary_drift;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.BinaryDrift — successive Build Thing artifacts of one variant, compared \
                         symbol by symbol. A change another commit or toolchain explains is listed for \
                         what it is; other bytes from the SAME commits and toolchain (or an added reach to \
                         process / exec / network) are unexplained: a non-reproducible build or a \
                         build-time injection.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            // The ATTESTATION summary — lead with the yes/no verdict + unexplained count.
            let attested = v.get("attested").and_then(|b| b.as_bool()).unwrap_or(false);
            let has_symbols = v
                .get("first_party_symbols")
                .and_then(|n| n.as_u64())
                .unwrap_or(0)
                > 0;
            let (color, verdict) = if !has_symbols {
                (AMBER, "drift explained: UNKNOWN")
            } else if attested {
                (TEAL, "drift explained: YES")
            } else {
                (RED, "drift explained: NO")
            };
            ui.horizontal_wrapped(|ui| {
                chip(ui, verdict, color);
                if let Some(a) = v.get("attestation").and_then(|x| x.as_str()) {
                    ui.label(egui::RichText::new(a).small().italics());
                }
            });
            ui.horizontal_wrapped(|ui| {
                chip(
                    ui,
                    &format!(
                        "{} first-party symbols",
                        thin_n(v, "first_party_symbols") as i64
                    ),
                    TEAL,
                );
                chip(
                    ui,
                    &format!("{} explained", thin_n(v, "explained") as i64),
                    TEAL,
                );
                chip(
                    ui,
                    &format!("{} unexplained", thin_n(v, "unexplained") as i64),
                    if thin_n(v, "unexplained") > 0.0 {
                        RED
                    } else {
                        TEAL
                    },
                );
                for (k, n) in v
                    .get("by_kind")
                    .and_then(|b| b.as_object())
                    .into_iter()
                    .flatten()
                {
                    let severe = k.as_str() == "unexplained-edge" || k.as_str() == "toolchain";
                    chip(ui, &format!("{k} {n}"), if severe { RED } else { AMBER });
                }
            });
            egui::ScrollArea::vertical()
                .max_height(280.0)
                .id_salt("thin-binary-drift")
                .show(ui, |ui| {
                    for f in thin_arr(v, "findings") {
                        ui.horizontal(|ui| {
                            let kind = thin_s(f, "kind");
                            chip(
                                ui,
                                &kind,
                                if thin_s(f, "severity") == "high" {
                                    RED
                                } else {
                                    AMBER
                                },
                            );
                            let callee = f.get("callee").and_then(|c| c.as_str()).unwrap_or("");
                            // a change between two builds carries its byte delta and what explains it
                            let delta = match f.get("delta_bytes").and_then(|d| d.as_i64()) {
                                Some(d) => format!(
                                    "{d:+} B ({})",
                                    f.get("explained_by").and_then(|e| e.as_str()).unwrap_or("?")
                                ),
                                None => String::new(),
                            };
                            ui.monospace(format!(
                                "{:<10} {:<44} {} {}",
                                thin_s(f, "repo"),
                                thin_s(f, "demangled"),
                                delta,
                                if callee.is_empty() {
                                    String::new()
                                } else {
                                    format!("→ {callee}")
                                },
                            ));
                        });
                    }
                });
            // Honest boundary — say what this CANNOT catch, verbatim from the engine.
            if let Some(cc) = v.get("cant_catch").and_then(|c| c.as_array()) {
                if !cc.is_empty() {
                    ui.add_space(4.0);
                    ui.label(
                        egui::RichText::new("Cannot catch (heuristic boundary):")
                            .small()
                            .strong(),
                    );
                    for item in cc {
                        if let Some(s) = item.as_str() {
                            ui.label(egui::RichText::new(format!("• {s}")).small().weak());
                        }
                    }
                }
            }
        }
        View::Quality => {
            let card = brokkr_scorecard(r);
            ui.horizontal_wrapped(|ui| {
                let overall = thin_s(&card, "overall");
                // No score while a lens did not answer: the chip says how many, never 0/100.
                let score = match card.get("score").and_then(|x| x.as_i64()) {
                    Some(n) => format!("{n}/100"),
                    None => format!(
                        "not scored: {} of {} lens(es) did not answer",
                        thin_n(&card, "unmeasured") as u64,
                        thin_arr(&card, "factors").len()
                    ),
                };
                chip(
                    ui,
                    &format!("Brokkr: {overall}  ({score})"),
                    match overall.as_str() {
                        "heresy" => RED,
                        "warn" | "unknown" => AMBER,
                        _ => TEAL,
                    },
                );
                ui.label(
                    egui::RichText::new(
                        "Software AS A GRAPH, graded by its lenses — one Brokkr criterion per lens. \
                         The score is a SORT KEY; the factors below are the columns.",
                    )
                    .small(),
                );
            });
            if let Some(a) = card.get("attribution").and_then(|x| x.as_str()) {
                ui.label(egui::RichText::new(a).small().italics());
            }
            ui.separator();
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-quality")
                .show(ui, |ui| {
                    for f in thin_arr(&card, "factors") {
                        ui.horizontal(|ui| {
                            let st = thin_s(f, "status");
                            chip(
                                ui,
                                &st,
                                match st.as_str() {
                                    "fail" => RED,
                                    "warn" => AMBER,
                                    NOT_MEASURED => RED,
                                    _ => TEAL,
                                },
                            );
                            // An unmeasured factor has NO count: it shows its lens's state.
                            let count = match f.get("count").and_then(|c| c.as_u64()) {
                                Some(n) => format!("{n:>4}"),
                                None => {
                                    let state = thin_s(f, "state");
                                    format!(
                                        "   — [{}]",
                                        if state.is_empty() { "no state" } else { &state }
                                    )
                                }
                            };
                            ui.monospace(format!("{:<34} {count}", thin_s(f, "name")));
                        });
                        ui.label(
                            egui::RichText::new(format!("    {}", thin_s(f, "criterion"))).small(),
                        );
                    }
                });
        }
        View::Churn => {
            let v = &r.churn;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                let band = thin_s(v, "band");
                chip(ui, &format!("{band} filter"), AMBER);
                ui.label(
                    egui::RichText::new(
                        "Mimir.Churn — a frequency filter over the per-node change-series \
                         (git_heat_facts). high-pass = volatile hotspots (churn × complexity), \
                         low-pass = stable core (a stable node that fired is flagged), band-pass \
                         = a target band. The hotspot is a SORT KEY; frequency and complexity \
                         are the columns.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            if let Some(a) = v.get("model_attribution").and_then(|x| x.as_str()) {
                ui.label(egui::RichText::new(a).small().italics());
            }
            ui.label(
                egui::RichText::new(format!(
                    "{:<10} {:<40} {:>6} {:>7} {:>6} {:>8}",
                    "REPO", "FILE", "FREQ", "TOTAL", "CX", "HOTSPOT"
                ))
                .small()
                .monospace()
                .strong(),
            );
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-churn")
                .show(ui, |ui| {
                    for n in thin_arr(v, "worst") {
                        let file = thin_s(n, "file");
                        let file = if file.len() > 40 {
                            file[file.len() - 40..].to_string()
                        } else {
                            file
                        };
                        ui.horizontal(|ui| {
                            if n.get("stable_but_changed")
                                .and_then(|b| b.as_bool())
                                .unwrap_or(false)
                            {
                                chip(ui, "STABLE→CHANGED", RED);
                            }
                            ui.label(
                                egui::RichText::new(format!(
                                    "{:<10} {:<40} {:>6} {:>7} {:>6} {:>8}",
                                    thin_s(n, "repo"),
                                    file,
                                    thin_n(n, "frequency") as u64,
                                    thin_n(n, "commits_total") as u64,
                                    thin_n(n, "complexity") as u64,
                                    thin_n(n, "hotspot") as u64,
                                ))
                                .monospace()
                                .small(),
                            );
                        });
                    }
                });
        }
        View::Anomaly => {
            let v = &r.anomaly;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.Anomaly — statistical outliers on the change-series: a stable \
                         node that suddenly churns, a change-rate spike, an anomalously broad \
                         change wave, and new-coupling candidates (a documented proxy). Each row \
                         carries baseline vs observed — the z-score is never a lone verdict.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            if let Some(a) = v.get("model_attribution").and_then(|x| x.as_str()) {
                ui.label(egui::RichText::new(a).small().italics());
            }
            let insuf = ju64(v, "insufficient_history");
            if insuf > 0 {
                ui.label(
                    egui::RichText::new(format!(
                        "{insuf} node(s) had too little history to judge — reported, not fabricated."
                    ))
                    .small()
                    .italics(),
                );
            }
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-anomaly")
                .show(ui, |ui| {
                    for a in thin_arr(v, "worst") {
                        let kind = thin_s(a, "kind");
                        ui.horizontal(|ui| {
                            chip(
                                ui,
                                &kind,
                                if kind == "stable-node-churn" || kind == "breadth-outlier" {
                                    RED
                                } else {
                                    AMBER
                                },
                            );
                            ui.monospace(format!(
                                "{:<40} baseline {:>6.1} → observed {:>6.0}  z {:>5.1}",
                                {
                                    let s = thin_s(a, "subject");
                                    if s.len() > 40 {
                                        s[s.len() - 40..].to_string()
                                    } else {
                                        s
                                    }
                                },
                                thin_n(a, "baseline"),
                                thin_n(a, "observed"),
                                thin_n(a, "z"),
                            ));
                        });
                    }
                });
        }
        View::WorldImpact => {
            let v = &r.world_impact;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.WorldImpact — changes ranked by how far they ripple: semver_diffs \
                         (the change) × call_edges (world callers) × dep_graph_edges (constellation \
                         blast radius). A breaking drop in a widely-depended symbol ranks first. \
                         The world-impact number is a SORT KEY; the two counts are the columns.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            ui.label(
                egui::RichText::new(format!(
                    "{:<26} {:>4} {:>4} {:>4} {:>7} {:>7} {:>8}",
                    "CRATE", "ADD", "RM", "CHG", "WORLD", "CONST", "IMPACT"
                ))
                .small()
                .monospace()
                .strong(),
            );
            egui::ScrollArea::vertical()
                .max_height(300.0)
                .id_salt("thin-world")
                .show(ui, |ui| {
                    for c in thin_arr(v, "worst") {
                        ui.horizontal(|ui| {
                            if c.get("breaking").and_then(|b| b.as_bool()).unwrap_or(false) {
                                chip(ui, "BREAKING", RED);
                            }
                            ui.label(
                                egui::RichText::new(format!(
                                    "{:<26} {:>4} {:>4} {:>4} {:>7} {:>7} {:>8}",
                                    thin_s(c, "crate_name"),
                                    thin_n(c, "added_items") as u64,
                                    thin_n(c, "removed_items") as u64,
                                    thin_n(c, "changed_items") as u64,
                                    thin_n(c, "world_caller_count") as u64,
                                    thin_n(c, "constellation_count") as u64,
                                    thin_n(c, "world_impact") as u64,
                                ))
                                .monospace()
                                .small(),
                            );
                        });
                    }
                });
        }
        View::Subclone => {
            let v = &r.subclone;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.Subclone — SUB-ITEM clone regions of ONE repo's source: shared \
                         runs SMALLER than the item they sit in, which no chunk-keyed tier can \
                         state. PER-REPO by construction: with no repo named the server answers \
                         a refusal that names the askable repos — a refusal, never a clean \
                         constellation.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            if v.get("scope_required").and_then(|b| b.as_bool()).unwrap_or(false) {
                // THE REFUSAL, usable: each repo it names is a CLICK that writes the
                // pane's subclone repo filter and re-asks the one lens.
                ui.label(egui::RichText::new("name a repo:").strong());
                ui.horizontal_wrapped(|ui| {
                    for repo in thin_arr(v, "repos_available") {
                        if let Some(name) = repo.as_str() {
                            if ui
                                .link(egui::RichText::new(name).monospace())
                                .clicked()
                            {
                                clicks.sub_repo = Some(name.to_string());
                            }
                        }
                    }
                });
            } else if v.get("scanned").and_then(|b| b.as_bool()).unwrap_or(false) {
                ui.label(
                    egui::RichText::new(format!(
                        "`{}` — {} file(s) scanned · {} region(s) (from {} group(s), {} before \
                         subsumption)",
                        thin_s(v, "repo"),
                        thin_n(v, "files_scanned") as u64,
                        thin_n(v, "findings_total") as u64,
                        thin_n(v, "groups") as u64,
                        thin_n(v, "groups_before_subsumption") as u64,
                    ))
                    .small(),
                );
                egui::ScrollArea::vertical()
                    .max_height(300.0)
                    .id_salt("thin-subclone")
                    .show(ui, |ui| {
                        // One row per finding — the answer's own fields (file+lines in
                        // `location`, sites, files, restated_by, damage), clickable
                        // through to the source like a semantic-clone member.
                        for (i, f) in thin_arr(v, "worst").iter().enumerate() {
                            ui.horizontal(|ui| {
                                if f.get("sub_item").and_then(|b| b.as_bool()).unwrap_or(false) {
                                    chip(ui, "sub-item", TEAL);
                                }
                                let row = format!(
                                    "{:<36} {:>3} lines × {} site(s) in {} file(s)  restated ×{}  damage {:>5.0}",
                                    thin_s(f, "location"),
                                    thin_n(f, "lines") as u64,
                                    thin_n(f, "sites") as u64,
                                    thin_n(f, "file_count") as u64,
                                    thin_n(f, "restated_by") as u64,
                                    thin_n(f, "damage"),
                                );
                                if ui
                                    .link(egui::RichText::new(row).monospace().small())
                                    .clicked()
                                {
                                    clicks.sub_find = Some(i);
                                }
                            });
                        }
                    });
            }
        }
        View::SemanticTwins => {
            let v = &r.semantic_twins;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.SemanticTwins — v1's semantic twins on v2: Type-4 TWINS (one idea \
                         written twice: close in meaning, far in structure and body) and Type-3 \
                         COPIES. One row per pair, both members; ⇆ opens both sources side by side.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            render_pair_rows(ui, "semantic_twins", v, clicks);
        }
        View::GroupTwins => {
            let v = &r.group_twins;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.GroupTwins — the same semantic GROUP of statements in two functions \
                         of ONE repo, found across interleaved lines. PER-REPO by construction: with \
                         no repo named the server answers a refusal that names the askable repos.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            if v.get("scope_required").and_then(|b| b.as_bool()).unwrap_or(false) {
                ui.label(egui::RichText::new("name a repo:").strong());
                ui.horizontal_wrapped(|ui| {
                    for repo in thin_arr(v, "repos_available") {
                        if let Some(name) = repo.as_str() {
                            if ui.link(egui::RichText::new(name).monospace()).clicked() {
                                clicks.gt_repo = Some(name.to_string());
                            }
                        }
                    }
                });
            } else {
                render_pair_rows(ui, "group_twins", v, clicks);
            }
        }
        View::ReimplementsShared => {
            let v = &r.reimplements_shared;
            ui.horizontal_wrapped(|ui| {
                chip(ui, "nornir RPC", TEAL);
                ui.label(
                    egui::RichText::new(
                        "Mimir.ReimplementsShared — crate A DEPENDS ON crate B and holds a copy or twin \
                         of a B function: replace A's with a call to B's (B is the library because A \
                         imports it). One row per function; ⇆ opens both sources side by side.",
                    )
                    .small(),
                );
            });
            thin_evidence(ui, v);
            render_pair_rows(ui, "reimplements_shared", v, clicks);
        }
        View::NameCollision => render_rows(ui, &r.name_collision, "thin-name-collision"),
        View::DataReach => render_rows(ui, &r.data_reach, "thin-data-reach"),
        View::Trust => render_rows(ui, &r.trust, "thin-trust"),
        View::SymbolIdentity => render_rows(ui, &r.symbol_identity, "thin-symbol-identity"),
        View::UndocApi => render_rows(ui, &r.undoc_api, "thin-undoc-api"),
        View::ReachableVuln => render_rows(ui, &r.reachable_vuln, "thin-reachable-vuln"),
        View::Belongs => {}
    }
}

// ── the twin pairs: every member a pointer to source ────────────────────────────

/// The lenses whose findings are PAIRS the pane draws with both members and
/// opens side by side ([`LensPane::pick_twin_pair`]).
pub const TWIN_LENSES: [&str; 5] = ["semantic_twins", "group_twins", "semantic_clones", "similar_code", "reimplements_shared"];

/// The pair row's click: open both members' source side by side.
pub const OPEN_BOTH: &str = "⇆ open both";

/// One member of a pair, as the lens's answer names it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TwinSide {
    pub repo: String,
    /// The function: its room path (`korp::esc::escape`), or the span's ident.
    pub function: String,
    pub file: String,
    pub line: u64,
    pub end_line: u64,
    /// The room's id for it when the answer carries one the room resolves.
    pub moniker: Option<String>,
    /// A group twin's statement lines (`2-3,5-7,9`); `""` for a whole function.
    pub group_lines: String,
}

impl TwinSide {
    /// `file:line` — where the member starts.
    pub fn at(&self) -> String {
        format!("{}:{}", self.file, self.line)
    }
    /// What `Knowledge.Source` is asked for: by moniker when the answer has one,
    /// else by the function's path (scoped to the member's repo by the caller).
    fn node(&self) -> crate::graph_source::NodeRef {
        crate::graph_source::NodeRef {
            label: self.function.clone(),
            moniker: self.moniker.clone(),
            kind: crate::graph_source::NodeKind::Symbol,
        }
    }
    fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "repo": self.repo, "function": self.function, "file": self.file,
            "line": self.line, "end_line": self.end_line, "at": self.at(),
            "moniker": self.moniker, "group_lines": self.group_lines,
        })
    }
}

/// One pair finding: its kind, its similarity, the lens's own `why`, both members.
#[derive(Clone, Debug, PartialEq)]
pub struct TwinRow {
    pub kind: String,
    pub score: String,
    pub why: String,
    pub a: TwinSide,
    pub b: TwinSide,
}

impl TwinRow {
    fn to_json(&self) -> serde_json::Value {
        serde_json::json!({ "kind": self.kind, "score": self.score, "why": self.why, "a": self.a.to_json(), "b": self.b.to_json() })
    }
}

fn ju(v: &serde_json::Value, k: &str) -> u64 {
    v.get(k).and_then(serde_json::Value::as_u64).unwrap_or(0)
}
fn js<'a>(v: &'a serde_json::Value, k: &str) -> &'a str {
    v.get(k).and_then(serde_json::Value::as_str).unwrap_or("")
}

/// **The pair rows of a twins-shaped answer** — read off each lens's wire shape
/// (nornir `room_feed::clones::semantic_twins` / `group_twins::envelope` /
/// `SemanticCloneReport` / `DuplicateCluster`), never guessed across lenses. A
/// finding with fewer than two members is no pair and is not drawn as one.
pub fn twin_rows(lens: &str, v: &serde_json::Value) -> Vec<TwinRow> {
    let side_placed = |m: &serde_json::Value| TwinSide {
        repo: js(m, "repo").to_string(),
        function: if js(m, "path").is_empty() { js(m, "name").to_string() } else { js(m, "path").to_string() },
        file: js(m, "file").to_string(),
        line: ju(m, "line"),
        end_line: ju(m, "end_line"),
        // the answer's `id` is the STABLE moniker (version `*`), which the room's
        // exact lookup does not resolve: ask by path in the member's repo instead
        moniker: None,
        group_lines: String::new(),
    };
    match lens {
        "semantic_twins" => thin_arr(v, "worst")
            .iter()
            .filter_map(|f| {
                let m = thin_arr(f, "members");
                (m.len() >= 2).then(|| TwinRow {
                    kind: js(f, "kind").to_string(),
                    score: format!(
                        "Type-{} · cos {:.3} · struct {:.2}{}",
                        ju(f, "clone_type"),
                        thin_n(f, "cosine"),
                        thin_n(f, "structural"),
                        f.get("body_similarity").and_then(serde_json::Value::as_f64).map(|b| format!(" · body {b:.2}")).unwrap_or_default()
                    ),
                    why: js(f, "why").to_string(),
                    a: side_placed(&m[0]),
                    b: side_placed(&m[1]),
                })
            })
            .collect(),
        "group_twins" => thin_arr(v, "worst")
            .iter()
            .filter(|f| f.get("a").is_some() && f.get("b").is_some())
            .map(|f| {
                let side = |m: &serde_json::Value| TwinSide {
                    repo: js(m, "repo").to_string(),
                    function: js(m, "ident").to_string(),
                    file: js(m, "file").to_string(),
                    line: ju(m, "line"),
                    end_line: ju(m, "end_line"),
                    moniker: None,
                    group_lines: js(m, "group_lines").to_string(),
                };
                TwinRow {
                    kind: "group twin".to_string(),
                    score: if f.get("exact").and_then(serde_json::Value::as_bool).unwrap_or(false) {
                        format!("exact group body · {} shared k-gram(s), {} rare", ju(f, "shared_kgrams"), ju(f, "rare_shared"))
                    } else {
                        format!("cos {:.3} · {} shared k-gram(s), {} rare", thin_n(f, "cosine"), ju(f, "shared_kgrams"), ju(f, "rare_shared"))
                    },
                    why: js(f, "why").to_string(),
                    a: side(&f["a"]),
                    b: side(&f["b"]),
                }
            })
            .collect(),
        "semantic_clones" => thin_arr(v, "findings")
            .iter()
            .filter_map(|c| {
                let m = thin_arr(c, "members");
                let side = |m: &serde_json::Value| TwinSide {
                    repo: js(m, "repo").to_string(),
                    function: format!("{}::{}", js(m, "module"), js(m, "ident")),
                    file: js(m, "file").to_string(),
                    line: ju(m, "line"),
                    end_line: ju(m, "line"),
                    moniker: m.get("id").and_then(serde_json::Value::as_str).map(str::to_string),
                    group_lines: String::new(),
                };
                (m.len() >= 2).then(|| TwinRow {
                    kind: "re-implementation".to_string(),
                    score: format!("score {:.2} · cos {:.2} · struct {:.2}", thin_n(c, "score"), thin_n(c, "avg_cosine"), thin_n(c, "avg_structural")),
                    why: format!("{} ×{}", js(c, "canonical"), ju(c, "size")),
                    a: side(&m[0]),
                    b: side(&m[1]),
                })
            })
            .collect(),
        // a = the function to replace, b = the library's one to call
        "reimplements_shared" => thin_arr(v, "worst")
            .iter()
            .filter(|f| f.get("replace").is_some() && f.get("use_instead").is_some())
            .map(|f| TwinRow {
                kind: format!("call the library ({})", js(f, "fit").replace('_', "-")),
                score: format!("sim {:.2} · {} line(s) · {} caller(s)", thin_n(f, "similarity"), ju(&f["gain"], "lines"), ju(&f["gain"], "callers")),
                why: js(f, "action").to_string(),
                a: side_placed(&f["replace"]),
                b: side_placed(&f["use_instead"]),
            })
            .collect(),
        "similar_code" => first_arr(v, &["worst", "top_clusters"])
            .iter()
            .filter_map(|c| {
                let m = thin_arr(c, "members");
                let side = |m: &serde_json::Value| TwinSide {
                    repo: js(m, "repo").to_string(),
                    function: format!("{}::{}", js(m, "module_path"), js(m, "item_name")),
                    file: js(m, "file").to_string(),
                    line: ju(m, "line"),
                    end_line: ju(m, "line"),
                    moniker: m.get("id").and_then(serde_json::Value::as_str).map(str::to_string),
                    group_lines: String::new(),
                };
                (m.len() >= 2).then(|| TwinRow {
                    kind: "copy cluster".to_string(),
                    score: format!("cos {:.3} · struct {:.2} · ×{}", thin_n(c, "avg_cosine"), thin_n(c, "structural"), m.len()),
                    why: js(c, "recommendation").to_string(),
                    a: side(&m[0]),
                    b: side(&m[1]),
                })
            })
            .collect(),
        _ => Vec::new(),
    }
}

/// **What a twins lens rests on**, in the lens's own fields: semantic_twins'
/// `thresholds`, group_twins' `floor_basis` (the floor's calibration, or that it
/// is fixture-set) and gates. `""` when the answer states none.
pub fn twin_basis(lens: &str, v: &serde_json::Value) -> String {
    match lens {
        "semantic_twins" => {
            let t = &v["thresholds"];
            if !t.is_object() {
                return String::new();
            }
            format!(
                "twin (Type-4): cosine ≥ {} AND structural ≤ {} AND body MinHash < {} AND mutual kNN k={}; copy (Type-3): cosine ≥ {} AND structural ≥ {}",
                t["twin_cosine"], t["twin_structural_ceiling"], t["twin_body_ceiling"], t["mutual_k"], t["copy_cosine"], t["copy_structural_floor"]
            )
        }
        "group_twins" => {
            let o = &v["options"];
            let gates = if o.is_object() {
                format!(
                    "a twin = one normalised group body, or group-vector cosine ≥ {} with ≥ {} shared role-class {}-gram(s); ",
                    o["cosine_floor"], o["min_shared_kgrams"], o["kgram"]
                )
            } else {
                String::new()
            };
            match js(v, "floor_basis") {
                "" if gates.is_empty() => String::new(),
                "" => gates.trim_end_matches("; ").to_string(),
                fb => format!("{gates}floor basis: {fb}"),
            }
        }
        _ => v.get("thresholds").filter(|t| t.is_object()).map(|t| format!("thresholds: {t}")).unwrap_or_default(),
    }
}

/// `2-3,5-7,9` → `[(2,3),(5,7),(9,9)]` (group_twins' `ranges` format).
pub fn line_ranges(s: &str) -> Vec<(u64, u64)> {
    s.split(',')
        .filter_map(|p| {
            let p = p.trim();
            match p.split_once('-') {
                Some((a, b)) => Some((a.trim().parse().ok()?, b.trim().parse().ok()?)),
                None => p.parse().ok().map(|n| (n, n)),
            }
        })
        .collect()
}

/// The pair rows of one twins lens: its basis line, then per pair the kind, the
/// similarity, the [`OPEN_BOTH`] click, the lens's `why`, and both members
/// (repo, function, file:line, a group twin's lines). An empty list is drawn as
/// a measured empty ONLY when the lens says so — a refusal or a gap keeps the
/// evidence above as its whole word.
fn render_pair_rows(ui: &mut egui::Ui, lens: &'static str, v: &serde_json::Value, clicks: &mut Clicks) {
    let basis = twin_basis(lens, v);
    if !basis.is_empty() {
        ui.label(egui::RichText::new(format!("basis — {basis}")).small().italics());
    }
    let rows = twin_rows(lens, v);
    if rows.is_empty() {
        if LensAnswer::of(v) == LensAnswer::EmptyFact {
            ui.label(egui::RichText::new("no pair cleared the gates — a measured empty, per the note above").small());
        }
        return;
    }
    egui::ScrollArea::vertical()
        .max_height(360.0)
        .id_salt(("thin-pairs", lens))
        .show(ui, |ui| {
            for (i, row) in rows.iter().enumerate() {
                ui.horizontal_wrapped(|ui| {
                    chip(ui, &row.kind, if row.kind == "copy" || row.kind == "copy cluster" { AMBER } else { TEAL });
                    ui.monospace(&row.score);
                    if ui.link(OPEN_BOTH).clicked() {
                        clicks.pair = Some((lens, i));
                    }
                });
                if !row.why.is_empty() {
                    ui.label(egui::RichText::new(&row.why).small());
                }
                for side in [&row.a, &row.b] {
                    ui.horizontal(|ui| {
                        ui.add_space(14.0);
                        let lines = if side.group_lines.is_empty() { String::new() } else { format!("  lines {}", side.group_lines) };
                        ui.monospace(format!("{:<10} {}  {}{lines}", side.repo, side.function, side.at()));
                    });
                }
                ui.add_space(4.0);
            }
        });
}

/// The renderer for a lens with no view of its own: its evidence first, then
/// each ranked row as its scalar fields, verbatim (the pane adds no numbers).
fn render_rows(ui: &mut egui::Ui, v: &serde_json::Value, salt: &str) {
    thin_evidence(ui, v);
    egui::ScrollArea::vertical()
        .max_height(320.0)
        .id_salt(salt)
        .show(ui, |ui| {
            for row in thin_arr(v, "worst") {
                let line = match row.as_object() {
                    Some(o) => o
                        .iter()
                        .filter(|(k, x)| {
                            !x.is_array() && !x.is_object() && k.as_str() != "evidence_note"
                        })
                        .take(8)
                        .map(|(k, _)| format!("{k}={}", thin_s(row, k).chars().take(80).collect::<String>()).replace('\n', " "))
                        .collect::<Vec<_>>()
                        .join("  "),
                    None => row.to_string(),
                };
                ui.monospace(line);
            }
        });
}

// ── find → FIX ─────────────────────────────────────────────────────────────────────
// A lens today is a FINDING (read-only). The find→fix pattern adds the second half: for
// a lens whose finding maps to a concrete remediation, surface the ACTIONABLE next step
// beside the row — the imperative, the exact command/edit, and an honest
// `auto_apply` marker. There is no fix-APPLY seam wired from dwarves-gui yet (the
// dupes engine computes executable refactor plans, but nothing in this UI runs them), so
// every fix says so: `auto_apply` is a TODO the pane never fakes as done. A diagnostic
// lens (churn/anomaly/world-impact) has NO direct code fix and returns an empty list —
// the same honesty the rest of the pane keeps: never a fabricated action.

/// The universal auto-apply state: dwarves-gui has no fix-apply seam wired, so a fix is
/// a SUGGESTION + the exact command, never a silently-applied edit.
const AUTO_APPLY_TODO: &str = "TODO — no fix-apply seam wired in dwarves-gui (the fix is \
                               a suggestion + the exact command; nothing here executes it)";

/// One actionable remediation derived from ONE lens finding.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Fix {
    /// The lens this fix came from (`zombie`, `bloat`, …) — matches [`View::as_str`].
    pub lens: &'static str,
    /// What the fix targets (the finding, named): `korp / spark41`, `znippy::parse_stream`.
    pub subject: String,
    /// The imperative — what to DO.
    pub action: String,
    /// The exact command or edit to apply it (copy-pasteable / precise).
    pub command: String,
    /// Honest auto-apply state — a TODO (no apply seam) or `N/A` for a security finding
    /// that must never be auto-applied.
    pub auto_apply: &'static str,
}

impl Fix {
    fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "lens": self.lens,
            "subject": self.subject,
            "action": self.action,
            "command": self.command,
            "auto_apply": self.auto_apply,
        })
    }
}

/// The first present array among `keys` (thin and fat state_json name the same finding
/// list differently for a couple of lenses — read whichever is there).
fn first_arr<'a>(v: &'a serde_json::Value, keys: &[&str]) -> &'a [serde_json::Value] {
    for k in keys {
        if let Some(a) = v.get(k).and_then(|a| a.as_array()) {
            return a.as_slice();
        }
    }
    &[]
}

/// **The find→fix derivation — a pure fold over ONE lens's `state_json`.**
///
/// Reads exactly the finding the pane renders (thin RPC value or fat engine value, same
/// shape) and returns the actionable fix per finding. It invents no findings and no
/// numbers — a fix exists only where a finding does. Capped so the fix list mirrors the
/// ranked head the row list shows.
pub fn fixes_for(view: View, v: &serde_json::Value) -> Vec<Fix> {
    let cap = 6;
    match view {
        View::Zombie => first_arr(v, &["worst", "zombies"])
            .iter()
            .take(cap)
            .map(|f| {
                let (repo, feat) = (thin_s(f, "repo"), thin_s(f, "feature"));
                Fix {
                    lens: "zombie",
                    subject: format!("{repo} / {feat}"),
                    action: "remove the never-built feature and the code it gates".into(),
                    command: format!(
                        "# in {repo}/Cargo.toml drop the `{feat}` feature, then delete its \
                         `#[cfg(feature = \"{feat}\")]` items"
                    ),
                    auto_apply: AUTO_APPLY_TODO,
                }
            })
            .collect(),
        View::Bloat => first_arr(v, &["worst_artifacts"])
            .iter()
            .flat_map(|a| {
                first_arr(a, &["top_named"]).iter().map(|g| {
                    let (krate, sym, n) =
                        (thin_s(g, "crate"), thin_s(g, "symbol"), thin_n(g, "instantiations") as u64);
                    Fix {
                        lens: "bloat",
                        subject: format!("{krate}::{sym} ×{n}"),
                        action: "type-erase the monomorphised generic to recover the tax".into(),
                        command: format!(
                            "# split `{sym}`: keep a thin generic wrapper, move the body into a \
                             non-generic inner fn (one copy instead of {n})"
                        ),
                        auto_apply: AUTO_APPLY_TODO,
                    }
                })
            })
            .take(cap)
            .collect(),
        View::Villa => first_arr(v, &["findings"])
            .iter()
            .take(cap)
            .map(|h| {
                let (repo, loc) = (thin_s(h, "repo"), thin_s(h, "loc"));
                let tool = h.get("known_tool").and_then(|t| t.as_str()).unwrap_or("the external tool");
                Fix {
                    lens: "villa",
                    subject: format!("{repo} {loc} → {tool}"),
                    action: "replace the shell-out with the pure-Rust in-tree equivalent".into(),
                    command: format!(
                        "# drop the std::process::Command call to `{tool}`; call the pure-Rust \
                         crate instead (baremetal — no OS tooling)"
                    ),
                    auto_apply: AUTO_APPLY_TODO,
                }
            })
            .collect(),
        View::WeakTests => first_arr(v, &["worst", "never_red", "weak"])
            .iter()
            .take(cap)
            .map(|t| {
                let (repo, name) = (thin_s(t, "repo"), thin_s(t, "test_name"));
                Fix {
                    lens: "weak_tests",
                    subject: format!("{repo}::{name}"),
                    action: "make the test able to go RED (assert on the churning path)".into(),
                    command: format!(
                        "# add an assertion to `{name}` that fails when its covered code changes \
                         (a placebo test proves nothing)"
                    ),
                    auto_apply: AUTO_APPLY_TODO,
                }
            })
            .collect(),
        View::Risk => first_arr(v, &["worst"])
            .iter()
            .filter(|u| thin_n(u, "coverage_frac") < 1.0)
            .take(cap)
            .map(|u| {
                let (repo, file) = (thin_s(u, "repo"), thin_s(u, "file"));
                Fix {
                    lens: "risk",
                    subject: format!("{repo} {file}"),
                    action: "raise coverage on the high-risk file (callers × churn × uncovered)".into(),
                    command: format!("# add tests reaching {file}; it is high-churn, widely-called and under-covered"),
                    auto_apply: AUTO_APPLY_TODO,
                }
            })
            .collect(),
        View::BenchGaps => {
            // Thin shape is `{per_repo:[{repo, worst:[{module}]}]}`; fat shape is a bare
            // array of per-repo bench objects. Normalise to the per-repo list either way.
            let repos: &[serde_json::Value] = if v.is_array() {
                v.as_array().map(|a| a.as_slice()).unwrap_or(&[])
            } else {
                first_arr(v, &["per_repo"])
            };
            repos
                .iter()
                .flat_map(|rp| {
                    let repo = thin_s(rp, "repo");
                    first_arr(rp, &["worst"]).iter().map(move |h| {
                        let module = thin_s(h, "module");
                        Fix {
                            lens: "bench_gaps",
                            subject: format!("{repo}::{module}"),
                            action: "add a heavy bench for the un-benched hot path".into(),
                            command: format!(
                                "# add a criterion bench for `{module}` (benches/…); cargo bench -p {repo}"
                            ),
                            auto_apply: AUTO_APPLY_TODO,
                        }
                    })
                })
                .take(cap)
                .collect()
        }
        View::SimilarCode => {
            // The relocated dupes query. The EXECUTABLE fix (absorb → delete → repoint)
            // already exists in dwarves::dupes::proposals and renders in the 🧬 Dupes
            // pane — so the fix here points at that plan rather than re-deriving it.
            let clusters = first_arr(v, &["top_clusters", "clusters"]).len();
            if clusters == 0 {
                return Vec::new();
            }
            vec![Fix {
                lens: "similar_code",
                subject: format!("{clusters} similar-code cluster(s)"),
                action: "consume the shared leaf: absorb the unique knowledge, THEN delete the copy, then repoint".into(),
                command: "# 🧬 Dupes → REFACTOR PLANS → ▷ Dry-run apply (or drive it: update_json {\"apply\":\"dry-run\"} / {\"apply\":\"execute\"})".into(),
                // The ONE lens whose remediation is EXECUTABLE, and now WIRED: the Dupes
                // pane runs `dwarves::dupes::apply` over the computed refactor plans —
                // dry-run projects the transcript touching nothing, execute drives the
                // type-gated absorb→delete→repoint through a recording executor (no fs I/O
                // in this build; a real fs executor is the production plug).
                auto_apply: "WIRED — dwarves::dupes::apply drives the plan in the 🧬 Dupes pane: DRY-RUN projects the \
                             absorb→delete→repoint transcript (nothing touched), EXECUTE runs the plan's state machine \
                             (no filesystem edits in this build). A real fs executor is the production plug.",
            }]
        }
        View::SemanticClones => {
            // NO EXECUTABLE FIX, and that is the honest answer rather than a gap.
            // SimilarCode's remediation is absorb → delete → repoint because the copies
            // ARE the same code. A Type-4 pair is the opposite case: two independent
            // implementations of one behaviour, so "delete one" is a DESIGN call about
            // which behaviour is canonical — and the lens itself states on every response
            // that its thresholds are uncalibrated for the shipped q8 space.
            let n = first_arr(v, &["findings"]).len();
            if n == 0 {
                return Vec::new();
            }
            vec![Fix {
                lens: "semantic_clones",
                subject: format!("{n} independent re-implementation(s)"),
                action: "READ the pair, pick the canonical behaviour, then consolidate by hand".into(),
                command: "# there is no mechanical rewrite for a Type-4 clone — the two sites are different code".into(),
                auto_apply: "NEVER — a Type-4 pair is two implementations of ONE behaviour, so which survives \
                             is a design decision, not a refactor. The thresholds behind the finding are also \
                             HEURISTIC and uncalibrated for the shipped embedding profile.",
            }]
        }
        View::BinaryDrift => first_arr(v, &["findings"])
            .iter()
            .take(cap)
            .map(|f| {
                let (repo, sym) = (thin_s(f, "repo"), thin_s(f, "demangled"));
                Fix {
                    lens: "binary_drift",
                    subject: format!("{repo} {sym}"),
                    action: "AUDIT the compile-time injection — do NOT auto-apply".into(),
                    command: format!(
                        "# inspect the build.rs / proc-macro / toolchain that emitted `{sym}` (no \
                         source counterpart); this is a security finding"
                    ),
                    auto_apply: "N/A — a security finding is never auto-applied; it must be audited by a human",
                }
            })
            .collect(),
        View::NameCollision => first_arr(v, &["worst"])
            .iter()
            .take(cap)
            .map(|c| {
                let symbol = thin_s(c, "symbol");
                let repos: Vec<String> = c
                    .get("repos")
                    .and_then(|a| a.as_array())
                    .map(|a| a.iter().filter_map(|r| r.as_str().map(str::to_string)).collect())
                    .unwrap_or_default();
                Fix {
                    lens: "name_collision",
                    subject: format!("{symbol} ({})", repos.join(", ")),
                    action: "disambiguate the cross-repo path: rename one owner, or extract the shared \
                             definition to a single leaf both repos depend on".into(),
                    command: format!(
                        "# `{symbol}` is defined in {}: give each a distinct qualified path (rename \
                         one), or hoist it to one edda leaf so the call graph has ONE owner to bind to",
                        repos.join(" AND ")
                    ),
                    auto_apply: AUTO_APPLY_TODO,
                }
            })
            .collect(),
        // Sub-item clone regions ARE the dedup finding: the fix is the hoist.
        View::Subclone => first_arr(v, &["worst"])
            .iter()
            .take(cap)
            .map(|f| Fix {
                lens: "subclone",
                subject: thin_s(f, "location"),
                action: "hoist the shared region into ONE fn and call it from every site".into(),
                command: format!(
                    "# {} lines shared at {} site(s) across {}: extract the region at {} into \
                     one fn (it is SMALLER than the item it sits in — no whole-item dedup sees it)",
                    thin_n(f, "lines") as u64,
                    thin_n(f, "sites") as u64,
                    thin_s(f, "files"),
                    thin_s(f, "location"),
                ),
                auto_apply: AUTO_APPLY_TODO,
            })
            .collect(),
        // A twin pair is two implementations of one idea (or a copy across types):
        // which survives is a design call, as for semantic_clones.
        View::SemanticTwins => {
            let n = first_arr(v, &["worst"]).len();
            if n == 0 {
                return Vec::new();
            }
            vec![Fix {
                lens: "semantic_twins",
                subject: format!("{n} twin/copy pair(s)"),
                action: "READ both sides (⇆ open both), pick the canonical one, consolidate by hand".into(),
                command: "# a Type-4 twin is different code for one behaviour: no mechanical rewrite".into(),
                auto_apply: "NEVER — which implementation survives is a design decision, not a refactor.",
            }]
        }
        // A group twin IS a hoistable region: the same statements in two functions.
        View::GroupTwins => first_arr(v, &["worst"])
            .iter()
            .take(cap)
            .map(|f| Fix {
                lens: "group_twins",
                subject: format!("{} ↔ {}", js(&f["a"], "ident"), js(&f["b"], "ident")),
                action: "hoist the shared statement group into ONE fn both call".into(),
                command: format!(
                    "# extract lines {} of {} and {} of {} into one fn (the group is interleaved with other lines)",
                    js(&f["a"], "group_lines"),
                    js(&f["a"], "file"),
                    js(&f["b"], "group_lines"),
                    js(&f["b"], "file"),
                ),
                auto_apply: AUTO_APPLY_TODO,
            })
            .collect(),
        // A re-implemented library: the call to the library replaces the copy.
        View::ReimplementsShared => first_arr(v, &["worst"])
            .iter()
            .take(cap)
            .map(|f| Fix {
                lens: "reimplements_shared",
                subject: format!("{} → {}", js(&f["replace"], "path"), js(&f["use_instead"], "path")),
                action: js(f, "action").to_string(),
                command: format!(
                    "# {} lines {}-{} of {}: call `{}` ({})",
                    js(&f["replace"], "repo"),
                    ju(&f["replace"], "line"),
                    ju(&f["replace"], "end_line"),
                    js(&f["replace"], "file"),
                    js(&f["use_instead"], "path"),
                    js(f, "fit").replace('_', "-"),
                ),
                auto_apply: AUTO_APPLY_TODO,
            })
            .collect(),
        // Diagnostic lenses point at WHAT changed / WHAT is risky — there is no single
        // code edit that "fixes" them, so they honestly carry no fix.
        View::Churn
        | View::Anomaly
        | View::WorldImpact
        | View::Quality
        | View::DataReach
        | View::Trust
        | View::SymbolIdentity
        | View::UndocApi
        | View::ReachableVuln
        | View::Belongs => Vec::new(),
    }
}

/// The find→fix contract, as observable metadata (a robot reads the honesty line here).
fn fix_pattern_meta() -> serde_json::Value {
    serde_json::json!({
        "kind": "find_fix",
        "actionable": true,
        // The read-only ↔ actionable line, stated plainly and HONESTLY split: the one lens
        // with an executable remediation (SimilarCode/Dupes) now has a REAL apply seam;
        // every other lens surfaces the fix + the exact command but has no executor, so it
        // stays a suggestion. "partial" is the honest state — neither a blanket TODO nor a
        // claim that everything auto-applies.
        "auto_apply": "partial",
        // The lenses whose fix is EXECUTABLE today (the apply seam is wired for them).
        "apply_wired_lenses": ["similar_code"],
        "apply_seam": "dwarves::dupes::apply (ApplyOutcome::dry_run / ::execute over the computed \
                       RefactorPlan) — driven from the 🧬 Dupes pane (update_json {\"apply\":\"dry-run\"} \
                       / {\"apply\":\"execute\"}).",
        "note": "Every lens finding carries its FIX — the imperative + the exact command. Auto-apply \
                 is PARTIAL: the SimilarCode/Dupes lens now has a real apply seam — dwarves::dupes::apply \
                 runs the plan (absorb → delete → repoint) as a DRY-RUN (projection, nothing touched) or \
                 an EXECUTE (the plan's state machine through a recording executor, no filesystem edits in \
                 this build; a real fs executor is the production plug). Every OTHER lens stays a suggestion \
                 (no executor), and a security finding (binary_drift) is never auto-applied by design.",
    })
}

/// Render the find→fix block for the current view's fixes (both thin and fat paths).
fn render_fixes(ui: &mut egui::Ui, fixes: &[Fix]) {
    ui.separator();
    ui.horizontal_wrapped(|ui| {
        ui.label(egui::RichText::new("🔧 Fix on lens").strong().color(TEAL));
        chip(ui, "read-only suggestion", AMBER);
        ui.label(
            egui::RichText::new(
                "the actionable next step per finding — the imperative + the exact command. \
                 Auto-apply is a TODO (no apply seam); nothing here edits your code.",
            )
            .small(),
        );
    });
    if fixes.is_empty() {
        ui.label(
            egui::RichText::new(
                "this lens is diagnostic — it names WHAT changed / WHAT is risky; there is no \
                 single code edit that fixes it, so it honestly carries no fix.",
            )
            .small()
            .weak(),
        );
        return;
    }
    egui::ScrollArea::vertical()
        .max_height(220.0)
        .id_salt("lens-fixes")
        .show(ui, |ui| {
            for f in fixes {
                ui.horizontal_wrapped(|ui| {
                    let (label, color) = if f.auto_apply.starts_with("N/A") {
                        ("audit only", RED)
                    } else if f.auto_apply.starts_with("WIRED") {
                        // The one lens with a real apply seam (SimilarCode/Dupes).
                        ("apply: wired (dry-run/execute)", TEAL)
                    } else {
                        ("auto-apply: TODO", AMBER)
                    };
                    chip(ui, label, color);
                    ui.label(egui::RichText::new(&f.action).strong());
                });
                ui.label(
                    egui::RichText::new(format!("    {}", f.subject))
                        .small()
                        .monospace(),
                );
                ui.label(
                    egui::RichText::new(format!("    {}", f.command))
                        .small()
                        .monospace()
                        .color(TEAL),
                );
            }
        });
}

impl Facet for LensPane {
    fn title(&self) -> &str {
        &self.title
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        self.poll();
        // READ ON OPEN. nornir computes each lens on the request from its code
        // room (Mímir v2: no chain, no cache — a query, not a job), so making the
        // operator press a button to see it is pure friction. Fires once per pane
        // (and again after a workspace change, which clears the reading).
        if !self.auto_read && self.pending.is_none() && !self.scanned() && self.error.is_none() {
            self.auto_read = true;
            self.scan();
        }
        if self.is_pending() {
            // Keep the frame ticking while the worker folds; land the result next poll.
            ui.ctx()
                .request_repaint_after(std::time::Duration::from_millis(120));
        }
        ui.horizontal(|ui| {
            ui.heading("🔍 Mímir");
            // NO Scan/⟳ button (owner 2026-08-22: SCAN is not a user action). This
            // pane only READS: it reads itself on open (above); a spinner shows while
            // that read lands, and the header says, per lens, what the answer rests
            // on (`answer_status`: complete / partial / empty / not wired / refused)
            // — never a button that makes the operator wait.
            //
            // There is ONE place to ask the server to catch up, which is NOT here:
            // `🧹 Repopulate` in the warehouse picker (`warehouse_select.rs`) drives
            // `Workspaces.Repopulate { purge: true }` — a consume for every member
            // behind its offset, once, for the whole warehouse. If a lens here says
            // `not_consumed`, that button is the answer; a lens that says
            // `not_wired` waits for a producer no button can start.
            if self.pending.is_some() {
                ui.spinner();
            }
            if self.view == View::Bloat {
                ui.checkbox(&mut self.actionable_only, "actionable only");
            }
            // The find→fix affordance: show the actionable next step per finding.
            ui.checkbox(&mut self.show_fixes, "🔧 fixes");
        });
        ui.label(egui::RichText::new(self.status()).monospace());
        ui.label(egui::RichText::new(format!("source: {}", self.source.describe())).small());
        ui.separator();

        // **The Mímir menu — one place for every code-graph / quality operation.**
        // The owner: "alla operationer för att hitta dubbletter och liknande skall ligga
        // under Mimir-meny" — dedup (SimilarCode), dead code (Zombie), risk, weak tests,
        // bench gaps, the new purity lens (Villa) and the Brokkr quality scorecard are
        // ONE grouped menu here, not scattered. Quality is listed first as the summary.
        ui.horizontal_wrapped(|ui| {
            ui.label(egui::RichText::new("Mímir:").strong().color(TEAL));
            for v in [
                View::Quality,
                View::SimilarCode,
                // Its twin sits NEXT to it: the two PARTITION the high-cosine pair
                // space, so a reader who finds one should see the other without hunting.
                View::SemanticClones,
                View::SemanticTwins,
                View::Zombie,
                View::Risk,
                View::WeakTests,
                View::BenchGaps,
                View::Bloat,
                View::Villa,
                View::Churn,
                View::Anomaly,
                View::WorldImpact,
                View::BinaryDrift,
                View::NameCollision,
                View::SymbolIdentity,
                View::DataReach,
                View::Trust,
                View::UndocApi,
                View::ReachableVuln,
                View::Subclone,
                View::GroupTwins,
                View::Belongs,
            ] {
                let label = MIMIR_MENU
                    .iter()
                    .find(|(view, _)| *view == v.as_str())
                    .map(|(_, l)| *l)
                    .unwrap_or(v.as_str());
                if ui.selectable_label(self.view == v, label).clicked() {
                    self.view = v;
                }
            }
        });
        ui.add_space(4.0);

        if self.view == View::Belongs {
            // The L5a decision, on screen — not buried in a module doc.
            ui.label(
                egui::RichText::new(
                    "These lenses QUERY the warehouse, and nornir owns the warehouse (L5a). \
                     The query half of each belongs in nornir behind an RPC so korp's UI and \
                     nornir's own viz can use it too — a query only dwarves can reach is the \
                     mistake the dupes engine already made. What stays in dwarves is the GATE \
                     (--fail-on-findings) and the PROPOSAL.",
                )
                .small(),
            );
            ui.add_space(6.0);

The file goes on; the console shows all of it.