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}