Builds a file-level directed adjacency map from the code graph. For each file, collects the files it depends on via `calls` and `uses` (imports) edges. Self-edges are excluded. `implements` and `extends` are intentionally **not** followed: the Rust resolver fuzzy-binds `impl Debug for T` and similar to whatever node happens to share the trait's short name, which on real codebases produces long ch
(
&self,
path_prefix: Option<&str>,
)
| 444 | /// When `path_prefix` is `Some`, only files under that prefix are included |
| 445 | /// (both as sources and targets). |
| 446 | pub async fn build_file_adjacency( |
| 447 | &self, |
| 448 | path_prefix: Option<&str>, |
| 449 | ) -> Result<HashMap<String, HashSet<String>>> { |
| 450 | let sql = "SELECT DISTINCT n1.file_path AS src_file, n2.file_path AS tgt_file \ |
| 451 | FROM edges e \ |
| 452 | JOIN nodes n1 ON e.source = n1.id \ |
| 453 | JOIN nodes n2 ON e.target = n2.id \ |
| 454 | WHERE e.kind IN ('calls', 'uses') \ |
| 455 | AND n1.file_path != n2.file_path"; |
| 456 | |
| 457 | let mut rows = |
| 458 | self.db |
| 459 | .conn() |
| 460 | .query(sql, ()) |
| 461 | .await |
| 462 | .map_err(|e| TraceDecayError::Database { |
| 463 | message: format!("failed to query file adjacency: {e}"), |
| 464 | operation: "build_file_adjacency".to_string(), |
| 465 | })?; |
| 466 | |
| 467 | // Normalise the prefix once: ensure it ends with '/'. |
| 468 | let prefix: Option<String> = path_prefix.map(|p| { |
| 469 | if p.ends_with('/') { |
| 470 | p.to_string() |
| 471 | } else { |
| 472 | format!("{p}/") |
| 473 | } |
| 474 | }); |
| 475 | |
| 476 | let mut adj: HashMap<String, HashSet<String>> = HashMap::new(); |
| 477 | |
| 478 | while let Some(row) = rows.next().await.map_err(|e| TraceDecayError::Database { |
| 479 | message: format!("failed to read adjacency row: {e}"), |
| 480 | operation: "build_file_adjacency".to_string(), |
| 481 | })? { |
| 482 | let src: String = row.get(0).unwrap_or_default(); |
| 483 | let tgt: String = row.get(1).unwrap_or_default(); |
| 484 | |
| 485 | if let Some(ref pfx) = prefix { |
| 486 | if !src.starts_with(pfx.as_str()) || !tgt.starts_with(pfx.as_str()) { |
| 487 | continue; |
| 488 | } |
| 489 | } |
| 490 | |
| 491 | adj.entry(src).or_default().insert(tgt); |
| 492 | } |
| 493 | |
| 494 | // Ensure every known file appears as a key (even leaf nodes with no deps). |
| 495 | let all_files = self.db.get_all_files().await?; |
| 496 | for file in all_files { |
| 497 | if let Some(ref pfx) = prefix { |
| 498 | if !file.path.starts_with(pfx.as_str()) { |
| 499 | continue; |
| 500 | } |
| 501 | } |
| 502 | adj.entry(file.path).or_default(); |
| 503 | } |