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, ¶ms) {
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");
}
}
}