weavatrix_rust/engine/
repository_state.rs1use super::RepositoryState;
2use crate::analyzer::Analyzer;
3use crate::model::{Error, Result, Snapshot};
4use std::path::Path;
5use std::sync::{Arc, OnceLock};
6use std::time::{Duration, Instant};
7use weavatrix_graph::{EdgeKind, Graph, Node, NodeIndex, NodeKind};
8use weavatrix_scan::ScanReport;
9
10impl RepositoryState {
11 pub(crate) fn build(analyzer: &Analyzer, root: impl AsRef<Path>) -> Result<Self> {
12 let started = Instant::now();
13 let (snapshot, scan) = analyzer.analyze_with_report(root)?;
14 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
15 let snapshot_root = std::path::PathBuf::from(&snapshot.repository);
16 let root = snapshot_root
17 .canonicalize()
18 .map_err(|source| Error::io(&snapshot_root, source))?;
19 Ok(Self {
20 root,
21 snapshot: Arc::new(snapshot),
22 graph: Arc::new(graph),
23 scan: Arc::new(scan),
24 build_time: started.elapsed(),
25 built_at: Instant::now(),
26 weak_components: Arc::new(OnceLock::new()),
27 census: Arc::new(OnceLock::new()),
28 })
29 }
30
31 pub(super) fn from_scan(analyzer: &Analyzer, root: &Path, scan: ScanReport) -> Result<Self> {
32 let started = Instant::now();
33 let snapshot = analyzer.analyze_report(root, &scan)?;
34 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
35 Ok(Self {
36 root: root.to_path_buf(),
37 snapshot: Arc::new(snapshot),
38 graph: Arc::new(graph),
39 scan: Arc::new(scan),
40 build_time: started.elapsed(),
41 built_at: Instant::now(),
42 weak_components: Arc::new(OnceLock::new()),
43 census: Arc::new(OnceLock::new()),
44 })
45 }
46
47 #[must_use]
49 pub fn graph_age_seconds(&self) -> u64 {
50 self.built_at.elapsed().as_secs()
51 }
52
53 #[must_use]
54 pub fn root(&self) -> &Path {
55 &self.root
56 }
57
58 #[must_use]
59 pub fn snapshot(&self) -> &Snapshot {
60 self.snapshot.as_ref()
61 }
62
63 #[must_use]
64 pub fn graph(&self) -> &Graph {
65 self.graph.as_ref()
66 }
67
68 #[must_use]
69 pub const fn build_time(&self) -> Duration {
70 self.build_time
71 }
72
73 #[must_use]
74 pub fn scan_report(&self) -> &ScanReport {
75 self.scan.as_ref()
76 }
77
78 pub(crate) fn census(&self) -> &super::GraphCensus {
79 self.census.get_or_init(|| {
80 let mut kinds = std::collections::BTreeMap::<String, u64>::new();
81 let mut relations = std::collections::BTreeMap::<String, u64>::new();
82 let mut evidence = std::collections::BTreeMap::<String, u64>::new();
83 for node in self.graph.nodes() {
84 *kinds.entry(node.kind.as_str().to_owned()).or_default() += 1;
85 }
86 for edge in self.graph.edges() {
87 *relations.entry(edge.kind.as_str().to_owned()).or_default() += 1;
88 *evidence
89 .entry(edge.provenance.evidence.as_str().to_owned())
90 .or_default() += 1;
91 }
92 super::GraphCensus {
93 kinds,
94 relations,
95 evidence,
96 }
97 })
98 }
99
100 pub(crate) fn coupled_components(&self) -> &[Vec<NodeIndex>] {
101 self.weak_components
102 .get_or_init(|| coupled_components(self.graph.as_ref()))
103 .as_slice()
104 }
105
106 pub fn warm_communities(&self) {
108 if self.weak_components.get().is_some() {
109 return;
110 }
111 let graph = Arc::clone(&self.graph);
112 let destination = Arc::clone(&self.weak_components);
113 std::thread::spawn(move || {
114 std::thread::sleep(Duration::from_millis(10));
115 destination.get_or_init(|| coupled_components(graph.as_ref()));
116 });
117 }
118
119 pub(crate) fn resolve_node(&self, label: &str) -> std::result::Result<NodeIndex, String> {
120 if let Some(index) = self.graph.node_index(label) {
121 return Ok(index);
122 }
123 let matches = self
124 .graph
125 .nodes()
126 .iter()
127 .enumerate()
128 .filter(|(_, node)| node.label == label)
129 .collect::<Vec<_>>();
130 let candidates = prefer_non_test_only(&matches);
131 match candidates.as_slice() {
132 [] => Err(format!("node not found: {label}")),
133 [(index, _)] => Ok(NodeIndex::new(
134 u32::try_from(*index).map_err(|_| "node index overflow")?,
135 )),
136 _ => Err(format!(
137 "ambiguous node label {label:?}; use one of: {}",
138 candidates
139 .iter()
140 .take(8)
141 .map(|(_, node)| node.id.as_str())
142 .collect::<Vec<_>>()
143 .join(", ")
144 )),
145 }
146 }
147
148 pub(crate) fn node(&self, index: NodeIndex) -> std::result::Result<&Node, String> {
149 self.graph
150 .node_at(index)
151 .ok_or_else(|| format!("node index out of range: {}", index.index()))
152 }
153}
154
155fn prefer_non_test_only<'a>(matches: &'a [(usize, &'a Node)]) -> Vec<(usize, &'a Node)> {
156 if matches.len() <= 1 {
157 return matches.to_vec();
158 }
159 let production = matches
160 .iter()
161 .copied()
162 .filter(|(_, node)| {
163 !matches!(
164 node.attributes.get("test_only"),
165 Some(weavatrix_graph::AttributeValue::Bool(true))
166 )
167 })
168 .collect::<Vec<_>>();
169 if production.len() == 1 {
170 production
171 } else if production.is_empty() {
172 matches.to_vec()
173 } else {
174 production
175 }
176}
177
178#[must_use]
182pub fn git_head(root: &Path) -> Option<String> {
183 let mut git_dir = root.join(".git");
184 if git_dir.is_file() {
185 let text = std::fs::read_to_string(&git_dir).ok()?;
186 let relative = text.strip_prefix("gitdir:")?.trim();
187 let candidate = Path::new(relative);
188 git_dir = if candidate.is_absolute() {
189 candidate.to_path_buf()
190 } else {
191 root.join(candidate)
192 };
193 }
194 let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
195 let head = head.trim();
196 let Some(reference) = head.strip_prefix("ref:") else {
197 return Some(head.to_owned());
198 };
199 let reference = reference.trim();
200 let common = std::fs::read_to_string(git_dir.join("commondir"))
201 .map_or_else(|_| git_dir.clone(), |dir| git_dir.join(dir.trim()));
202 for base in [&git_dir, &common] {
203 if let Ok(hash) = std::fs::read_to_string(base.join(reference)) {
204 return Some(hash.trim().to_owned());
205 }
206 }
207 let packed = std::fs::read_to_string(common.join("packed-refs")).ok()?;
208 packed
209 .lines()
210 .filter_map(|line| line.split_once(' '))
211 .find(|(_, name)| *name == reference)
212 .map(|(hash, _)| hash.trim().to_owned())
213}
214
215fn coupled_components(graph: &Graph) -> Vec<Vec<NodeIndex>> {
220 fn find(parent: &mut [usize], node: usize) -> usize {
221 let mut root = node;
222 while parent[root] != root {
223 root = parent[root];
224 }
225 let mut current = node;
226 while parent[current] != root {
227 let next = parent[current];
228 parent[current] = root;
229 current = next;
230 }
231 root
232 }
233 let nodes = graph.nodes();
234 let mut parent = (0..nodes.len()).collect::<Vec<_>>();
235 let is_package = |slot: usize| {
236 nodes
237 .get(slot)
238 .is_some_and(|node| node.kind == NodeKind::Package)
239 };
240 for slot in 0..nodes.len() {
241 let index = NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX));
242 for edge in graph.outgoing_at(index) {
243 if matches!(edge.kind, EdgeKind::Contains | EdgeKind::Method) {
244 continue;
245 }
246 let Some(target) = graph.node_index(edge.target.as_str()) else {
247 continue;
248 };
249 if is_package(slot) || is_package(target.index()) {
250 continue;
251 }
252 let left = find(&mut parent, slot);
253 let right = find(&mut parent, target.index());
254 if left != right {
255 parent[left.max(right)] = left.min(right);
256 }
257 }
258 }
259 let mut groups = std::collections::BTreeMap::<usize, Vec<NodeIndex>>::new();
260 for slot in 0..nodes.len() {
261 let root = find(&mut parent, slot);
262 groups
263 .entry(root)
264 .or_default()
265 .push(NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX)));
266 }
267 let mut components = groups
268 .into_values()
269 .filter(|members| members.len() > 1)
270 .collect::<Vec<_>>();
271 components.sort_by_key(|members| {
272 (
273 std::cmp::Reverse(members.len()),
274 members.first().map_or(0, |index| index.index()),
275 )
276 });
277 components
278}