Skip to main content

_diffctx/edges/structural/
containment.rs

1use std::path::Path;
2
3use rustc_hash::FxHashMap;
4
5use crate::config::weights::EDGE_WEIGHTS;
6use crate::types::Fragment;
7
8use super::super::EdgeDict;
9use super::super::base::{EdgeBuilder, add_edge};
10
11pub struct ContainmentEdgeBuilder;
12
13impl EdgeBuilder for ContainmentEdgeBuilder {
14    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
15        let weight = EDGE_WEIGHTS["containment"].forward;
16        let reverse_factor = EDGE_WEIGHTS["containment"].reverse_factor;
17
18        let mut by_path: FxHashMap<&str, Vec<&Fragment>> = FxHashMap::default();
19        for f in fragments {
20            by_path.entry(f.path()).or_default().push(f);
21        }
22
23        let mut edges: EdgeDict = FxHashMap::default();
24
25        // Computed only for the experimental star below — OFF by default, and
26        // measured net-negative when ON (#208): 21 corpus regressions vs 14
27        // improvements, 2026-08-16 A/B at shipped defaults.
28        let star_on = std::env::var_os("DIFFCTX_FILE_STAR").is_some();
29        let reps = if star_on {
30            super::super::base::file_representatives(fragments)
31        } else {
32            FxHashMap::default()
33        };
34
35        for (path, frags) in &by_path {
36            if frags.len() < 2 {
37                continue;
38            }
39            let mut sorted = frags.clone();
40            sorted.sort_by(|a, b| {
41                a.start_line()
42                    .cmp(&b.start_line())
43                    .then(b.end_line().cmp(&a.end_line()))
44            });
45
46            let mut stack: Vec<&Fragment> = Vec::new();
47
48            for f in &sorted {
49                while let Some(top) = stack.last() {
50                    if f.start_line() > top.end_line() {
51                        stack.pop();
52                    } else {
53                        break;
54                    }
55                }
56
57                if let Some(parent) = stack.last() {
58                    if parent.start_line() <= f.start_line()
59                        && f.end_line() <= parent.end_line()
60                        && parent.id != f.id
61                    {
62                        add_edge(&mut edges, &f.id, &parent.id, weight, reverse_factor);
63                    }
64                }
65
66                stack.push(f);
67            }
68
69            // Experimental intra-file spine: connects every fragment to the
70            // file representative so top-level siblings become mutually
71            // reachable. Ships OFF — the 2026-08-16 corpus A/B measured it
72            // net-negative (#208) — so by default a flat file's siblings are
73            // reachable only through nesting and naming edges, and the
74            // representative-only builders accept that.
75            if star_on {
76                if let Some(rep) = reps.get(*path) {
77                    for f in &sorted {
78                        if f.id != *rep {
79                            add_edge(&mut edges, &f.id, rep, weight, reverse_factor);
80                        }
81                    }
82                }
83            }
84        }
85
86        edges
87    }
88}