src/code_room/reader/wired.rs
//! The linked half of the reader: every surface answered from a room.
//!
//! One rule for all of them: a real answer carries the room's evidence
//! (`state`, `evidence`), and "cannot answer" is a [`Refusal`] by state —
//! `not_consumed` when the repo has no consumer offset, `invalid_argument`
//! when a name resolves to nothing, `not_wired` (with the producer row that
//! is missing) when the graph does not hold the fact at all. Never an empty
//! list that reads as "there is none".
use std::collections::{BTreeMap, BTreeSet, HashMap};
use mimir_room::{AnswerState, EdgeSel, Evidence, GroupBy, Mask, MaskSpec, NameMatch, NameQuery, Room, Scope};
use mimir_schema::{EdgeKind, Origin, Provenance, kind as nk};
use serde_json::{Value, json};
use super::super::{CodeRoom, Reach, Refusal, State};
use crate::arch::{self, ArchGraph};
use crate::knowledge::query::KnowledgeView;
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};
use crate::warehouse::access_scan::{Access, AccessEdge};
/// Said with every reachability answer (producer gap P6).
pub const LOWER_BOUND: &str =
"calls through operators, closures and fn pointers are not recorded by the producer yet: every reachability answer is a lower bound";
/// Said with every answer of the binary verbs (producer gap P4).
pub const LINKED_ONLY: &str = "source CALLS between LINKED nodes only (provenance `both`): a call the compiler inlined away or made indirect is absent — \
binary call edges have no producer yet";
/// Said when a room holds `cfg` gates but no GATED_BY edge (a log whose
/// producer predates mimir-schema 2, or a repo consumed without the gate scan).
pub const ACTIVE_CFG_ONLY: &str = "this room holds no GATED_BY edge and no `cfg_gated_out` stub: only items the consumer's `cargo check` configuration \
compiled are listed, by their own `cfg` — what a feature that was OFF there would add is absent (a lower bound)";
/// Node kinds that are SYMBOLS in the v1 row vocabulary (`symbol_facts`).
const SYMBOL_KINDS: [&str; 13] = [
nk::FN,
nk::METHOD,
nk::STRUCT,
nk::ENUM,
nk::UNION,
nk::TRAIT,
nk::TRAIT_ALIAS,
nk::IMPL,
nk::CONST,
nk::STATIC,
nk::TYPE,
nk::MACRO,
nk::MOD,
];
/// `Viz.Architecture` on the wire — field for field the 🏛 tab's
/// `viz::arch_tab::ArchPayload` (the tab is an egui type the server does not
/// build; a test on the tab's side decodes this into it).
#[derive(Debug, Clone, serde::Serialize)]
pub struct ArchPayload {
pub graph: ArchGraph,
pub members_with_data: Vec<String>,
pub errors: Vec<String>,
pub coverage: BTreeMap<String, String>,
pub symbols: Vec<SymbolRow>,
pub calls: Vec<CallEdgeRow>,
pub metro_fuel_source: String,
/// `(gRPC handler fn, table)`: the warehouse table(s) nearest to each
/// handler along CALLS + TOUCHES_TABLE — the 🚇 lines' termini. A handler
/// that reaches no table has no pair (its line ends in a placeholder).
pub access: Vec<(String, String)>,
}
/// `Viz.Knowledge` on the wire — the 🗺 tab's `KnowledgeSummary`.
#[derive(Debug, Clone, serde::Serialize)]
pub struct KnowledgeSummary {
pub repos: Vec<RepoScanSummary>,
pub crates: Vec<Bubble>,
/// Local crate → local crate (CALLS ∪ USES_TYPE): the release tab's deep edges.
pub crate_edges: Vec<CrateEdge>,
}
/// `from` calls or uses a type of `to`.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct CrateEdge {
pub from: String,
pub to: String,
/// Item-level edges collapsed into this one.
pub edges: u64,
/// A few of the names used (the first by ordinal).
pub via: Vec<String>,
}
/// One consumed repo of the knowledge map.
#[derive(Debug, Clone, serde::Serialize)]
pub struct RepoScanSummary {
pub repo: String,
pub ok: bool,
pub symbols: usize,
pub calls: usize,
pub features: usize,
pub git_files: usize,
pub error: Option<String>,
}
/// One local crate of the knowledge map.
#[derive(Debug, Clone, serde::Serialize)]
pub struct Bubble {
pub repo: String,
pub krate: String,
pub symbols: usize,
pub calls: usize,
pub files: usize,
pub gates: usize,
pub heat_30d: i64,
}
/// Call edges shipped as metro fuel over one wire message, at most.
const METRO_FUEL_CAP: usize = 250_000;
pub(super) fn internal(e: impl std::fmt::Display) -> Refusal {
Refusal::new(State::Internal, format!("code room: {e}"))
}
fn invalid(msg: impl Into<String>) -> Refusal {
Refusal::new(State::InvalidArgument, msg)
}
fn not_wired(msg: impl Into<String>) -> Refusal {
Refusal::new(State::NotWired, msg)
}
fn text<'a>(p: &'a Value, key: &str) -> &'a str {
p.get(key).and_then(Value::as_str).unwrap_or("").trim()
}
fn limit_of(p: &Value, default: usize) -> usize {
match p.get("limit").and_then(Value::as_u64) {
Some(0) | None => default,
Some(n) => n as usize,
}
}
/// The room of `repo` — every consumed repo when `repo` is empty — or the
/// state that says why there is none.
pub fn open(reach: &Reach<'_>, repo: &str) -> Result<Room, Refusal> {
let handle = reach.code_room()?;
open_in(handle, repo)
}
fn open_in(handle: &CodeRoom, repo: &str) -> Result<Room, Refusal> {
let scope = if repo.is_empty() { Scope::current() } else { Scope::repo(repo, None) };
let room = handle.room(&scope)?;
if room.scope().repos.is_empty() {
return Err(Refusal::new(
State::NotConsumed,
format!("code room of `{}`: no repo has a consumer offset yet — nothing of this workspace was consumed", handle.workspace()),
));
}
Ok(room)
}
/// Fold another query's gaps and notes into the evidence that is returned.
fn absorb(into: &mut Evidence, other: &Evidence) {
for m in &other.missing {
if !into.missing.contains(m) {
into.missing.push(m.clone());
}
}
for n in &other.notes {
// each query words its own "empty answer" note; the final state says it once
if !n.starts_with("empty answer") && !n.starts_with("matched as") && !n.starts_with("capped:") && !into.notes.contains(n) {
into.notes.push(n.clone());
}
}
for k in &other.kinds {
if !into.kinds.contains(k) {
into.kinds.push(*k);
}
}
}
/// The final state of an answer that is `empty` or not, given its evidence.
fn settle(ev: &mut Evidence, empty: bool) {
let gaps = !ev.missing.is_empty();
ev.state = match (empty, gaps) {
(false, false) => AnswerState::Complete,
(false, true) => AnswerState::Partial,
(true, false) => AnswerState::EmptyFact,
(true, true) => AnswerState::EmptyGap,
};
ev.notes.retain(|n| !n.starts_with("empty answer"));
if empty {
ev.notes.push(if gaps {
"empty answer, and inputs are MISSING (above): the empty set may be the gap, not a fact".into()
} else {
"empty answer with every input present: the room holds no match (a fact, not a gap)".into()
});
}
}
fn note(ev: &mut Evidence, n: &str) {
if !ev.notes.iter().any(|x| x == n) {
ev.notes.push(n.to_string());
}
}
fn miss(ev: &mut Evidence, input: &str, repo: Option<&str>, why: impl Into<String>) {
let m = mimir_room::Missing { input: input.to_string(), repo: repo.map(str::to_string), why: why.into() };
if !ev.missing.contains(&m) {
ev.missing.push(m);
}
}
/// `body` (a JSON object) with `state` and `evidence` beside its own fields.
fn envelope(mut body: Value, ev: &Evidence) -> Value {
if let Some(o) = body.as_object_mut() {
o.insert("state".into(), json!(ev.state.as_str()));
o.insert("evidence".into(), serde_json::to_value(ev).unwrap_or(Value::Null));
}
body
}
/// A JSON object from OWNED values, moved in. (`json!` serializes every
/// expression it is handed once more — `"fns": fns` deep-copied a function
/// tier's 120 000 rows, a quarter of its answer time.)
pub(crate) fn obj<const N: usize>(pairs: [(&str, Value); N]) -> Value {
Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
/// Readable paths, computed once per node.
struct Paths<'r> {
room: &'r Room,
memo: HashMap<u64, String>,
}
impl<'r> Paths<'r> {
fn new(room: &'r Room) -> Self {
Self { room, memo: HashMap::new() }
}
fn of(&mut self, o: u64) -> String {
if let Some(p) = self.memo.get(&o) {
return p.clone();
}
let p = self.room.display_path(o);
self.memo.insert(o, p.clone());
p
}
}
/// The nodes `name` means, or `invalid_argument` naming what was tried: a
/// name that resolves to nothing is not "no callers".
fn resolve(room: &Room, name: &str, what: &str) -> Result<(Vec<u64>, Evidence), Refusal> {
if name.is_empty() {
return Err(invalid(format!("`{what}` is empty: give a moniker, a `a::b::c` path (or its suffix), or a linker symbol")));
}
let found = room.lookup(&NameQuery::new(name).mode(NameMatch::Auto)).map_err(internal)?;
if found.value.is_empty() {
return Err(invalid(format!(
"`{name}` ({what}) names no node of the room [{}] — tried as a moniker, as a linker symbol and as a path suffix",
room.scope()
)));
}
Ok((found.value, found.evidence))
}
// ------------------------------------------------------------------ rows
fn kind_of<'a>(room: &'a Room, o: u64) -> &'a str {
room.node(o).kind.unwrap_or("")
}
/// `fn` for both free fns and methods (the v1 `symbol_facts.item_kind`).
fn item_kind(kind: &str) -> &str {
if kind == nk::METHOD { nk::FN } else { kind }
}
/// The locally defined symbol nodes of `repo` (all repos of the room when
/// `None`), in ordinal order.
fn symbol_nodes(room: &Room, repo: Option<&str>) -> Result<Mask, Refusal> {
let spec = MaskSpec {
repos: repo.map(|r| vec![r.to_string()]).unwrap_or_default(),
origins: vec![Origin::Local],
node_kinds: SYMBOL_KINDS.iter().map(|k| k.to_string()).collect(),
defined_only: true,
..Default::default()
};
// (a symbol kind no node of this room has is no gap of the answer)
Ok(room.mask(&spec).map_err(internal)?.value)
}
/// The trait an impl block implements, by its readable name.
fn impl_traits(room: &Room) -> Result<HashMap<u64, u64>, Refusal> {
let e = room.edges(&EdgeSel::kinds(&[EdgeKind::Implements])).map_err(internal)?;
Ok(e.value.edges.iter().map(|x| (x.from, x.to)).collect())
}
/// One node as a v1 symbol row. `visibility` is the producer's nominal
/// visibility; `doc_lines` is 1 / 0 for documented / not (the producer keeps
/// the first doc line, not a line count).
fn symbol_row(room: &Room, paths: &mut Paths<'_>, traits: &HashMap<u64, u64>, o: u64) -> SymbolRow {
let v = room.node(o);
let kind = v.kind.unwrap_or("");
let name = v.name.unwrap_or("").to_string();
let item_name = if kind == nk::IMPL {
let ty = room.impl_self(o).and_then(|t| room.node(t).name).map(str::to_string).unwrap_or_else(|| paths.of(o).rsplit("::").next().unwrap_or("?").to_string());
match traits.get(&o).and_then(|t| room.node(*t).name) {
Some(tr) => format!("impl {tr} for {ty}"),
None => format!("impl {ty}"),
}
} else {
name
};
SymbolRow {
crate_name: v.krate.unwrap_or("").to_string(),
module_path: v.parent.map(|p| paths.of(p)).unwrap_or_default(),
item_kind: item_kind(kind).to_string(),
item_name,
visibility: v.visibility.unwrap_or("").to_string(),
file: v.file.unwrap_or("").to_string(),
line: v.start_line.unwrap_or(0),
doc_lines: v.has_doc.unwrap_or(false) as u32,
signature: v.ty.map(str::to_string),
}
}
/// CALLS edges as v1 call rows: one row per positioned site, or one row at
/// line 0 for an edge without one (a call inside a macro expansion).
fn call_rows(room: &Room, paths: &mut Paths<'_>, edges: &[mimir_room::EdgeOut], cap: usize) -> Vec<CallEdgeRow> {
let mut out = Vec::new();
for e in edges {
let (caller, callee) = (paths.of(e.from), paths.of(e.to));
let krate = room.node(e.from).krate.unwrap_or("").to_string();
let sites = room.sites(e.from, e.to, e.kind);
let row = |file: &str, line: u32| CallEdgeRow {
crate_name: krate.clone(),
caller_path: caller.clone(),
callee_ident: callee.clone(),
call_kind: "call".to_string(),
file: file.to_string(),
line,
};
if sites.is_empty() {
out.push(row(room.node(e.from).file.unwrap_or(""), 0));
} else {
out.extend(sites.iter().map(|s| row(s.file, s.start_line)));
}
if out.len() >= cap {
out.truncate(cap);
break;
}
}
out
}
/// The symbol / call view of one repo (every repo of the room when `None`).
fn view_rows(room: &Room, repo: Option<&str>) -> Result<(Vec<SymbolRow>, Vec<CallEdgeRow>, Evidence), Refusal> {
let nodes = symbol_nodes(room, repo)?;
let traits = impl_traits(room)?;
let mut paths = Paths::new(room);
let symbols: Vec<SymbolRow> = nodes.ordinals().iter().map(|o| symbol_row(room, &mut paths, &traits, *o)).collect();
// every call a local definition of the repo makes — to anything
let callers = room
.mask(&MaskSpec { repos: repo.map(|r| vec![r.to_string()]).unwrap_or_default(), origins: vec![Origin::Local], defined_only: true, ..Default::default() })
.map_err(internal)?
.value;
let edges = room.edges(&EdgeSel { from: Some(&callers), ..EdgeSel::kinds(&[EdgeKind::Calls]) }).map_err(internal)?;
let calls = call_rows(room, &mut paths, &edges.value.edges, usize::MAX);
let mut ev = edges.evidence;
ev.query = format!("symbol / call view of {}", repo.unwrap_or("every consumed repo"));
note(&mut ev, LOWER_BOUND);
p1_gap(room, &mut ev);
settle(&mut ev, symbols.is_empty());
Ok((symbols, calls, ev))
}
/// [`super::knowledge_view`].
pub fn knowledge_view(reach: &Reach<'_>, repo: &str) -> Result<KnowledgeView, Refusal> {
if repo.is_empty() {
return Err(invalid("the knowledge view is per repo: `repo` is empty"));
}
let room = open(reach, repo)?;
let (symbols, calls, _) = view_rows(&room, Some(repo))?;
Ok(KnowledgeView::new(symbols, calls))
}
fn symbol_json(repo: Option<&str>, s: &SymbolRow) -> Value {
json!({
"repo": repo,
"crate_name": s.crate_name,
"module_path": s.module_path,
"item_kind": s.item_kind,
"item_name": s.item_name,
"visibility": s.visibility,
"file": s.file,
"line": s.line,
"doc_lines": s.doc_lines,
"signature": s.signature.clone().unwrap_or_default(),
})
}
/// A symbol row plus what only the room knows of the node: how far outside
/// its crate it is visible, the first doc line, its moniker.
fn symbol_json_of(room: &Room, o: u64, s: &SymbolRow) -> Value {
let v = room.node(o);
let mut row = symbol_json(v.repo, s);
row["effective_visibility"] = json!(v.effective_visibility.map(|e| e.as_str()));
row["doc"] = json!(v.doc);
row["moniker"] = json!(v.moniker);
row
}
// ------------------------------------------------------------------ call verbs
#[derive(Clone, Copy, PartialEq, Eq)]
enum Dir {
Callers,
Callees,
}
/// The linked part of the room (`both`), or the state that says the binary
/// stream is not there. With `binary`, narrowed to the nodes that artifact links.
fn linked_mask(reach: &Reach<'_>, room: &Room, binary: &str) -> Result<(Mask, Evidence), Refusal> {
let all = room.symbols(None, None).map_err(internal)?;
if all.value.is_empty() {
return Err(no_linked_symbol(reach, room, "a verb over the linked call graph cannot answer, and that is not `no callers`"));
}
let both = room.mask(&MaskSpec { provenances: vec![Provenance::Both], ..Default::default() }).map_err(internal)?;
let mut ev = both.evidence;
if binary.is_empty() {
return Ok((both.value, ev));
}
// `binary` names an artifact: its file name, its bin name, or `name@triple:profile`
let base = binary.rsplit('/').next().unwrap_or(binary);
let of: Vec<u64> = all
.value
.iter()
.filter(|s| s.binary == Some(base) || s.artifact == binary || s.artifact.split('@').next() == Some(base))
.map(|s| s.node)
.collect();
if of.is_empty() {
let known: BTreeSet<&str> = all.value.iter().map(|s| s.artifact).collect();
return Err(invalid(format!("no linked artifact `{binary}` in the room [{}] (linked: {known:?})", room.scope())));
}
note(&mut ev, &format!("restricted to what the artifact `{binary}` links"));
Ok((both.value.intersect(&room.mask_of(&of)), ev))
}
/// The state of a room that holds no linked symbol: the binary hook looked
/// and found no artifact (`no_artifact`, with where it looked), or the binary
/// stream of its repos was never consumed (`not_consumed`). `what` says what
/// therefore cannot answer — never an empty answer.
fn no_linked_symbol(reach: &Reach<'_>, room: &Room, what: &str) -> Refusal {
if let Some(why) = no_artifact_of(reach, room) {
return Refusal::new(State::NoArtifact, format!("the room [{}] holds no linked symbol: {why} — {what}", room.scope()));
}
Refusal::new(
State::NotConsumed,
format!(
"the room [{}] holds no linked symbol: the binary stream of its repos was not consumed (no linked artifact of them was read: the binary hook reads `<builds>/mimir_v2/artifacts/<member>/<sha>/`) — {what}",
room.scope()
),
)
}
/// The binary hook's own word for the repos of `room`, when it is
/// `no_artifact` for the commit a repo is at: `<repo>: <where it looked>`.
fn no_artifact_of(reach: &Reach<'_>, room: &Room) -> Option<String> {
let log = reach.code_room().ok()?.log().ok()?;
let said: Vec<String> = room
.scope()
.offset_shas
.iter()
.filter_map(|(repo, sha)| {
let row = log.block_on(log.feed_last(repo)).ok()?.remove(crate::code_room::feeds::Binary::NAME)?;
(row.status == mimir_ingest::schema::STATUS_NO_ARTIFACT && row.sha == *sha).then(|| format!("{repo}: no_artifact — {}", row.detail.unwrap_or_default()))
})
.collect();
(!said.is_empty()).then(|| said.join("; "))
}
/// Direct callers / callees of `name`, optionally inside `within` (the
/// linked nodes): the edges themselves, with their sites.
fn one_hop(room: &Room, dir: Dir, name: &str, within: Option<&Mask>, limit: usize) -> Result<(Vec<mimir_room::EdgeOut>, Evidence), Refusal> {
let (seeds, lookup_ev) = resolve(room, name, "name")?;
let seed_mask = room.mask_of(&seeds);
let seed_in = within.map(|w| seed_mask.intersect(w));
let seed_ref = seed_in.as_ref().unwrap_or(&seed_mask);
let sel = match dir {
Dir::Callers => EdgeSel { to: Some(seed_ref), from: within, cap: Some(limit), ..EdgeSel::kinds(&[EdgeKind::Calls]) },
Dir::Callees => EdgeSel { from: Some(seed_ref), to: within, cap: Some(limit), ..EdgeSel::kinds(&[EdgeKind::Calls]) },
};
let found = room.edges(&sel).map_err(internal)?;
let mut ev = found.evidence;
ev.query = format!("{} of `{name}` ({} node(s))", if dir == Dir::Callers { "callers" } else { "callees" }, seeds.len());
absorb(&mut ev, &lookup_ev);
if let Some(w) = within
&& seed_mask.intersect(w).is_empty()
{
note(&mut ev, &format!("`{name}` itself is not linked (no symbol, no linked instance): it has no place in the linked call graph"));
}
note(&mut ev, LOWER_BOUND);
let empty = found.value.edges.is_empty();
settle(&mut ev, empty);
Ok((found.value.edges, ev))
}
fn edge_rows(room: &Room, paths: &mut Paths<'_>, edges: &[mimir_room::EdgeOut]) -> Vec<Value> {
edges
.iter()
.map(|e| {
let sites = room.sites(e.from, e.to, e.kind);
json!({
"caller": paths.of(e.from),
"callee": paths.of(e.to),
"caller_moniker": room.moniker(e.from),
"callee_moniker": room.moniker(e.to),
"sites": e.count,
"file": sites.first().map(|s| s.file),
"lines": sites.iter().map(|s| s.start_line).collect::<Vec<_>>(),
})
})
.collect()
}
/// `{"names": […]}` for a one-hop verb: the OTHER end of every edge.
fn names_answer(room: &Room, dir: Dir, edges: &[mimir_room::EdgeOut], ev: &Evidence, linked: bool) -> Value {
let mut paths = Paths::new(room);
let names: BTreeSet<String> = edges.iter().map(|e| paths.of(if dir == Dir::Callers { e.from } else { e.to })).collect();
envelope(json!({ "names": names, "rows": edge_rows(room, &mut paths, edges), "linked_only": linked }), ev)
}
/// A shortest CALLS path `from` → `to` as `{"names": […]}`.
fn path_answer(room: &Room, from: &str, to: &str, within: Option<&Mask>, extra: &[&str]) -> Result<Value, Refusal> {
let (a, ev_a) = resolve(room, from, "from")?;
let (b, ev_b) = resolve(room, to, "to")?;
let found = room.path(&a, &b, &[EdgeKind::Calls], None, within).map_err(internal)?;
let mut ev = found.evidence;
ev.query = format!("call path `{from}` → `{to}`");
absorb(&mut ev, &ev_a);
absorb(&mut ev, &ev_b);
note(&mut ev, LOWER_BOUND);
for n in extra {
note(&mut ev, n);
}
settle(&mut ev, found.value.is_none());
let mut paths = Paths::new(room);
let (names, steps): (Vec<String>, Vec<Value>) = match &found.value {
Some(p) => (
p.nodes.iter().map(|o| paths.of(*o)).collect(),
p.nodes
.iter()
.map(|o| {
let v = room.node(*o);
json!({ "node": paths.of(*o), "moniker": v.moniker, "kind": v.kind, "file": v.file, "line": v.start_line })
})
.collect(),
),
None => (Vec::new(), Vec::new()),
};
Ok(envelope(json!({ "names": names, "path": steps, "hops": names.len().saturating_sub(1), "linked_only": within.is_some() }), &ev))
}
/// What a path answered through the ring says about itself.
const THROUGH_THE_RING: &str = "walked with the universe ring folded in (the dependencies and the toolchain's std, down to their `extern` declarations): \
the room of the repo's own code holds no such path";
/// `Knowledge.CallPath`: a shortest CALLS path `from` → `to`.
///
/// Asked of the room of the repo's own code first (what every other reader
/// opens). That room ends at the reference stubs of what the code calls in
/// other crates — so when it holds no answer (`to` names no node of it, or no
/// path ends inside it) the walk is done again with the UNIVERSE RING folded
/// in: the dependency crates and the toolchain's std, where a path from a
/// workspace function to an `extern "C"` item goes. The answer says which
/// room it is from (`ring`, and a note).
fn call_path(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let (repo, from, to) = (text(p, "repo"), text(p, "from"), text(p, "to"));
let room = open(reach, repo)?;
let first = path_answer(&room, from, to, None, &[]);
let found = |v: &Value| v["names"].as_array().is_some_and(|n| !n.is_empty());
match &first {
Ok(v) if found(v) => return first,
// an empty `from` / `to` is the caller's, with or without the ring
Err(r) if r.state != State::InvalidArgument || from.is_empty() || to.is_empty() => return first,
_ => {}
}
let handle = reach.code_room()?;
let scope = if repo.is_empty() { Scope::current() } else { Scope::repo(repo, None) };
let through = handle.room(&scope.with_universe(true)).and_then(|ring| path_answer(&ring, from, to, None, &[THROUGH_THE_RING]));
match through {
Ok(mut v) if found(&v) => {
v["ring"] = json!(true);
Ok(v)
}
// the ring holds no such path either: the answer of the repo's own room
// stands (its evidence says where its walk stopped)
_ => first,
}
}
/// `Introspect.*` and `Dwarf.*`: ONE reader — source CALLS between linked nodes.
fn binary_verb(reach: &Reach<'_>, surface: &str, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
pinned_sha(&room, repo, text(p, "sha"))?;
let (linked, linked_ev) = linked_mask(reach, &room, text(p, "binary"))?;
let finish = |mut v: Value| {
if let Some(e) = v.get_mut("evidence").and_then(|e| e.get_mut("notes")).and_then(Value::as_array_mut) {
for n in linked_ev.notes.iter().map(String::as_str).chain([LINKED_ONLY]) {
if !e.iter().any(|x| x == n) {
e.push(json!(n));
}
}
}
if let Some(m) = v.get_mut("evidence").and_then(|e| e.get_mut("missing")).and_then(Value::as_array_mut) {
for gap in &linked_ev.missing {
let g = serde_json::to_value(gap).unwrap_or(Value::Null);
if !m.contains(&g) {
m.push(g);
}
}
}
v
};
let name = if text(p, "name").is_empty() { text(p, "arg") } else { text(p, "name") };
match surface.rsplit('.').next().unwrap_or("") {
"Callers" => {
let (edges, ev) = one_hop(&room, Dir::Callers, name, Some(&linked), limit_of(p, 200))?;
Ok(finish(names_answer(&room, Dir::Callers, &edges, &ev, true)))
}
"Callees" => {
let (edges, ev) = one_hop(&room, Dir::Callees, name, Some(&linked), limit_of(p, 200))?;
Ok(finish(names_answer(&room, Dir::Callees, &edges, &ev, true)))
}
_ => Ok(finish(path_answer(&room, text(p, "from"), text(p, "to"), Some(&linked), &[])?)),
}
}
/// A `sha` pin must be the room's own commit: this reader does not time
/// travel by sha (the ledger maps sha → seq; the handle does not expose it).
fn pinned_sha(room: &Room, repo: &str, sha: &str) -> Result<(), Refusal> {
if sha.is_empty() || repo.is_empty() {
return Ok(());
}
let at = room.scope().offset_shas.get(repo).map(String::as_str).unwrap_or("");
if at.starts_with(sha) || sha.starts_with(at) && !at.is_empty() {
return Ok(());
}
Err(invalid(format!("`{repo}` is consumed at {at}; this reader answers at the consumer offset only, not at `{sha}`")))
}
fn knowledge_calls(reach: &Reach<'_>, dir: Dir, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
let limit = limit_of(p, 100);
let (edges, ev) = one_hop(&room, dir, text(p, "name"), None, limit)?;
let mut paths = Paths::new(&room);
let calls = call_rows(&room, &mut paths, &edges, limit);
Ok(envelope(json!({ "calls": calls, "edges": edges.len() }), &ev))
}
// ------------------------------------------------------------------ knowledge
fn symbol_lookup(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
let arg = text(p, "arg");
if arg.is_empty() {
return Err(invalid("symbol lookup: `arg` (a name, or part of one) is empty"));
}
let limit = limit_of(p, 100);
let scope = room
.mask(&MaskSpec {
repos: if repo.is_empty() { vec![] } else { vec![repo.to_string()] },
origins: vec![Origin::Local],
node_kinds: SYMBOL_KINDS.iter().map(|k| k.to_string()).collect(),
defined_only: true,
..Default::default()
})
.map_err(internal)?;
// the v1 contract: the item's NAME contains the pattern, any case (an
// impl block's name is `impl Trait for Type`)
let low = arg.to_lowercase();
let traits = impl_traits(&room)?;
let mut paths = Paths::new(&room);
let mut rows: Vec<Value> = Vec::new();
let mut total = 0usize;
for o in scope.value.ordinals() {
let v = room.node(*o);
let is_impl = v.kind == Some(nk::IMPL);
if !is_impl && !v.name.is_some_and(|n| n.to_lowercase().contains(&low)) {
continue;
}
let row = symbol_row(&room, &mut paths, &traits, *o);
if is_impl && !row.item_name.to_lowercase().contains(&low) {
continue;
}
total += 1;
if rows.len() < limit {
rows.push(symbol_json_of(&room, *o, &row));
}
}
let mut ev = scope.evidence;
// a symbol kind no node of this room has is no gap of a name lookup
ev.missing.retain(|m| !m.input.starts_with("node kind"));
ev.query = format!("symbols whose name contains `{arg}`");
if total > rows.len() {
ev.capped = Some(mimir_room::Capped { shown: rows.len() as u64, total: total as u64 });
}
p1_gap(&room, &mut ev);
settle(&mut ev, rows.is_empty());
Ok(envelope(json!({ "symbols": rows }), &ev))
}
/// A log written before the producer knew visibility and docs: the row
/// fields are empty, and the evidence says they are unknown, not `private`.
fn p1_gap(room: &Room, ev: &mut Evidence) {
if !room.has_attr("visibility") {
miss(ev, "visibility", None, "this log's node payload has no `visibility` / `has_doc` column (written before mimir-schema 2): those row fields are empty, not `private` / undocumented");
}
}
fn defined_in(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
let suffix = text(p, "arg");
if suffix.is_empty() {
return Err(invalid("defined-in: `arg` (a file path, or its tail) is empty"));
}
let files: Vec<(String, String)> = room
.files()
.iter()
.filter(|f| f.path.ends_with(suffix) && (repo.is_empty() || f.repo == repo))
.map(|f| (f.repo.to_string(), f.path.to_string()))
.collect();
let mut ev = room.mask(&MaskSpec::default()).map_err(internal)?.evidence;
ev.query = format!("symbols defined in a file ending with `{suffix}`");
if room.files().is_empty() {
miss(&mut ev, "code_span_events", None, "the room holds no span row: no node can be placed in a file");
} else if files.is_empty() {
note(&mut ev, &format!("no file of the room ends with `{suffix}` ({} files hold spans)", room.files().len()));
}
let traits = impl_traits(&room)?;
let mut paths = Paths::new(&room);
let limit = limit_of(p, 100);
let mut rows = Vec::new();
for (r, path) in &files {
for o in room.nodes_in_file(Some(r), path) {
if SYMBOL_KINDS.contains(&kind_of(&room, o)) && room.node(o).defined {
rows.push(symbol_json_of(&room, o, &symbol_row(&room, &mut paths, &traits, o)));
}
}
}
let total = rows.len();
rows.truncate(limit);
if total > rows.len() {
ev.capped = Some(mimir_room::Capped { shown: rows.len() as u64, total: total as u64 });
}
p1_gap(&room, &mut ev);
settle(&mut ev, rows.is_empty());
Ok(envelope(json!({ "symbols": rows, "files": files.iter().map(|f| &f.1).collect::<Vec<_>>() }), &ev))
}
/// The cargo-feature view of a repo: every item with a `cfg` of its own
/// (`gates`), every item → feature gate with its `negated` / `partial` flags
/// (`gated_by`), the features and whether the build switched them on, and the
/// items the configuration compiled OUT (`gated_out`, token-level stubs).
fn feature_gates(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
let repos = if repo.is_empty() { vec![] } else { vec![repo.to_string()] };
let gated = room.mask(&MaskSpec { repos: repos.clone(), origins: vec![Origin::Local], with_cfg: true, defined_only: true, ..Default::default() }).map_err(internal)?;
let own = room.mask(&MaskSpec { repos: repos.clone(), origins: vec![Origin::Local], ..Default::default() }).map_err(internal)?.value;
let mut paths = Paths::new(&room);
let place = |o: u64, paths: &mut Paths<'_>| {
let v = room.node(o);
json!({
"crate_name": v.krate,
"module_path": v.parent.map(|x| paths.of(x)).unwrap_or_default(),
"item_name": v.name,
"item_kind": if v.kind == Some(nk::CFG_GATED_OUT) { v.ty } else { v.kind },
"cfg_expr": v.cfg,
"file": v.file,
"line": v.start_line,
})
};
let (mut gates, mut gated_out) = (Vec::new(), Vec::new());
for o in gated.value.ordinals() {
if kind_of(&room, *o) == nk::CFG_GATED_OUT { gated_out.push(place(*o, &mut paths)) } else { gates.push(place(*o, &mut paths)) }
}
let by = room.edges(&EdgeSel { from: Some(&own), ..EdgeSel::kinds(&[EdgeKind::GatedBy]) }).map_err(internal)?;
let gated_by: Vec<Value> = by
.value
.edges
.iter()
.map(|e| {
let sites = room.sites(e.from, e.to, e.kind);
json!({
"item": paths.of(e.from),
"feature": room.node(e.to).name,
"feature_of": room.node(e.to).krate,
"negated": e.negated,
"partial": e.partial,
"compiled_out": kind_of(&room, e.from) == nk::CFG_GATED_OUT,
"file": sites.first().map(|s| s.file),
"lines": sites.iter().map(|s| s.start_line).collect::<Vec<_>>(),
})
})
.collect();
// a feature is ON in this build iff its crate node ENABLES it
let on: BTreeSet<u64> = room.edges(&EdgeSel { from: Some(&own), ..EdgeSel::kinds(&[EdgeKind::Enables]) }).map_err(internal)?.value.edges.iter().filter(|e| kind_of(&room, e.from) == nk::CRATE).map(|e| e.to).collect();
let features: Vec<Value> = room
.mask(&MaskSpec { repos, node_kinds: vec![nk::FEATURE.into()], ..Default::default() })
.map_err(internal)?
.value
.ordinals()
.iter()
.map(|o| {
let v = room.node(*o);
json!({ "crate_name": v.krate, "feature": v.name, "declared": v.defined, "enabled_in_this_build": on.contains(o) })
})
.collect();
let mut ev = gated.evidence;
ev.query = "what is behind a cfg / a cargo feature".into();
ev.missing.retain(|m| !m.input.starts_with("node kind"));
if by.value.total == 0 && gated_out.is_empty() {
miss(&mut ev, "GATED_BY edges", None, ACTIVE_CFG_ONLY);
}
settle(&mut ev, gates.is_empty() && gated_by.is_empty() && gated_out.is_empty());
Ok(envelope(json!({ "gates": gates, "gated_by": gated_by, "features": features, "gated_out": gated_out }), &ev))
}
/// The knowledge block of a repo: what the room holds of it. The first
/// surface that needs nothing but the room.
pub fn knowledge_block(reach: &Reach<'_>, repo: &str) -> Result<Value, Refusal> {
let room = open(reach, repo)?;
let st = room.stats();
let symbols = symbol_nodes(&room, (!repo.is_empty()).then_some(repo))?;
let local = room
.mask(&MaskSpec { repos: if repo.is_empty() { vec![] } else { vec![repo.to_string()] }, origins: vec![Origin::Local], defined_only: true, ..Default::default() })
.map_err(internal)?;
let calls = room.edges(&EdgeSel { from: Some(&local.value), cap: Some(0), ..EdgeSel::kinds(&[EdgeKind::Calls]) }).map_err(internal)?;
let mut by_kind: BTreeMap<&str, u64> = BTreeMap::new();
for o in symbols.ordinals() {
*by_kind.entry(kind_of(&room, *o)).or_default() += 1;
}
let mut paths = Paths::new(&room);
let sample: Vec<String> = symbols.ordinals().iter().filter(|o| [nk::FN, nk::METHOD, nk::STRUCT, nk::TRAIT].contains(&kind_of(&room, **o))).take(12).map(|o| paths.of(*o)).collect();
let mut ev = local.evidence;
ev.query = format!("what the room holds of {}", if repo.is_empty() { "every consumed repo" } else { repo });
absorb(&mut ev, &calls.evidence);
note(&mut ev, LOWER_BOUND);
p1_gap(&room, &mut ev);
for (r, rows) in &st.repo_rows {
if rows.binary == 0 {
miss(&mut ev, "binary stream", Some(r), "consumed without the binary layer: provenance `both` / `binary_only` and linked sizes are absent here, not zero");
}
}
settle(&mut ev, symbols.is_empty());
let offsets: Vec<Value> = room.scope().repos.iter().map(|(r, seq)| json!({ "repo": r, "seq": seq, "sha": room.scope().offset_shas.get(r) })).collect();
Ok(envelope(
json!({
"source": "code_room",
"consumed": offsets,
"symbols": symbols.len(),
"symbols_by_kind": by_kind,
"call_edges": calls.value.total,
"call_sites": calls.value.sites,
"tests": room.mask(&MaskSpec { repos: if repo.is_empty() { vec![] } else { vec![repo.to_string()] }, is_test: Some(true), ..Default::default() }).map_err(internal)?.value.len(),
"files": room.files().iter().filter(|f| repo.is_empty() || f.repo == repo).count(),
"sample_symbols": sample,
"room": {
"scope": st.scope,
"nodes": st.nodes,
"edges": st.edges.iter().map(|(k, n)| (k.as_str().to_string(), *n)).collect::<BTreeMap<_, _>>(),
"provenance": st.provenance.iter().map(|(k, n)| (k.as_str().to_string(), *n)).collect::<BTreeMap<_, _>>(),
"linked_symbols": st.symbols,
"repeated_keys": st.duplicate_keys,
},
}),
&ev,
))
}
/// The graph half of a context slice: the seeds' call neighbourhood, each
/// node with its span — what a prompt needs to READ. The semantic half
/// (which spans answer a free-text question) is the embeddings lane.
fn context(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
pinned_sha(&room, repo, text(p, "sha"))?;
let seeds: Vec<&str> = p.get("seeds").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).filter(|s| !s.trim().is_empty()).collect()).unwrap_or_default();
if seeds.is_empty() {
return Err(not_wired(format!(
"knowledge context for the free-text query `{}`: ranking spans by meaning needs the embeddings index over the room's item spans (the vector lane) — \
not built yet. Pass `seeds` (symbol names) and this reader answers their call neighbourhood with file spans from the room",
text(p, "query")
)));
}
let hops = p.get("hops").and_then(Value::as_u64).unwrap_or(1).clamp(0, 6) as u32;
let callers_too = p.get("include_callers").and_then(Value::as_bool).unwrap_or(false);
let limit = limit_of(p, 8).max(1);
let mut start: Vec<u64> = Vec::new();
let mut ev: Option<Evidence> = None;
for s in &seeds {
let (nodes, e) = resolve(&room, s, "seed")?;
start.extend(nodes);
match &mut ev {
Some(acc) => absorb(acc, &e),
None => ev = Some(e),
}
}
let mut ev = ev.unwrap_or_default();
let down = room.hops_from(&start, &[EdgeKind::Calls], Some(hops), None).map_err(internal)?;
absorb(&mut ev, &down.evidence);
let mut radius: BTreeMap<u64, u64> = start.iter().map(|o| (*o, 0)).collect();
for (o, h) in &down.value {
radius.entry(*o).or_insert(*h);
}
if callers_too {
// the hop a caller sits at is not layered by this walk: it is "within `hops`"
let up = room.dependents_by(&start, &[EdgeKind::Calls], Some(hops), None).map_err(internal)?;
absorb(&mut ev, &up.evidence);
for o in &up.value.reached {
radius.entry(*o).or_insert(hops as u64);
}
}
let mut paths = Paths::new(&room);
let mut slices: Vec<(u64, Value)> = Vec::new();
let mut unplaced = 0u64;
for (o, h) in &radius {
let v = room.node(*o);
match (v.file, v.start_line, v.end_line) {
(Some(file), Some(s), Some(e)) => slices.push((*h, json!({
"repo": room.span(*o).map(|x| x.repo),
"file": file,
"start_line": s,
"end_line": e,
"radius_hops": h,
"symbols": [paths.of(*o)],
"kind": v.kind,
}))),
_ => unplaced += 1,
}
}
slices.sort_by(|a, b| a.0.cmp(&b.0).then(a.1["file"].as_str().cmp(&b.1["file"].as_str())).then(a.1["start_line"].as_u64().cmp(&b.1["start_line"].as_u64())));
let total = slices.len();
slices.truncate(limit);
ev.query = format!("call neighbourhood of {seeds:?} within {hops} hop(s)");
if total > slices.len() {
ev.capped = Some(mimir_room::Capped { shown: slices.len() as u64, total: total as u64 });
}
if unplaced > 0 {
note(&mut ev, &format!("{unplaced} node(s) of the neighbourhood have no span in the room (dependency / std items, stubs): not sliceable here"));
}
miss(&mut ev, "embeddings", None, "no semantic ranking: slices are ordered by call distance from the seeds, not by similarity to the query (the vector lane)");
note(&mut ev, LOWER_BOUND);
settle(&mut ev, slices.is_empty());
Ok(envelope(json!({ "slices": slices.into_iter().map(|s| s.1).collect::<Vec<_>>(), "ranking": "call_distance" }), &ev))
}
// ------------------------------------------------------------------ tests
/// The test inventory: the `is_test` nodes of the graph.
pub fn test_inventory(reach: &Reach<'_>, repo: &str) -> Result<(Vec<Value>, Evidence), Refusal> {
let room = open(reach, repo)?;
let tests = room
.mask(&MaskSpec { repos: if repo.is_empty() { vec![] } else { vec![repo.to_string()] }, is_test: Some(true), ..Default::default() })
.map_err(internal)?;
let mut paths = Paths::new(&room);
// `#[ignore]` as the driver recorded it; a test consumed before the
// column existed has no value there
let mut unknown = 0usize;
let rows: Vec<Value> = tests
.value
.ordinals()
.iter()
.map(|o| {
let v = room.node(*o);
let mut row = json!({
"repo": v.repo,
"crate_name": v.krate,
"module_path": v.parent.map(|x| paths.of(x)).unwrap_or_default(),
"test_name": v.name,
"file": v.file,
"line": v.start_line,
});
match room.attr(*o, "ignored").and_then(|a| a.as_bool()) {
Some(h) => row["is_heavy"] = json!(h),
None => unknown += 1,
}
row
})
.collect();
let mut ev = tests.evidence;
ev.query = format!("test inventory of {}", if repo.is_empty() { "every consumed repo" } else { repo });
if unknown > 0 {
miss(
&mut ev,
"is_heavy",
None,
&format!("{unknown} test(s) have no `ignored` (`#[ignore]`) value in the log — consumed before mimir-schema 3 added the column and not consumed again since: `is_heavy` is left out of their rows, not false"),
);
}
note(&mut ev, "tests of targets the consumer's `cargo check` did not compile (a test harness behind a feature that is off) are not in the graph");
settle(&mut ev, rows.is_empty());
Ok((rows, ev))
}
/// `Test.List`: the inventory joined with the latest result per test when
/// the door reaches a warehouse (ok | fail | X); the inventory alone, and
/// said so, when it does not.
fn test_list(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let (mut rows, mut ev) = test_inventory(reach, repo)?;
let results_state = match reach.warehouse {
None => {
miss(&mut ev, "test_results", None, "this front door has no warehouse in reach: the inventory is not joined with run results (every test would read `X`)");
"not_joined"
}
Some(wh) => {
use crate::warehouse::test_results::{TestSelector, query_test_results};
let results = wh.block_on(query_test_results(wh, &TestSelector::All)).map_err(internal)?;
// the latest result per (repo, test name as the runner prints it)
let mut latest: HashMap<(String, String), (i64, String)> = HashMap::new();
for r in results {
let slot = latest.entry((r.repo.clone(), r.test_name.clone())).or_insert((i64::MIN, String::new()));
if r.ts_micros >= slot.0 {
*slot = (r.ts_micros, r.status.clone());
}
}
for row in &mut rows {
let (r, name) = (row["repo"].as_str().unwrap_or("").to_string(), row["test_name"].as_str().unwrap_or("").to_string());
let module = row["module_path"].as_str().unwrap_or("");
// the runner prints `module::path::test` without the crate segment
let tail = module.split_once("::").map(|(_, m)| format!("{m}::{name}")).unwrap_or_else(|| name.clone());
let hit = [tail, format!("{module}::{name}"), name].into_iter().find_map(|k| latest.get(&(r.clone(), k)).map(|x| x.1.clone()));
row["status"] = json!(match hit.as_deref() {
Some("pass") => "ok",
Some("fail") | Some("stalled") => "fail",
_ => "X",
});
}
note(&mut ev, "joined with `test_results` on (repo, module path without the crate + test name): a test never run reads `X`");
"joined"
}
};
settle(&mut ev, rows.is_empty());
Ok(envelope(json!({ "inventory": rows, "results_state": results_state }), &ev))
}
// ------------------------------------------------------------------ tables
/// Said with every answer about table access: what the producer cannot see.
pub const TABLES_LOWER_BOUND: &str = "a table reached through a closure parameter or a fn pointer, through a trait none of whose impls the producer saw, \
or named only inside SQL text, is not a TOUCHES_TABLE edge: table access is a lower bound";
/// One fn → table edge of the room, read.
struct Touch {
from: u64,
/// `None` = a table whose name is not known at compile time.
table: Option<String>,
access: Option<mimir_schema::Access>,
/// Sites the edge counts (≥ the positioned ones).
count: u32,
/// `(file, line)` of each positioned site.
sites: Vec<(String, u32)>,
}
/// The TOUCHES_TABLE edges that leave the local code of `repo` (every repo
/// of the room when `None`), with the evidence of the read.
struct Touches {
edges: Vec<Touch>,
evidence: Evidence,
}
impl Touches {
fn unresolved(&self) -> impl Iterator<Item = &Touch> {
self.edges.iter().filter(|t| t.table.is_none())
}
/// The names of the tables touched, sorted.
fn tables(&self) -> BTreeSet<&str> {
self.edges.iter().filter_map(|t| t.table.as_deref()).collect()
}
/// The board's access rows: one per (fn, table, direction, site).
fn access_rows(&self, room: &Room, paths: &mut Paths<'_>) -> Vec<AccessEdge> {
let mut out = Vec::new();
for t in &self.edges {
let Some(table) = &t.table else { continue };
let (caller, krate) = (paths.of(t.from), room.node(t.from).krate.unwrap_or("").to_string());
let directions: &[Access] = match t.access {
Some(mimir_schema::Access::Read) => &[Access::Read],
Some(mimir_schema::Access::Write) => &[Access::Write],
// `both`, and an edge with no value (a touch all the same)
_ => &[Access::Read, Access::Write],
};
let at: Vec<(String, u32)> = if t.sites.is_empty() { vec![(room.node(t.from).file.unwrap_or("").to_string(), 0)] } else { t.sites.clone() };
for access in directions {
for (file, line) in &at {
out.push(AccessEdge { caller_fn: caller.clone(), crate_name: krate.clone(), table: table.clone(), access: *access, file: file.clone(), line: *line });
}
}
}
out
}
/// Say what the edges do not cover: the runtime-named sites, by fn.
fn say_unresolved(&self, paths: &mut Paths<'_>, ev: &mut Evidence) {
let fns: Vec<String> = self.unresolved().map(|t| paths.of(t.from)).collect();
if fns.is_empty() {
return;
}
let sites: u32 = self.unresolved().map(|t| t.count).sum();
note(
ev,
&format!(
"{sites} site(s) in {} fn(s) touch a table whose name is not known at compile time (a runtime string): they stand for no named table ({}{})",
fns.len(),
fns.iter().take(5).cloned().collect::<Vec<_>>().join(", "),
if fns.len() > 5 { ", …" } else { "" }
),
);
}
}
fn touches(room: &Room, repo: Option<&str>) -> Result<Touches, Refusal> {
let local = room
.mask(&MaskSpec { repos: repo.map(|r| vec![r.to_string()]).unwrap_or_default(), origins: vec![Origin::Local], defined_only: true, ..Default::default() })
.map_err(internal)?
.value;
let found = room.edges(&EdgeSel { from: Some(&local), ..EdgeSel::kinds(&[EdgeKind::TouchesTable]) }).map_err(internal)?;
let mut ev = found.evidence;
// In a lake created with the `access` column every edge row was written
// by a producer that writes TOUCHES_TABLE: none is then a FACT about the
// code. In an older lake (the column was added in place) a repo not
// consumed since says nothing: none stays a gap.
if !room.edge_column_added_in_place("access") {
ev.missing.retain(|m| m.input != "TOUCHES_TABLE edges");
} else if found.value.edges.is_empty() && ev.missing.iter().all(|m| m.input != "TOUCHES_TABLE edges") {
miss(
&mut ev,
"TOUCHES_TABLE edges",
repo,
"none leaves this code, and the lake was created before mimir-schema 4: either it touches no table or it was last consumed by a producer that did not write the edge — its next consumed commit tells",
);
}
let edges = found
.value
.edges
.iter()
.map(|e| Touch {
from: e.from,
table: mimir_schema::moniker::table_name(room.moniker(e.to)).filter(|t| t != mimir_schema::TABLE_UNRESOLVED),
access: e.access,
count: e.count,
sites: room.sites(e.from, e.to, EdgeKind::TouchesTable).iter().map(|s| (s.file.to_string(), s.start_line)).collect(),
})
.collect();
note(&mut ev, TABLES_LOWER_BOUND);
Ok(Touches { edges, evidence: ev })
}
/// The 🚇 lines' termini: for every gRPC handler of `symbols`, the table(s)
/// NEAREST to it along CALLS + TOUCHES_TABLE, as `(handler fn, table)` —
/// nearest first is all there is: every pair of one handler is at the same
/// (fewest) number of hops, sorted by table name. A handler whose nearest
/// table is named at runtime gets `<unresolved>`.
fn handler_termini(room: &Room, symbols: &[SymbolRow], ev: &mut Evidence) -> Result<Vec<(String, String)>, Refusal> {
let handlers = arch::grpc_handlers_from_symbols(symbols);
if handlers.is_empty() {
return Ok(Vec::new());
}
let kinds = [EdgeKind::Calls, EdgeKind::TouchesTable];
let tables = MaskSpec::kinds(&[nk::TABLE]);
let near = room.min_hops_to(&tables, &kinds, None).map_err(internal)?;
if near.value.is_empty() {
return Ok(Vec::new());
}
let hops: HashMap<u64, u64> = near.value.into_iter().collect();
let is_table = room.mask(&tables).map_err(internal)?.value;
let mut paths = Paths::new(room);
let mut out: Vec<(String, String)> = Vec::new();
let mut reaching = 0usize;
// the handler fns: a symbol node whose `module::name` the classifier took
for o in symbol_nodes(room, None)?.ordinals() {
let v = room.node(*o);
if !matches!(v.kind, Some(nk::FN) | Some(nk::METHOD)) {
continue;
}
let (Some(parent), Some(name)) = (v.parent, v.name) else { continue };
let fq = format!("{}::{name}", paths.of(parent));
if !handlers.contains_key(&fq) {
continue;
}
let Some(k) = hops.get(o) else { continue };
reaching += 1;
let walk = room.reach(&[*o][..], &kinds, Some(*k as u32), None).map_err(internal)?;
let mut names: Vec<String> = walk.value.reached.iter().filter(|t| is_table.contains(**t)).filter_map(|t| room.node(*t).name.map(str::to_string)).collect();
names.sort();
out.extend(names.into_iter().map(|t| (fq.clone(), t)));
}
note(ev, &format!("metro termini: {reaching} of {} gRPC handler(s) reach a warehouse table along CALLS + TOUCHES_TABLE; each ends at its NEAREST table(s)", handlers.len()));
out.sort();
Ok(out)
}
// ------------------------------------------------------------------ boards
/// The architecture board of one repo: the kept classifier over the room's
/// symbol / call rows, with the room's TOUCHES_TABLE edges as the board's
/// Table nodes and reads / writes edges.
fn board(room: &Room, repo: Option<&str>) -> Result<(ArchGraph, Vec<SymbolRow>, Vec<CallEdgeRow>, Evidence), Refusal> {
let (symbols, calls, mut ev) = view_rows(room, repo)?;
let touched = touches(room, repo)?;
let mut paths = Paths::new(room);
let graph = arch::generate(&symbols, &calls, &touched.access_rows(room, &mut paths));
ev.query = format!("architecture board of {}", repo.unwrap_or("every consumed repo"));
absorb(&mut ev, &touched.evidence);
touched.say_unresolved(&mut paths, &mut ev);
settle(&mut ev, graph.nodes.is_empty());
Ok((graph, symbols, calls, ev))
}
/// [`super::arch_graph`].
pub fn arch_graph(reach: &Reach<'_>, repo: &str) -> Result<ArchGraph, Refusal> {
if repo.is_empty() {
return Err(invalid("the architecture board is per repo: `repo` is empty"));
}
let room = open(reach, repo)?;
Ok(board(&room, Some(repo))?.0)
}
/// `Viz.Architecture`: the merged board of every consumed repo + the metro
/// fuel (the symbol / call rows), in the tab's own wire type.
pub fn arch_payload(reach: &Reach<'_>) -> Result<(ArchPayload, Evidence), Refusal> {
let room = open(reach, "")?;
let (graph, symbols, calls, mut ev) = board(&room, None)?;
let access = handler_termini(&room, &symbols, &mut ev)?;
let mut members: BTreeSet<String> = BTreeSet::new();
for o in symbol_nodes(&room, None)?.ordinals() {
if let Some(r) = room.node(*o).repo {
members.insert(r.to_string());
}
}
let mut errors = Vec::new();
let (symbols, calls) = if calls.len() > METRO_FUEL_CAP {
errors.push(format!("metro fuel left out: {} call rows exceed the {METRO_FUEL_CAP} one message carries — the board is complete, the 🚇 lines are not drawn", calls.len()));
miss(&mut ev, "metro fuel", None, format!("{} call rows exceed the wire cap {METRO_FUEL_CAP}", calls.len()));
(Vec::new(), Vec::new())
} else {
(symbols, calls)
};
Ok((
ArchPayload {
graph,
members_with_data: members.into_iter().collect(),
errors,
// `surface_coverage` is written by the autonom gate, which fails closed (gap P1)
coverage: BTreeMap::new(),
symbols,
calls,
metro_fuel_source: "resolved/scip".to_string(),
access,
},
ev,
))
}
fn arch_verb(reach: &Reach<'_>, surface: &str, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
if repo.is_empty() {
return Err(invalid(format!("{surface}: `repo` is empty")));
}
let room = open(reach, repo)?;
let (graph, symbols, _, ev) = board(&room, Some(repo))?;
match surface {
"Arch.Graph" => Ok(envelope(json!({ "repo": repo, "graph": graph }), &ev)),
"Arch.Generate" => Ok(envelope(
json!({
"repo": repo,
"nodes": graph.nodes.len(),
"edges": graph.edges.len(),
"graph": graph,
"svg": graph.to_svg(),
"historized": false,
"note": "the board is derived from the code room on every ask; nothing is generated or stored (its history is the room at an earlier seq: `arch history`)",
}),
&ev,
)),
"Arch.Trace" => {
let entry = text(p, "entrypoint");
let sub = arch::trace_from(&graph, entry);
if sub.nodes.is_empty() {
return Err(invalid(format!("arch trace: no node of the board of `{repo}` matches the entrypoint `{entry}` ({} nodes on the board)", graph.nodes.len())));
}
Ok(envelope(json!({ "repo": repo, "entrypoint": entry, "nodes": sub.nodes.len(), "edges": sub.edges.len(), "graph": sub, "svg": sub.to_svg() }), &ev))
}
_ => {
let symbol = text(p, "symbol");
let locs = arch::resolve_symbol(&symbols, symbol);
let mut ev = ev;
ev.query = format!("source location of `{symbol}`");
settle(&mut ev, locs.is_empty());
Ok(envelope(json!({ "repo": repo, "symbol": symbol, "locations": locs }), &ev))
}
}
}
/// `Arch.History`: the board at the last consumer ordinals, newest first.
fn arch_history(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
if repo.is_empty() {
return Err(invalid("arch history: `repo` is empty"));
}
let handle = reach.code_room()?;
let head = open_in(handle, repo)?;
let top = head.scope().repos.get(repo).copied().unwrap_or(0);
let depth = limit_of(p, 5).clamp(1, 20) as i64;
let mut rows = Vec::new();
let mut newer: Option<ArchGraph> = None;
let mut ev = Evidence::default();
for seq in ((top - depth + 1).max(1)..=top).rev() {
let room = handle.room(&Scope::repo(repo, Some(seq)))?;
let (graph, _, _, e) = board(&room, Some(repo))?;
if seq == top {
ev = e;
}
if let (Some(n), Some(last)) = (&newer, rows.last_mut()) {
let (old_n, new_n): (BTreeSet<&str>, BTreeSet<&str>) = (graph.nodes.iter().map(|x| x.id.as_str()).collect(), n.nodes.iter().map(|x| x.id.as_str()).collect());
let last: &mut Value = last;
last["added_nodes"] = json!(new_n.difference(&old_n).collect::<Vec<_>>());
last["removed_nodes"] = json!(old_n.difference(&new_n).collect::<Vec<_>>());
last["edges_delta"] = json!(n.edges.len() as i64 - graph.edges.len() as i64);
}
rows.push(json!({ "seq": seq, "sha": room.scope().offset_shas.get(repo), "nodes": graph.nodes.len(), "edges": graph.edges.len() }));
newer = Some(graph);
}
ev.query = format!("architecture board of `{repo}` at its last {} consumer ordinal(s)", rows.len());
note(&mut ev, "each row is the board of the room at that seq; `added_nodes` / `removed_nodes` / `edges_delta` compare it with the seq before");
settle(&mut ev, rows.is_empty());
Ok(envelope(json!({ "repo": repo, "history": rows }), &ev))
}
/// 30-day heat per (repo, crate): the commits of the last 30 days on the
/// crate's HOTTEST file — each repo's newest `git_heat_facts` scan joined on
/// the files the crate's items lie in — plus the repos that have a scan, and
/// why the table was not read when it was not.
fn crate_heat(reach: &Reach<'_>, room: &Room) -> (HashMap<(String, String), i64>, BTreeSet<String>, Vec<String>) {
use crate::mimir_lens::room_feed::{Basis, Warehouse};
let mut basis = Basis::default();
let scan = Warehouse::of(reach).latest(&mut basis, "git_heat_facts");
let c = scan.cursor(&["repo", "file", "commits_30d"]);
let mut per_file: HashMap<(&str, &str), i64> = HashMap::new();
let mut scanned = BTreeSet::new();
for r in c.rows() {
scanned.insert(c.get(r, 0).to_string());
per_file.insert((c.get(r, 0), c.get(r, 1)), c.get(r, 2).parse().unwrap_or(0));
}
let mut heat: HashMap<(String, String), i64> = HashMap::new();
for f in room.files() {
let Some(commits) = per_file.get(&(f.repo, f.path)) else { continue };
// a file belongs to the crate of the items in it
let Some(krate) = room.nodes_in_file(Some(f.repo), f.path).first().and_then(|o| room.node(*o).krate) else { continue };
let slot = heat.entry((f.repo.to_string(), krate.to_string())).or_default();
*slot = (*slot).max(*commits);
}
(heat, scanned, basis.gaps)
}
/// `Viz.Knowledge`: one bubble per local crate, one row per consumed repo.
pub fn knowledge_summary(reach: &Reach<'_>) -> Result<(KnowledgeSummary, Evidence), Refusal> {
let room = open(reach, "")?;
let q = room.quotient(GroupBy::Crate, &[EdgeKind::Calls], None).map_err(internal)?;
let mut ev = q.evidence;
let (heat, heat_scanned, heat_unread) = crate_heat(reach, &room);
let mut crates: Vec<Bubble> = Vec::new();
let mut per_repo: BTreeMap<String, RepoScanSummary> =
room.scope().repos.keys().map(|r| (r.clone(), RepoScanSummary { repo: r.clone(), ok: true, symbols: 0, calls: 0, features: 0, git_files: 0, error: None })).collect();
for f in room.files() {
if let Some(r) = per_repo.get_mut(f.repo) {
r.git_files += 1;
}
}
for g in &q.value.groups {
// a local crate: its crate node is a local definition of a repo in scope
let Some(node) = g.node else { continue };
let v = room.node(node);
let (Some(repo), true, Some(Origin::Local)) = (v.repo, v.defined, v.origin) else { continue };
let Some(summary) = per_repo.get_mut(repo) else { continue };
let symbols = g.node_kinds.iter().filter(|(k, _)| SYMBOL_KINDS.contains(&k.as_str())).map(|(_, n)| *n).sum::<u64>() as usize;
let gates = room
.mask(&MaskSpec { crates: vec![g.key.clone()], with_cfg: true, defined_only: true, ..Default::default() })
.map_err(internal)?
.value
.len();
let calls = (g.internal + g.fan_out) as usize;
summary.symbols += symbols;
summary.calls += calls;
summary.features += gates;
let heat_30d = v.krate.and_then(|k| heat.get(&(repo.to_string(), k.to_string()))).copied().unwrap_or(0);
crates.push(Bubble { repo: repo.to_string(), krate: g.label.clone(), symbols, calls, files: g.files as usize, gates, heat_30d });
}
for (r, s) in per_repo.iter_mut() {
if s.symbols == 0 {
s.ok = false;
s.error = Some(format!("`{r}` is consumed, and the room holds no local crate of it (nothing compiled at its offset?)"));
}
}
// crate → crate between LOCAL definitions, with the names that justify it
let local = room.mask(&MaskSpec { origins: vec![Origin::Local], defined_only: true, ..Default::default() }).map_err(internal)?.value;
let cross = room.edges(&EdgeSel::kinds(&[EdgeKind::Calls, EdgeKind::UsesType]).inside(&local)).map_err(internal)?;
let mut acc: BTreeMap<(String, String), (u64, BTreeSet<String>)> = BTreeMap::new();
for e in &cross.value.edges {
let (from, to) = (room.node(e.from), room.node(e.to));
let (Some(a), Some(b)) = (from.krate, to.krate) else { continue };
if a == b {
continue;
}
let slot = acc.entry((a.to_string(), b.to_string())).or_default();
slot.0 += 1;
if slot.1.len() < 6 && let Some(n) = to.name {
slot.1.insert(n.to_string());
}
}
let crate_edges: Vec<CrateEdge> = acc.into_iter().map(|((from, to), (edges, via))| CrateEdge { from, to, edges, via: via.into_iter().collect() }).collect();
ev.query = "knowledge map: one bubble per local crate".into();
// heat 0 is "cold" only where a scan of the repo was read
if heat_scanned.is_empty() {
let why = heat_unread.first().cloned().unwrap_or_else(|| {
"`git_heat_facts` holds no row: the consumer's `git_heat` feed (one scan per consumed (repo, sha)) has not run for this workspace yet".to_string()
});
miss(&mut ev, "git_heat_facts", None, format!("{why} — every bubble's heat is 0, not cold"));
} else {
for r in per_repo.keys().filter(|r| !heat_scanned.contains(*r)) {
miss(&mut ev, "git_heat_facts", Some(r), "no git-heat scan of this repo (its `git_heat` feed has not run yet): its bubbles' heat is 0, not cold");
}
}
note(&mut ev, "symbols = item-level definitions; calls = CALLS edges leaving the crate's nodes (to anything); files = files its spans lie in; gates = items with a cfg; heat_30d = commits of the last 30 days on the crate's hottest file (the repo's newest git-heat scan)");
settle(&mut ev, crates.is_empty());
Ok((KnowledgeSummary { repos: per_repo.into_values().collect(), crates, crate_edges }, ev))
}
/// The call graph of a repo for the 3D lens: nodes with provenance, CALLS
/// edges between the repo's own definitions, heaviest first, capped.
pub fn call_graph(reach: &Reach<'_>, repo: &str, cap: usize) -> Result<(Value, Evidence), Refusal> {
let room = open(reach, repo)?;
let own = room
.mask(&MaskSpec { repos: if repo.is_empty() { vec![] } else { vec![repo.to_string()] }, origins: vec![Origin::Local], defined_only: true, node_kinds: vec![nk::FN.into(), nk::METHOD.into()], ..Default::default() })
.map_err(internal)?
.value;
let found = room.edges(&EdgeSel { heaviest_first: true, ..EdgeSel::kinds(&[EdgeKind::Calls]).inside(&own).cap(cap) }).map_err(internal)?;
let mut paths = Paths::new(&room);
let mut ids: BTreeMap<u64, usize> = BTreeMap::new();
let mut nodes: Vec<Value> = Vec::new();
let mut index = |o: u64, nodes: &mut Vec<Value>, paths: &mut Paths<'_>| -> usize {
*ids.entry(o).or_insert_with(|| {
let v = room.node(o);
nodes.push(json!({
"id": paths.of(o),
"moniker": v.moniker,
"crate": v.krate,
"kind": v.kind,
"provenance": v.provenance.map(|p| p.as_str()),
"file": v.file,
"line": v.start_line,
}));
nodes.len() - 1
})
};
let edges: Vec<Value> = found
.value
.edges
.iter()
.map(|e| {
let (f, t) = (index(e.from, &mut nodes, &mut paths), index(e.to, &mut nodes, &mut paths));
json!({ "from": f, "to": t, "sites": e.count })
})
.collect();
let mut ev = found.evidence;
ev.query = format!("call graph of {} (fns and methods, heaviest {cap} edges)", if repo.is_empty() { "every consumed repo" } else { repo });
note(&mut ev, LOWER_BOUND);
settle(&mut ev, edges.is_empty());
Ok((obj([("nodes", Value::Array(nodes)), ("edges", Value::Array(edges)), ("total_edges", json!(found.value.total))]), ev))
}
/// The repo × repo quotient of CALLS ∪ USES_TYPE: the release tab's deep edges.
pub fn repo_edges(reach: &Reach<'_>) -> Result<(Vec<(String, String, u64)>, Evidence), Refusal> {
let room = open(reach, "")?;
let q = room.quotient(GroupBy::Repo, &[EdgeKind::Calls, EdgeKind::UsesType], None).map_err(internal)?;
let edges: Vec<(String, String, u64)> = q.value.edges.iter().map(|e| (q.value.groups[e.from].key.clone(), q.value.groups[e.to].key.clone(), e.edges)).collect();
let mut ev = q.evidence;
ev.query = "repo × repo edges (CALLS ∪ USES_TYPE)".into();
note(&mut ev, "an edge A → B: a definition of repo A calls or uses a type that repo B's rows define — counted at item level, in the consumed commits");
note(&mut ev, LOWER_BOUND);
settle(&mut ev, edges.is_empty());
Ok((edges, ev))
}
/// [`super::api_surface`].
pub fn api_surface(reach: &Reach<'_>, repo: &str) -> Result<super::ApiSurface, Refusal> {
if repo.is_empty() {
return Err(invalid("the API surface is per repo: `repo` is empty"));
}
let room = open(reach, repo)?;
if !room.has_attr("effective_visibility") {
return Err(not_wired(format!(
"the log of `{repo}` carries no visibility (written before mimir-schema 2): which fn is public API cannot be told — re-consume it with the current driver"
)));
}
let fns = vec![nk::FN.to_string(), nk::METHOD.to_string()];
let own = MaskSpec { repos: vec![repo.to_string()], origins: vec![Origin::Local], defined_only: true, ..Default::default() };
let tests = room.mask(&MaskSpec { is_test: Some(true), ..own.clone() }).map_err(internal)?.value;
let api = room.mask(&MaskSpec { node_kinds: fns, public_api: Some(true), is_test: Some(false), ..own.clone() }).map_err(internal)?;
let mut paths = Paths::new(&room);
let mut out = super::ApiSurface::default();
for (set, is_test) in [(&tests, true), (&api.value, false)] {
for o in set.ordinals() {
if let Some(name) = room.node(*o).name {
out.fns.push(super::ApiFn { name: name.to_string(), path: paths.of(*o), is_test });
}
}
}
let local = room.mask(&own).map_err(internal)?.value;
let edges = room.edges(&EdgeSel { from: Some(&local), ..EdgeSel::kinds(&[EdgeKind::Calls, EdgeKind::Tests]) }).map_err(internal)?;
let mut calls: BTreeSet<(String, String)> = BTreeSet::new();
for e in &edges.value.edges {
if let (Some(a), Some(b)) = (room.node(e.from).name, room.node(e.to).name) {
calls.insert((a.to_string(), b.to_string()));
}
}
out.calls = calls.into_iter().collect();
out.notes = edges.evidence.missing.iter().map(|m| format!("missing {}: {}", m.input, m.why)).chain([LOWER_BOUND.to_string()]).collect();
Ok(out)
}
// ------------------------------------------------------------------ templates
const TEMPLATES: [(&str, &str, &str); 4] = [
("def-use", "a symbol's definition and every site that uses it (one hop, every edge kind)", "repo, symbol"),
("change-radius", "the symbols defined in a file and everything that must be rechecked when they change", "repo, file"),
("call-chain", "the direct callers and callees of a symbol", "repo, symbol"),
("table-access-lineage", "which functions read / write a warehouse table", "table"),
];
fn sql_template(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let template = text(p, "template");
let params = p.get("params").cloned().unwrap_or(Value::Null);
if template.is_empty() {
return Ok(json!({
"templates": TEMPLATES.iter().map(|(n, d, ps)| json!({
"name": n, "description": d, "params": ps,
"state": "code_room",
})).collect::<Vec<_>>(),
"note": "the templates are answered by the code room, not by SQL over the removed tables",
}));
}
if p.get("render_only").and_then(Value::as_bool).unwrap_or(false) {
return Ok(json!({ "template": template, "sql": null, "note": "there is no SQL to render: this template is a question to the code room (the tables it used to SELECT from are gone)" }));
}
let repo = text(¶ms, "repo");
match template {
"table-access-lineage" => table_lineage(reach, repo, text(¶ms, "table")),
"call-chain" => {
let room = open(reach, repo)?;
let symbol = text(¶ms, "symbol");
let (callers, mut ev) = one_hop(&room, Dir::Callers, symbol, None, 500)?;
let (callees, ev2) = one_hop(&room, Dir::Callees, symbol, None, 500)?;
absorb(&mut ev, &ev2);
let mut paths = Paths::new(&room);
let (a, b) = (edge_rows(&room, &mut paths, &callers), edge_rows(&room, &mut paths, &callees));
ev.query = format!("call-chain of `{symbol}`");
settle(&mut ev, a.is_empty() && b.is_empty());
Ok(envelope(json!({ "template": template, "callers": a, "callees": b }), &ev))
}
"def-use" => {
let room = open(reach, repo)?;
let symbol = text(¶ms, "symbol");
let (defs, lookup_ev) = resolve(&room, symbol, "symbol")?;
let seed = room.mask_of(&defs);
let kinds: Vec<EdgeKind> = EdgeKind::ALL.iter().copied().filter(|k| *k != EdgeKind::Contains && *k != EdgeKind::DependsOn).collect();
let uses = room.edges(&EdgeSel { to: Some(&seed), cap: Some(1000), ..EdgeSel::kinds(&kinds) }).map_err(internal)?;
let traits = impl_traits(&room)?;
let mut paths = Paths::new(&room);
let definitions: Vec<Value> = defs.iter().map(|o| symbol_json(room.node(*o).repo, &symbol_row(&room, &mut paths, &traits, *o))).collect();
let rows: Vec<Value> = uses
.value
.edges
.iter()
.map(|e| {
let sites = room.sites(e.from, e.to, e.kind);
json!({ "kind": e.kind.as_str(), "user": paths.of(e.from), "used": paths.of(e.to), "sites": e.count, "file": sites.first().map(|s| s.file), "lines": sites.iter().map(|s| s.start_line).collect::<Vec<_>>() })
})
.collect();
let mut ev = uses.evidence;
// kinds no producer writes are not what this template is missing
ev.missing.retain(|m| !m.input.ends_with(" edges") || rows.is_empty());
absorb(&mut ev, &lookup_ev);
ev.query = format!("def-use of `{symbol}`");
note(&mut ev, LOWER_BOUND);
settle(&mut ev, rows.is_empty());
Ok(envelope(json!({ "template": template, "definitions": definitions, "uses": rows }), &ev))
}
"change-radius" => {
let room = open(reach, repo)?;
let file = text(¶ms, "file");
let changed: Vec<u64> = room.files().iter().filter(|f| f.path.ends_with(file) && !file.is_empty()).flat_map(|f| room.nodes_in_file(Some(f.repo), f.path)).collect();
if changed.is_empty() {
return Err(invalid(format!("change-radius: no span of the room [{}] lies in a file ending with `{file}`", room.scope())));
}
let walk = room.dependents(&changed, None).map_err(internal)?;
let mut paths = Paths::new(&room);
let place = |o: u64, paths: &mut Paths<'_>| {
let v = room.node(o);
json!({ "symbol": paths.of(o), "kind": v.kind, "file": v.file, "line": v.start_line })
};
let symbols: Vec<Value> = changed.iter().filter(|o| SYMBOL_KINDS.contains(&kind_of(&room, **o))).map(|o| place(*o, &mut paths)).collect();
let affected: Vec<Value> = walk.value.reached.iter().filter(|o| !changed.contains(o) && SYMBOL_KINDS.contains(&kind_of(&room, **o))).take(2000).map(|o| place(*o, &mut paths)).collect();
let mut ev = walk.evidence;
ev.query = format!("change-radius of `{file}`");
note(&mut ev, "everything that (transitively) calls, uses, implements or is an impl for a symbol of the file — the Jikes walk, not one hop");
note(&mut ev, LOWER_BOUND);
settle(&mut ev, affected.is_empty());
Ok(envelope(json!({ "template": template, "symbols": symbols, "affected": affected }), &ev))
}
other => Err(invalid(format!("unknown template `{other}` (def-use | change-radius | call-chain | table-access-lineage)"))),
}
}
/// `table-access-lineage`: the fns that read / write `table` directly (one
/// row per site), and every fn that reaches one of them along CALLS. `repo`
/// narrows the code looked at; the table is the same node for every repo.
fn table_lineage(reach: &Reach<'_>, repo: &str, table: &str) -> Result<Value, Refusal> {
let room = open(reach, repo)?;
let touched = touches(&room, (!repo.is_empty()).then_some(repo))?;
let known = touched.tables();
let runtime = table == mimir_schema::TABLE_UNRESOLVED;
if table.is_empty() || !(runtime || known.contains(table)) {
// a name the code does not touch is not "no reader": say which it does
let unresolved: u32 = touched.unresolved().map(|t| t.count).sum();
return Err(invalid(format!(
"table-access-lineage: {} — the code of [{}] touches {} table(s) by name: {}{}",
if table.is_empty() { "`table` is empty".to_string() } else { format!("no fn touches a table named `{table}`") },
room.scope(),
known.len(),
if known.is_empty() { "(none)".to_string() } else { known.iter().copied().collect::<Vec<_>>().join(", ") },
if unresolved > 0 { format!("; {unresolved} more site(s) name their table at runtime (ask for `{}`)", mimir_schema::TABLE_UNRESOLVED) } else { String::new() },
)));
}
let mut paths = Paths::new(&room);
let mine: Vec<&Touch> = touched.edges.iter().filter(|t| if runtime { t.table.is_none() } else { t.table.as_deref() == Some(table) }).collect();
let mut rows: Vec<Value> = Vec::new();
for t in &mine {
let v = room.node(t.from);
let row = |file: &str, line: u32| {
json!({
"caller_fn": room.display_path(t.from),
"crate_name": v.krate,
"repo": v.repo,
"table": table,
"access": t.access.map(|a| a.as_str()),
"file": file,
"line": line,
"moniker": v.moniker,
})
};
if t.sites.is_empty() {
rows.push(row(v.file.unwrap_or(""), 0));
}
rows.extend(t.sites.iter().map(|(f, l)| row(f, *l)));
}
let direct: Vec<u64> = mine.iter().map(|t| t.from).collect::<BTreeSet<_>>().into_iter().collect();
let walk = room.dependents_by(&direct, &[EdgeKind::Calls], None, None).map_err(internal)?;
const SHOW: usize = 500;
let reachers: Vec<String> = walk.value.reached.iter().take(SHOW).map(|o| paths.of(*o)).collect();
let mut ev = touched.evidence.clone();
absorb(&mut ev, &walk.evidence);
ev.query = format!("table-access-lineage of `{table}`");
if !runtime {
touched.say_unresolved(&mut paths, &mut ev);
}
note(&mut ev, LOWER_BOUND);
settle(&mut ev, rows.is_empty());
Ok(envelope(
json!({
"template": "table-access-lineage",
"table": table,
"rows": rows,
"readers": mine.iter().filter(|t| t.access.is_none_or(|a| a.reads())).count(),
"writers": mine.iter().filter(|t| t.access.is_none_or(|a| a.writes())).count(),
"sites": mine.iter().map(|t| t.count).sum::<u32>(),
"reached_from": reachers,
"reached_from_total": walk.value.reached.len(),
}),
&ev,
))
}
// ------------------------------------------------------------------ the rest
/// `repo@seq` → (repo, seq).
fn repo_at(spec: &str) -> Option<(&str, i64)> {
let (repo, seq) = spec.rsplit_once('@')?;
Some((repo, seq.parse().ok()?)).filter(|(r, _)| !r.is_empty())
}
fn snapshot_diff(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let (mut a, mut b) = (text(p, "snap_a").to_string(), text(p, "snap_b").to_string());
let mut first_commit: Option<String> = None;
if a.is_empty() && b.is_empty() {
// no states named: the last two consumer ordinals of `repo`
(a, b, first_commit) = panes::last_two(reach.code_room()?, text(p, "repo"))?;
}
let (Some((ra, sa)), Some((rb, sb))) = (repo_at(&a), repo_at(&b)) else {
return Err(invalid(format!(
"snapshot-diff takes two states of the code room as `repo@seq` (consumer ordinals), got `{a}` and `{b}`: \
the world-ring snapshot ids of the removed coderoom ingest have no counterpart"
)));
};
let handle = reach.code_room()?;
let older = handle.room(&Scope::repo(ra, Some(sa)))?;
let newer = handle.room(&Scope::repo(rb, Some(sb)))?;
let d = older.diff(&newer).map_err(internal)?;
const SHOW: usize = 200;
let cut = |v: &[String]| v.iter().take(SHOW).cloned().collect::<Vec<_>>();
let edges: BTreeMap<String, Value> = d
.value
.edges
.iter()
.map(|(k, e)| (k.as_str().to_string(), json!({ "added": e.added.len(), "removed": e.removed.len(), "added_sample": e.added.iter().take(20).collect::<Vec<_>>(), "removed_sample": e.removed.iter().take(20).collect::<Vec<_>>() })))
.collect();
let mut ev = d.evidence.clone();
if let Some(why) = &first_commit {
note(&mut ev, why);
}
Ok(envelope(
json!({
"from": a, "to": b,
"added_nodes": d.value.added_nodes.len(), "removed_nodes": d.value.removed_nodes.len(), "changed_nodes": d.value.changed.len(),
"added": cut(&d.value.added_nodes), "removed": cut(&d.value.removed_nodes), "changed": cut(&d.value.changed),
"edges": edges,
"provenance_changed": d.value.provenance_changed.len(),
"lists_capped_at": SHOW,
"by_crate": panes::diff_by_crate(&older, &newer, &d.value.added_nodes, &d.value.removed_nodes, &d.value.changed),
"room_from": panes::identity(handle, &older),
"room_to": panes::identity(handle, &newer),
}),
&ev,
))
}
/// Anchors for a task text: the symbols whose names its words name.
fn funnel_retrieve(reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
let repo = text(p, "repo");
let room = open(reach, repo)?;
let query = text(p, "query");
let limit = limit_of(p, 12);
let words: BTreeSet<String> = query.split(|c: char| !(c.is_alphanumeric() || c == '_')).filter(|w| w.len() >= 3).map(str::to_lowercase).collect();
if words.is_empty() {
return Err(invalid("funnel retrieve: `query` holds no word of three letters or more"));
}
let scope = symbol_nodes(&room, (!repo.is_empty()).then_some(repo))?;
let mut score: BTreeMap<u64, u32> = BTreeMap::new();
let mut ev = Evidence::default();
for w in &words {
let found = room.lookup(&NameQuery { within: Some(scope.clone()), ..NameQuery::new(w.as_str()).mode(NameMatch::Substring) }).map_err(internal)?;
for o in &found.value {
*score.entry(*o).or_default() += 1;
}
if ev.scope.is_empty() {
ev = found.evidence;
}
}
let mut ranked: Vec<(u64, u32)> = score.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let total = ranked.len();
ranked.truncate(limit);
let mut paths = Paths::new(&room);
let anchors: Vec<Value> = ranked
.iter()
.map(|(o, hits)| {
let v = room.node(*o);
json!({ "symbol": paths.of(*o), "kind": v.kind, "crate": v.krate, "file": v.file, "line": v.start_line, "words_matched": hits })
})
.collect();
ev.query = format!("anchors for {words:?}");
ev.notes.retain(|n| !n.starts_with("matched as"));
ev.capped = (total > anchors.len()).then_some(mimir_room::Capped { shown: anchors.len() as u64, total: total as u64 });
note(&mut ev, "ranked by how many words of the task a symbol's name contains — a name match, not a semantic one");
settle(&mut ev, anchors.is_empty());
Ok(envelope(json!({ "anchors": anchors }), &ev))
}
/// Every surface, by name.
pub fn answer(surface: &str, reach: &Reach<'_>, p: &Value) -> Result<Value, Refusal> {
// a door with no room is told so before its request is looked at: what it
// lacks is the room, not an argument (the menu of templates needs neither)
let needs_no_room = matches!(surface, "Coderoom.Ingest" | "Coderoom.Compat" | "Coderoom.WhatBreaks") || (surface == "Mimir.SqlTemplate" && text(p, "template").is_empty());
if !needs_no_room {
reach.code_room()?;
}
match surface {
"Introspect.Callers" | "Introspect.Callees" | "Introspect.PathBetween" | "Dwarf.Callers" | "Dwarf.Callees" | "Dwarf.CallPath" => binary_verb(reach, surface, p),
"Knowledge.SymbolLookup" => symbol_lookup(reach, p),
"Knowledge.DefinedIn" => defined_in(reach, p),
"Knowledge.Callers" => knowledge_calls(reach, Dir::Callers, p),
"Knowledge.Callees" => knowledge_calls(reach, Dir::Callees, p),
"Knowledge.CallPath" => call_path(reach, p),
"Knowledge.FeatureGates" => feature_gates(reach, p),
"Knowledge.Context" => context(reach, p),
"Knowledge.Map" => knowledge_block(reach, text(p, "repo")),
"Viz.Knowledge" => {
let (summary, ev) = knowledge_summary(reach)?;
Ok(envelope(serde_json::to_value(&summary).map_err(internal)?, &ev))
}
"Viz.Architecture" => {
let (payload, ev) = arch_payload(reach)?;
Ok(envelope(serde_json::to_value(&payload).map_err(internal)?, &ev))
}
"Viz.CrateGraph" => panes::crate_graph(reach, p),
"Viz.CallGraph" => panes::call_graph_answer(reach, p),
"Viz.FunctionTier" => panes::function_tier(reach, p),
"Viz.CodeRoom3D" => panes::code_room_3d(reach, p),
"Viz.CodeMap" => codemap::code_map(reach, p),
"Viz.Binary" => panes::binary(reach, p),
"Knowledge.Symbols" => panes::symbols(reach, p),
"Knowledge.Source" => panes::source(reach, p),
"Knowledge.TestReach" => panes::test_reach(reach, p),
"Viz.TestMatrix.inventory" => {
let (rows, ev) = test_inventory(reach, text(p, "repo"))?;
Ok(envelope(json!({ "inventory": rows }), &ev))
}
"Test.List" => test_list(reach, p),
"Test.Affected" => affected::affected_tests(reach, p),
"Arch.Generate" | "Arch.Trace" | "Arch.Open" | "Arch.Graph" => arch_verb(reach, surface, p),
"Arch.History" => arch_history(reach, p),
"Mimir.SqlTemplate" => sql_template(reach, p),
// the system boundary view (the `system_boundary` lens): the classes
// of system call a repo's code reaches, and from where
"Mimir.SystemBoundary" => {
let mut v = crate::mimir_lens::answer("system_boundary", reach, p);
// a lens that could not compute is the door's refusal (no room,
// nothing consumed, a wrong class), like every other surface
if let Some(state) = v["lens_meta"]["state"].as_str().and_then(crate::code_room::State::parse) {
let note = v["evidence_note"].as_str().unwrap_or("").trim_start_matches("system_boundary: ").to_string();
return Err(Refusal::new(state, note));
}
v["state"] = v["lens_meta"]["state"].clone();
Ok(v)
}
"Coderoom.SnapshotDiff" => snapshot_diff(reach, p),
// the build board: what the consumer's builds are doing, unit by unit
// (no room is opened)
"Coderoom.Units" => Ok(crate::code_room::units::answer(&reach.code_room()?.layout().clones(), p)),
// builder 2: the built artifacts (no room is opened)
"Coderoom.Artifacts" => crate::code_room::products::answer(reach.code_room()?, p),
"Funnel.Retrieve" => funnel_retrieve(reach, p),
"Coderoom.Ingest" | "Coderoom.Compat" => Err(not_wired(format!(
"{surface}: the world ring (crates.io sources of other crates, COMPATIBLE edges, semver diffs) has no v2 producer — the tarball ingest is gone and \
the driver dumps what a consumed repo compiles, not the registry (gap P5)"
))),
"Coderoom.WhatBreaks" => Err(not_wired(
"Coderoom.WhatBreaks: `breaks` = a PUBLIC item of a crate VERSION that went away or changed in another. The room can diff two states, tell the public API \
(effective visibility) and walk the dependents — but only of commits a consumer walked: the registry versions of other crates (the world ring) are not \
ingested (gap P5), so there are no two versions to diff. For a consumed repo use `coderoom snapshot-diff repo@seq repo@seq`",
)),
"Vector.SlicesIndex" | "Vector.SlicesSearch" => {
let room = open(reach, text(p, "repo"))?;
Err(not_wired(format!(
"{surface}: the room holds {} item spans to root slices on, but the slice index (embeddings over those spans) is the vector lane and is not built yet",
room.stats().spans
)))
}
other => Err(invalid(format!("`{other}` has no reader"))),
}
}
/// The readers behind a thin client's panes.
pub mod panes;
/// `Test.Affected`: the tests a commit can break.
pub mod affected;
/// `Viz.CodeMap`: the room as a map, laid out once per room version.
pub mod codemap;
/// A traced run on the map: traced functions → room functions, and the
/// replay's `Viz.CodeMap` lods (`trace_runs`, `trace`, `profile`).
pub mod trace;
#[cfg(test)]
mod trace_tests;
#[cfg(test)]
mod tests;