Skip to main content

devscout_rs/query/
imported.rs

1// Imported-edge reach for `impact`: given the native walk's already-computed
2// result and the validated artifact `import-edges` wrote, names every file
3// reached only through a foreign edge. This is a lookup over `ImpactModel`,
4// not a widening of `impact_walk` itself -- an imported edge reaches a file
5// and stops there, the same rule a heuristic edge already follows, so it
6// never feeds `add_adj` and never enters the next frontier. Kept out of
7// `impact.rs`, which has no room left under its own size ceiling.
8
9use std::collections::{HashMap, HashSet};
10
11use crate::graph;
12
13use super::impact::ImpactModel;
14use super::why::Why;
15
16/// One row of imported-edge reach: a file this graph never resolved an edge
17/// to, named only because a foreign, producer-asserted record points at (or
18/// out of) a file the native walk already reached.
19#[derive(Debug, Clone, PartialEq)]
20pub struct ImportedRow {
21    /// The foreign file this row names.
22    pub file: String,
23    /// The foreign repo id this file belongs to.
24    pub repo: String,
25    /// One more than the local file's own hop -- the same "one hop of
26    /// distance" `impact`'s native hops already mean.
27    pub hop: u32,
28    /// The producer's own kind for the record that reached this row
29    /// (`calls`, `publishes`, `consumes`, or `enqueues`).
30    pub imported_kind: String,
31    /// Always [`Why::ImportedEdge`]; carried on the row so every row this
32    /// module builds already carries the field its consumers read.
33    pub why: Why,
34}
35
36/// The imported half of an `impact` answer: capped and counted apart from
37/// the native rows, so a large import can never evict one of them.
38#[derive(Debug, Clone, PartialEq, Default)]
39pub struct ImportedSection {
40    /// The rows actually shown, ranked hop then repo then file.
41    pub rows: Vec<ImportedRow>,
42    /// How many more distinct foreign files the import named beyond `cap`.
43    pub dropped: usize,
44    /// Every distinct foreign file the import named, capped or not.
45    pub affected: usize,
46}
47
48// Every file the native walk already reached, keyed to its own hop -- a seed
49// file counts as hop 0, so a foreign edge landing on it reports at hop 1, the
50// same "one more hop out" rule a file the walk reached at hop N gives a
51// foreign edge landing there (hop N + 1).
52fn local_hops(model: &ImpactModel) -> HashMap<&str, u32> {
53    let mut hops = HashMap::new();
54    for f in &model.seed_files {
55        hops.entry(f.as_str()).or_insert(0);
56    }
57    for r in &model.rows {
58        hops.entry(r.file.as_str()).or_insert(r.hop);
59    }
60    hops
61}
62
63fn is_mapped(end: &graph::EdgeEnd, mapped_repo: &str) -> bool {
64    end.repo.as_deref() == Some(mapped_repo)
65}
66
67// Direct reach: a record whose `to` end resolves into the mapped repo at a
68// file the walk already reached makes the record's own foreign `from` file a
69// row -- the producer already joined the two sites, so no further widening
70// through it is needed or done.
71fn direct_rows(
72    edges: &graph::ImportedEdges,
73    hops: &HashMap<&str, u32>,
74    seen: &mut HashSet<(String, String)>,
75    out: &mut Vec<ImportedRow>,
76) {
77    for rec in &edges.edges {
78        if is_mapped(&rec.from, &edges.mapped_repo) || !is_mapped(&rec.to, &edges.mapped_repo) {
79            continue;
80        }
81        let Some(to_file) = rec.to.file.as_deref() else {
82            continue;
83        };
84        let Some(&hop) = hops.get(to_file) else {
85            continue;
86        };
87        let (Some(from_repo), Some(from_file)) =
88            (rec.from.repo.as_deref(), rec.from.file.as_deref())
89        else {
90            continue;
91        };
92        if seen.insert((from_repo.to_string(), from_file.to_string())) {
93            out.push(ImportedRow {
94                file: from_file.to_string(),
95                repo: from_repo.to_string(),
96                hop: hop + 1,
97                imported_kind: rec.kind.clone(),
98                why: Why::ImportedEdge,
99            });
100        }
101    }
102}
103
104// Composed reach: a `publishes` record out of the mapped repo, at a file the
105// walk already reached, joined through its message `key` to a `consumes`/
106// `enqueues` record whose `to` end is foreign, makes that foreign consumer a
107// row. Costs one hop, not two -- the message node is not a file -- and never
108// reports the arriving `publishes` record's own site as its own consumer.
109fn composed_rows(
110    edges: &graph::ImportedEdges,
111    hops: &HashMap<&str, u32>,
112    seen: &mut HashSet<(String, String)>,
113    out: &mut Vec<ImportedRow>,
114) {
115    for pub_rec in &edges.edges {
116        if pub_rec.kind != "publishes" || !is_mapped(&pub_rec.from, &edges.mapped_repo) {
117            continue;
118        }
119        let Some(pub_file) = pub_rec.from.file.as_deref() else {
120            continue;
121        };
122        let Some(&hop) = hops.get(pub_file) else {
123            continue;
124        };
125        let Some(msg_key) = pub_rec.key.as_deref() else {
126            continue;
127        };
128        for con_rec in &edges.edges {
129            if con_rec.kind != "consumes" && con_rec.kind != "enqueues" {
130                continue;
131            }
132            if con_rec.key.as_deref() != Some(msg_key) {
133                continue;
134            }
135            if is_mapped(&con_rec.to, &edges.mapped_repo) {
136                continue;
137            }
138            if con_rec.to.file.as_deref() == Some(pub_file) && con_rec.to.line == pub_rec.from.line
139            {
140                continue;
141            }
142            let (Some(con_repo), Some(con_file)) =
143                (con_rec.to.repo.as_deref(), con_rec.to.file.as_deref())
144            else {
145                continue;
146            };
147            if seen.insert((con_repo.to_string(), con_file.to_string())) {
148                out.push(ImportedRow {
149                    file: con_file.to_string(),
150                    repo: con_repo.to_string(),
151                    hop: hop + 1,
152                    imported_kind: con_rec.kind.clone(),
153                    why: Why::ImportedEdge,
154                });
155            }
156        }
157    }
158}
159
160/// Builds the imported section of an `impact` answer.
161///
162/// Reads the native model's already-finished result and the validated
163/// artifact. `cap` bounds `rows` the same way
164/// [`super::refs_tables::cap_rows`] bounds the native ones, but
165/// independently -- this count never competes with the native cap.
166pub fn build_imported_section(
167    model: &ImpactModel,
168    edges: &graph::ImportedEdges,
169    cap: usize,
170) -> ImportedSection {
171    let hops = local_hops(model);
172    let mut seen: HashSet<(String, String)> = HashSet::new();
173    let mut rows = Vec::new();
174    direct_rows(edges, &hops, &mut seen, &mut rows);
175    composed_rows(edges, &hops, &mut seen, &mut rows);
176
177    rows.sort_by(|a, b| {
178        a.hop
179            .cmp(&b.hop)
180            .then_with(|| a.repo.cmp(&b.repo))
181            .then_with(|| a.file.cmp(&b.file))
182    });
183
184    let affected = rows.len();
185    let dropped = affected.saturating_sub(cap);
186    rows.truncate(cap);
187    ImportedSection {
188        rows,
189        dropped,
190        affected,
191    }
192}