1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! `crossing` analysis — the "X" of Kazman & Cai's DV8 hotspot patterns
//! (Mo, Cai, Kazman, Xiao 2015 *Hotspot Patterns*).
//!
//! A **Crossing** is a file that is simultaneously a hub *and* a sink:
//! high fan-in (many files import it) **and** high fan-out (it imports
//! many), whose changes co-occur with **both** the files that depend on
//! it (upstream) **and** the files it depends on (downstream). It is the
//! point where two change-flows cross, coupling its upstream and
//! downstream together *through itself* — the hardest shape to change
//! safely, because edits ripple in both directions at once.
//!
//! ## Fusion — two graphs `CodeLore` already builds
//!
//! - **Structural:** the `imports` table gives, per file, both its
//! importers (in-edges → fan-in) and its imports (out-edges →
//! fan-out).
//! - **Temporal:** [`coupling::run_coupling`](crate::analyses::coupling::run_coupling)
//! Fisher-significant co-change partners.
//!
//! A crossing co-changes with ≥ 1 importer AND ≥ 1 import — both flows
//! are live, not just structurally present.
//!
//! ## Calibration
//!
//! Both fan-in and fan-out must reach [`DEFAULT_MIN_FAN`] (the "X" needs
//! genuine breadth on both axes), and at least one co-change partner must
//! sit on each side. `crossing_score = coupled_upstream +
//! coupled_downstream` ranks by how much change actually flows through
//! the crossing in both directions. Accuracy follows the import
//! resolver's language coverage, same caveat as `god_classes` fan-in.
use std::collections::{HashMap, HashSet};
use crate::facts::FactsDb;
use crate::{Options, Result};
/// Minimum fan-in and fan-out for a file to count as a structural "X".
pub const DEFAULT_MIN_FAN: u32 = 3;
/// One crossing finding.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CrossingRow {
/// The crossing file.
pub path: String,
/// Distinct files importing this one (fan-in / afferent).
pub fan_in: u32,
/// Distinct files this one imports (fan-out / efferent, resolved).
pub fan_out: u32,
/// Importers that also co-change with this file (upstream flow).
pub coupled_upstream: u32,
/// Imports that also co-change with this file (downstream flow).
pub coupled_downstream: u32,
/// `coupled_upstream + coupled_downstream` — total change flowing
/// through the crossing in both directions. Higher = worse.
pub crossing_score: f64,
}
/// Run the `crossing` analysis. Returns crossings ranked by composite
/// crossing score (highest first).
///
/// # Errors
///
/// Returns [`crate::CodeLoreError::Analysis`] on `DuckDB` query errors
/// (propagated from the imports scan or the inner coupling run).
#[tracing::instrument(name = "crossing", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_crossing(db: &FactsDb, opts: &Options) -> Result<Vec<CrossingRow>> {
// Resolved import edges. One pass builds both directions:
// importers[target] = files importing target (in-edges)
// imports[src] = files src imports (out-edges)
let import_pairs: Vec<(String, String)> = crate::analyses::query::query_map_collect(
db,
"SELECT src_path, target_path FROM imports WHERE target_path IS NOT NULL",
[],
"crossing imports",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)?;
if import_pairs.is_empty() {
return Ok(Vec::new());
}
let mut importers: HashMap<String, HashSet<String>> = HashMap::new();
let mut imports: HashMap<String, HashSet<String>> = HashMap::new();
for (src, target) in import_pairs {
importers
.entry(target.clone())
.or_default()
.insert(src.clone());
imports.entry(src).or_default().insert(target);
}
// Fisher-significant co-change partners per file, both directions.
// Memoized inner call; `--rows N` must not cap the partner pool.
let coupling_rows = crate::analyses::coupling::run_coupling(db, &opts.with_no_row_limit())?;
let partners = crate::analyses::coupling::partner_index(&coupling_rows);
let mut out: Vec<CrossingRow> = Vec::new();
for (path, in_set) in &importers {
let fan_in = u32::try_from(in_set.len()).unwrap_or(u32::MAX);
if fan_in < DEFAULT_MIN_FAN {
continue;
}
// Must also import a comparable breadth — the other arm of the X.
let Some(out_set) = imports.get(path) else {
continue;
};
let fan_out = u32::try_from(out_set.len()).unwrap_or(u32::MAX);
if fan_out < DEFAULT_MIN_FAN {
continue;
}
// Change must actually flow on BOTH arms (DV8 requires both).
let Some(pt) = partners.get(path) else {
continue;
};
let coupled_upstream = u32::try_from(in_set.intersection(pt).count()).unwrap_or(u32::MAX);
let coupled_downstream =
u32::try_from(out_set.intersection(pt).count()).unwrap_or(u32::MAX);
if coupled_upstream == 0 || coupled_downstream == 0 {
continue;
}
out.push(CrossingRow {
path: path.clone(),
fan_in,
fan_out,
coupled_upstream,
coupled_downstream,
crossing_score: f64::from(coupled_upstream) + f64::from(coupled_downstream),
});
}
out.sort_by(|a, b| {
b.crossing_score
.partial_cmp(&a.crossing_score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.path.cmp(&b.path))
});
if let Some(limit) = opts.rows_limit {
out.truncate(limit as usize);
}
Ok(out)
}