Skip to main content

devscout_rs/query/
bus.rs

1// `bus-hop` query-layer plumbing: the index-build-time adjacency push
2// (`record_bus_edge`, the one-kind mirror of `dispatch.rs`'s own
3// `record_dispatch_edge`), and the provenance row `refs`/`read` render --
4// publisher, message, handler, evidence, and which side of the edge the
5// queried symbol sits on. `impact`'s own reach comes for free once
6// `bus_hop` joins `inbound_walk_kinds` (`dispatch.rs`) and `why.rs` names
7// `Why::BusHop`; this module owns nothing impact-specific.
8
9use std::collections::{HashMap, HashSet};
10
11use crate::graph;
12
13use super::index::{def_files, GraphIndex, InboundEntry, OutboundEntry};
14use super::refs_tables::{cap_rows, loc_cmp, Table};
15
16/// Which side of a `bus-hop` edge the queried symbol sits on.
17///
18/// `In` when the symbol is the edge's own handler (`to`), `Out` when one of
19/// the symbol's own declaring files is the edge's publish site
20/// (`from_file`). Carried on the row itself, not left to an inbound/outbound
21/// section to imply -- unlike a plain code reference, a bus-hop row names
22/// both ends of the edge regardless of which side the query started from.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum BusDirection {
25    /// A message reaches the queried symbol.
26    In,
27    /// The queried symbol's own file publishes a message a handler
28    /// elsewhere receives.
29    Out,
30}
31
32impl BusDirection {
33    /// The exact word this direction renders as, in text and `--json` alike.
34    pub fn as_str(self) -> &'static str {
35        match self {
36            BusDirection::In => "in",
37            BusDirection::Out => "out",
38        }
39    }
40}
41
42/// One `bus-hop` provenance row.
43///
44/// The publish site (`file`/`line`), the resolved `message`, the handler
45/// (`to`/`to_file`), the evidence word `resolve/bus.rs` established the hop
46/// through -- a pass-through, never respelled here, since that module's own
47/// word list is the one place any of them is spelled -- and which side of
48/// the edge the queried symbol sits on.
49#[derive(Debug, Clone, PartialEq)]
50pub struct BusHopRow {
51    /// The publish site's file.
52    pub file: String,
53    /// The publish site's line.
54    pub line: usize,
55    /// The resolved message def id.
56    pub message: String,
57    /// The handler/consumer def id.
58    pub to: String,
59    /// The handler's declaring file.
60    pub to_file: String,
61    /// The evidence word the hop resolved through.
62    pub evidence: String,
63    /// Which side of the edge the queried symbol sits on.
64    pub direction: BusDirection,
65    /// How many distinct handlers this row's message reaches across the
66    /// whole graph. A message with one handler is a route; a message with
67    /// eighty is a shared contract whose every publisher appears to reach
68    /// all of them, and a reader cannot tell those apart from the rows
69    /// alone. Counted from the edges at query time rather than stored:
70    /// the edges already carry it, so persisting it would only be a second
71    /// copy that can disagree.
72    pub message_handlers: usize,
73}
74
75// Distinct handlers per message, over every `bus-hop` edge in the graph.
76fn handlers_by_message(edges: &[graph::Edge]) -> HashMap<&str, HashSet<&str>> {
77    let mut out: HashMap<&str, HashSet<&str>> = HashMap::new();
78    for e in edges {
79        if let graph::Edge::BusHop { message, to, .. } = e {
80            out.entry(message.as_str()).or_default().insert(to.as_str());
81        }
82    }
83    out
84}
85
86fn bus_hop_row(
87    e: &graph::Edge,
88    direction: BusDirection,
89    handlers: &HashMap<&str, HashSet<&str>>,
90) -> BusHopRow {
91    let graph::Edge::BusHop {
92        from_file,
93        from_line,
94        message,
95        to,
96        to_file,
97        evidence,
98    } = e
99    else {
100        unreachable!("bus_hop_row is only ever called with a BusHop edge index");
101    };
102    BusHopRow {
103        file: from_file.clone(),
104        line: *from_line,
105        message: message.clone(),
106        to: to.clone(),
107        to_file: to_file.clone(),
108        evidence: evidence.clone(),
109        direction,
110        message_handlers: handlers.get(message.as_str()).map_or(0, HashSet::len),
111    }
112}
113
114/// Pushes edge `i` into both `outbound_by_file[from_file]` and
115/// `inbound[to]`'s `bus_hop` field -- the one-kind mirror of
116/// `dispatch::record_dispatch_edge`.
117pub(super) fn record_bus_edge(
118    i: usize,
119    from_file: &str,
120    to: &str,
121    outbound_by_file: &mut HashMap<String, OutboundEntry>,
122    inbound: &mut HashMap<String, InboundEntry>,
123) {
124    outbound_by_file
125        .entry(from_file.to_string())
126        .or_default()
127        .bus_hop
128        .push(i);
129    inbound.entry(to.to_string()).or_default().bus_hop.push(i);
130}
131
132/// An empty `bus-hop` table for a member seed, which has none of its own --
133/// bus-hop provenance is a TYPE-level fact (a consumer base, a publish
134/// site's own message), never a member's.
135pub(super) fn empty_bus_table() -> Table<BusHopRow> {
136    Table {
137        total: 0,
138        dropped: 0,
139        rows: Vec::new(),
140    }
141}
142
143/// Every `bus-hop` row for one resolved symbol: inbound (the symbol is the
144/// handler) then outbound (one of the symbol's own files is a publish
145/// site), each sorted by publish-site location, capped by `cap`. Never
146/// ranked against the ordinary inbound/outbound budgets -- a bus-hop row is
147/// a structural fact about a cross-cutting wire-up, not a usage call site.
148pub(super) fn symbol_bus_rows(index: &GraphIndex, def_id: &str, cap: usize) -> Table<BusHopRow> {
149    let edges = &index.graph.edges;
150    let mut inbound: Vec<usize> = index
151        .inbound
152        .get(def_id)
153        .map(|e| e.bus_hop.clone())
154        .unwrap_or_default();
155    inbound.sort_by(|&a, &b| loc_cmp(&edges[a], &edges[b]));
156    let mut outbound: Vec<usize> = Vec::new();
157    for file in def_files(index, def_id) {
158        if let Some(o) = index.outbound_by_file.get(&file) {
159            outbound.extend(o.bus_hop.iter().copied());
160        }
161    }
162    outbound.sort_by(|&a, &b| loc_cmp(&edges[a], &edges[b]));
163    let handlers = handlers_by_message(edges);
164    let combined: Vec<BusHopRow> = inbound
165        .iter()
166        .map(|&e| bus_hop_row(&edges[e], BusDirection::In, &handlers))
167        .chain(
168            outbound
169                .iter()
170                .map(|&e| bus_hop_row(&edges[e], BusDirection::Out, &handlers)),
171        )
172        .collect();
173    let total = combined.len();
174    let (rows, dropped) = cap_rows(combined, cap);
175    Table {
176        total,
177        dropped,
178        rows,
179    }
180}