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

nordisk/nornir

src/code_room/reader.rs

//! The readers of the code graph that are not lenses — ONE dispatch point.
//!
//! Every front door (gRPC `Knowledge.*` / `Introspect.*` / `Dwarf.*` / `Viz.*`,
//! the MCP tools, the CLI verbs) calls [`answer`] with the surface's name and
//! its request as JSON. The surface names are the gRPC verbs where one exists
//! ([`SURFACES`]); the MCP tool and the CLI verb of the same question use the
//! same name, so there is one reader per question.
//!
//! Answer shapes the gRPC doors decode (everything else is passed through as
//! the JSON it is):
//!
//! | surfaces | `Ok` value |
//! |---|---|
//! | `Introspect.Callers` / `Callees` / `PathBetween`, `Knowledge.CallPath` | `{"names": [string]}` |
//! | `Knowledge.SymbolLookup` / `DefinedIn` | `{"symbols": [{crate_name, module_path, item_kind, item_name, visibility, file, line, doc_lines, signature}]}` |
//! | `Knowledge.Callers` / `Callees` | `{"calls": [{crate_name, caller_path, callee_ident, call_kind, file, line}]}` |
//!
//! Every `Ok` value is a JSON object that carries, beside its own fields,
//! `state` (`complete` · `partial` · `empty_fact` · `empty_gap`) and
//! `evidence` (the room's record: scope, inputs, kinds walked, what is
//! MISSING, notes). A surface that cannot answer refuses by state:
//!
//! | state | when |
//! |---|---|
//! | `not_consumed` | the repo has no consumer offset; a binary verb on a repo consumed without the binary layer |
//! | `invalid_argument` | a name that resolves to no node, an empty required field, an unknown artifact |
//! | `not_wired` | the graph does not hold the fact: the world ring (P5), the embeddings index |
//! | `not_linked` | a binary built without `code-room` |
//!
//! The surfaces a thin client's panes ask through [`door`] (`CodeRoom.Answer`
//! on the wire) carry the room they were read from in `room` (workspace,
//! repo@seq + commit, node and edge counts): `Viz.CrateGraph`,
//! `Viz.CallGraph`, `Viz.FunctionTier`, `Knowledge.Symbols`,
//! `Knowledge.Source`, `Knowledge.TestReach`; `Coderoom.SnapshotDiff` carries
//! both (`room_from`, `room_to`).
//!
//! Still `not_wired`, each naming its missing producer: `Coderoom.Ingest` /
//! `Compat` / `WhatBreaks`, `Vector.SlicesIndex` / `SlicesSearch`,
//! `Knowledge.Context` without seeds.
//!
//! Table access (`TOUCHES_TABLE`, written by the rustc driver): the
//! `table-access-lineage` template, the architecture board's Table nodes and
//! reads / writes edges, and the 🚇 lines' termini
//! ([`wired::ArchPayload::access`]).

use serde_json::Value;

use super::{Reach, Refusal};

/// Every surface a front door sends here.
pub const SURFACES: &[&str] = &[
    "Introspect.Callers",
    "Introspect.Callees",
    "Introspect.PathBetween",
    "Knowledge.SymbolLookup",
    "Knowledge.DefinedIn",
    "Knowledge.Callers",
    "Knowledge.Callees",
    "Knowledge.CallPath",
    "Knowledge.FeatureGates",
    "Knowledge.Context",
    "Knowledge.Map",
    "Dwarf.Callers",
    "Dwarf.Callees",
    "Dwarf.CallPath",
    "Viz.Knowledge",
    "Viz.Architecture",
    "Viz.TestMatrix.inventory",
    "Viz.CrateGraph",
    "Viz.CallGraph",
    "Viz.FunctionTier",
    "Viz.CodeRoom3D",
    "Viz.CodeMap",
    "Viz.Binary",
    "Knowledge.Symbols",
    "Knowledge.Source",
    "Knowledge.TestReach",
    "Arch.Generate",
    "Arch.Trace",
    "Arch.Open",
    "Arch.History",
    "Arch.Graph",
    "Mimir.SqlTemplate",
    "Mimir.SystemBoundary",
    "Coderoom.Ingest",
    "Coderoom.Compat",
    "Coderoom.WhatBreaks",
    "Coderoom.SnapshotDiff",
    "Coderoom.Units",
    "Coderoom.Artifacts",
    "Funnel.Retrieve",
    "Test.List",
    "Test.Affected",
    "Vector.SlicesIndex",
    "Vector.SlicesSearch",
];

/// Answers a server computes on its jera queue, kept per room version.
pub mod queued;
pub use queued::{AnswerCache, Prepared, QUEUED_SURFACES, compute_and_keep, is_queued, queued_body, ready};

/// The linked half: every surface over a room.
#[cfg(feature = "code-room")]
pub mod wired;

/// The linked half of [`pending_blast`].
#[cfg(feature = "code-room")]
pub mod blast;

#[cfg(all(test, feature = "code-room"))]
pub(crate) mod fixture;

/// The refusal of every surface in a binary built without the engine.
#[cfg(not(feature = "code-room"))]
fn not_linked(what: &str) -> Refusal {
    Refusal::new(
        super::State::NotLinked,
        format!("{what}: this binary is built without the `code-room` feature (a thin atom) — the engine is not linked here, the server answers over gRPC"),
    )
}

/// The code room of the workspace whose warehouse root is `builds`, for a
/// reader that has no front-door handle of its own (the fat viz panes). ONE
/// handle per lake per process — a lake is a single-process skade catalog —
/// so every caller naming the same `builds` gets the same one.
pub fn local(workspace: &str, builds: &std::path::Path) -> std::sync::Arc<super::CodeRoom> {
    use std::collections::BTreeMap;
    use std::path::PathBuf;
    use std::sync::{Arc, Mutex};
    static LOCAL: Mutex<BTreeMap<PathBuf, Arc<super::CodeRoom>>> = Mutex::new(BTreeMap::new());
    let mut held = LOCAL.lock().unwrap_or_else(|e| e.into_inner());
    held.entry(builds.to_path_buf()).or_insert_with(|| Arc::new(super::CodeRoom::new(workspace, builds))).clone()
}

/// The step that dumps an UNCOMMITTED tree for a reader (the release
/// doctor's dry run). A door hands the one it has.
pub enum TreeDriver {
    /// This door has none, and why (`driver_not_available` with the probe's
    /// step; `not_linked` in a thin atom).
    Unavailable(Refusal),
    /// The nightly driver, probed.
    #[cfg(feature = "code-room")]
    Ready(Box<mimir_ingest::DriverReady>),
    /// A dump step given — `(member, tree)` → the dump (a scripted one in a test).
    #[cfg(feature = "code-room")]
    Given(Box<dyn Fn(&str, &std::path::Path) -> anyhow::Result<mimir_ingest::CommitDump> + Send + Sync>),
}

impl TreeDriver {
    /// The driver of this box as its settings say (`NORNIR_MIMIR_DRIVER` …,
    /// `code_room::driver`), located and probed now.
    pub fn from_env() -> Self {
        #[cfg(feature = "code-room")]
        {
            match super::driver::Settings::from_env().probe() {
                Ok(ready) => Self::Ready(Box::new(ready)),
                Err(why) => Self::Unavailable(why),
            }
        }
        #[cfg(not(feature = "code-room"))]
        {
            Self::Unavailable(not_linked("the dry run of an uncommitted tree"))
        }
    }
}

/// One place outside the member that names a changed symbol.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlastSite {
    pub repo: String,
    pub crate_name: String,
    /// The readable path of the node the edge leaves.
    pub caller: String,
    /// The edge kind (`CALLS`, `USES_TYPE`, `IMPLEMENTS`, `FOR_TYPE`, `READS_FIELD`).
    pub kind: String,
    pub file: String,
    pub line: u32,
}

/// One symbol of the member the tree changes or removes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlastSymbol {
    /// The readable path (`crate::module::Type::name`).
    pub symbol: String,
    /// The node id (moniker) in the consumed state.
    pub id: String,
    /// Gone from the tree (a change of payload otherwise: type, visibility,
    /// gate, doc line, or a param / field of it).
    pub removed: bool,
    /// Where other repos name it; empty = nobody outside the member does.
    pub sites: Vec<BlastSite>,
}

/// What the uncommitted tree of one member would break ([`pending_blast`]).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PendingBlast {
    pub member: String,
    /// The member's crates that hold a changed symbol.
    pub crates: Vec<String>,
    /// Every changed / removed symbol, most sites first.
    pub symbols: Vec<BlastSymbol>,
    /// Local units of the tree that did not compile: their rows are NOT in
    /// the delta, the answer is partial (a gate must not pass on it).
    pub failed_units: Vec<String>,
    /// Nodes of other repos that transitively depend on the change (the
    /// Jikes walk), and the repos they are in.
    pub dependents: u64,
    pub dependent_repos: Vec<String>,
    /// What the room says about the answer (lower bounds, gaps).
    pub notes: Vec<String>,
}

/// The semantic blast of `member`'s uncommitted `tree`: a dry-run dump of it
/// (nothing is written), its delta against the member's consumed state, and
/// who outside the member names what it changes or removes. `dependents` =
/// the repos whose sites the answer must hold: one of them not consumed
/// refuses, like the member itself (`not_consumed`). A tree that changes
/// nothing a node carries answers an empty blast, not a refusal.
///
/// As long as a `cargo check` of the member (the driver compiles the tree).
pub fn pending_blast(reach: &Reach<'_>, driver: &TreeDriver, member: &str, tree: &std::path::Path, dependents: &[String]) -> Result<PendingBlast, Refusal> {
    #[cfg(feature = "code-room")]
    {
        blast::pending_blast(reach, driver, member, tree, dependents)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, driver, member, tree, dependents);
        Err(not_linked("the semantic blast"))
    }
}

/// One fn of [`ApiSurface`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiFn {
    /// The bare name (the completeness gate matches handlers by name).
    pub name: String,
    /// The readable path (`crate::module::Type::name`).
    pub path: String,
    pub is_test: bool,
}

/// What the autonom completeness gate reads of one repo: its test fns (the
/// roots), the fns of its NAMEABLE API (effective visibility `direct` or
/// `reexported` — the surface to cover), and the calls to walk from the one
/// to the other.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApiSurface {
    pub fns: Vec<ApiFn>,
    /// `(caller, callee)` by bare name: CALLS and TESTS edges leaving the
    /// repo's own definitions.
    pub calls: Vec<(String, String)>,
    /// What the room says about the answer (lower bounds, gaps).
    pub notes: Vec<String>,
}

/// The fn-level API surface of `repo` from the code room. Refuses — never an
/// empty surface — when the repo is not consumed or its log carries no
/// visibility.
pub fn api_surface(reach: &Reach<'_>, repo: &str) -> Result<ApiSurface, Refusal> {
    #[cfg(feature = "code-room")]
    {
        wired::api_surface(reach, repo)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, repo);
        Err(not_linked("the API surface"))
    }
}

/// Answer `surface` for `params` from what the door reaches.
pub fn answer(surface: &str, reach: &Reach<'_>, params: &Value) -> Result<Value, Refusal> {
    if !SURFACES.contains(&surface) {
        return Err(Refusal::new(
            super::State::InvalidArgument,
            format!("`{surface}` is not a code-room surface (known: {})", SURFACES.join(", ")),
        ));
    }
    #[cfg(feature = "code-room")]
    {
        wired::answer(surface, reach, params)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, params);
        Err(not_linked(surface))
    }
}

/// The WIRE door (`CodeRoom.Answer`): any surface by name, its request as
/// JSON text, answered as ONE JSON object that always carries `state` and
/// `surface`. A refusal is a body too — `refused: true`, the refusal's state
/// and its sentence in `note` — because a thin client draws the state by
/// name and a status code cannot carry it.
pub fn door(surface: &str, reach: &Reach<'_>, params_json: &str) -> Value {
    let params = if params_json.trim().is_empty() {
        serde_json::json!({})
    } else {
        match serde_json::from_str::<Value>(params_json) {
            Ok(v) => v,
            Err(e) => return refusal_body(surface, &Refusal::new(super::State::InvalidArgument, format!("`{surface}`: the request is not JSON: {e}"))),
        }
    };
    match answer(surface, reach, &params) {
        Ok(mut v) => {
            if let Some(o) = v.as_object_mut() {
                o.insert("surface".into(), Value::String(surface.to_string()));
                // an answer that is a fixed list (the template menu) has no gaps to name
                o.entry("state").or_insert_with(|| Value::String("complete".into()));
            }
            v
        }
        Err(r) => refusal_body(surface, &r),
    }
}

/// A [`Refusal`] as the door's answer body.
pub fn refusal_body(surface: &str, r: &Refusal) -> Value {
    serde_json::json!({ "surface": surface, "state": r.state.as_str(), "refused": true, "note": r.note })
}

/// The symbol / call view of ONE repo — the typed model
/// ([`crate::knowledge::query::KnowledgeView`]) the MCP knowledge tools, the CLI
/// `knowledge query` and the Graph-RAG slice join fold. Same refusals as
/// [`answer`]; an `Ok` view is never a stand-in for "not consumed".
///
/// Rows in the v1 vocabulary, from the room: a symbol per locally defined item
/// (`fn` for fns and methods, `module_path` = the readable path of its parent,
/// an impl block named `impl Trait for Type`), a call row per positioned call
/// site of every CALLS edge a local definition has (callee = its readable path).
pub fn knowledge_view(reach: &Reach<'_>, repo: &str) -> Result<crate::knowledge::query::KnowledgeView, Refusal> {
    #[cfg(feature = "code-room")]
    {
        wired::knowledge_view(reach, repo)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, repo);
        Err(not_linked("the knowledge view"))
    }
}

/// The architecture board of ONE repo (the module / crate quotient of the
/// code graph) as the typed [`crate::arch::ArchGraph`] its renderers take.
pub fn arch_graph(reach: &Reach<'_>, repo: &str) -> Result<crate::arch::ArchGraph, Refusal> {
    #[cfg(feature = "code-room")]
    {
        wired::arch_graph(reach, repo)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, repo);
        Err(not_linked("the architecture board"))
    }
}

/// What the room holds of `repo` — the knowledge block of `Mimir.RepoOverview`
/// (and `Knowledge.Map`): consumed offsets, symbol / call / test / file counts,
/// a sample of names, the room's own stats, and the evidence.
pub fn knowledge_block(reach: &Reach<'_>, repo: &str) -> Result<Value, Refusal> {
    #[cfg(feature = "code-room")]
    {
        wired::knowledge_block(reach, repo)
    }
    #[cfg(not(feature = "code-room"))]
    {
        let _ = (reach, repo);
        Err(not_linked("the knowledge block"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::code_room::State;

    #[test]
    fn an_unknown_surface_is_an_argument_error_not_a_missing_producer() {
        let r = answer("Knowledge.Nope", &Reach::none(), &Value::Null).unwrap_err();
        assert_eq!(r.state, State::InvalidArgument);
    }

    #[test]
    fn surface_names_are_unique() {
        let mut seen = std::collections::BTreeSet::new();
        for s in SURFACES {
            assert!(seen.insert(*s), "`{s}` is listed twice");
        }
    }
}