1use crate::{Analyzer, Result, Snapshot};
2use blazingly_json::Value;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, OnceLock};
6use std::time::{Duration, Instant};
7use weavatrix_graph::{Graph, Node, NodeIndex, weakly_connected_components};
8use weavatrix_scan::ScanReport;
9
10#[derive(Debug, Clone)]
11pub struct RepositoryState {
12 root: PathBuf,
13 snapshot: Snapshot,
14 graph: Arc<Graph>,
15 scan: ScanReport,
16 build_time: Duration,
17 weak_components: Arc<OnceLock<Vec<Vec<NodeIndex>>>>,
18}
19
20impl RepositoryState {
21 pub(crate) fn build(analyzer: &Analyzer, root: impl AsRef<Path>) -> Result<Self> {
22 let started = Instant::now();
23 let (snapshot, scan) = analyzer.analyze_with_report(root)?;
24 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
25 let snapshot_root = PathBuf::from(&snapshot.repository);
26 let root = snapshot_root
27 .canonicalize()
28 .map_err(|source| crate::Error::io(&snapshot_root, source))?;
29 Ok(Self {
30 root,
31 snapshot,
32 graph: Arc::new(graph),
33 scan,
34 build_time: started.elapsed(),
35 weak_components: Arc::new(OnceLock::new()),
36 })
37 }
38
39 fn from_scan(analyzer: &Analyzer, root: &Path, scan: ScanReport) -> Result<Self> {
40 let started = Instant::now();
41 let snapshot = analyzer.analyze_report(root, &scan)?;
42 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
43 Ok(Self {
44 root: root.to_path_buf(),
45 snapshot,
46 graph: Arc::new(graph),
47 scan,
48 build_time: started.elapsed(),
49 weak_components: Arc::new(OnceLock::new()),
50 })
51 }
52
53 #[must_use]
54 pub fn root(&self) -> &Path {
55 &self.root
56 }
57
58 #[must_use]
59 pub const fn snapshot(&self) -> &Snapshot {
60 &self.snapshot
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 const fn scan_report(&self) -> &ScanReport {
75 &self.scan
76 }
77
78 pub(crate) fn weak_components(&self) -> &[Vec<NodeIndex>] {
79 self.weak_components
80 .get_or_init(|| {
81 let mut components = weakly_connected_components(self.graph.as_ref());
82 components.sort_unstable_by_key(|right| std::cmp::Reverse(right.len()));
83 components
84 })
85 .as_slice()
86 }
87
88 #[cfg(feature = "mcp")]
89 pub(crate) fn prime_weak_components(&self) {
90 if self.weak_components.get().is_some() {
91 return;
92 }
93 let graph = Arc::clone(&self.graph);
94 let destination = Arc::clone(&self.weak_components);
95 std::thread::spawn(move || {
96 std::thread::sleep(Duration::from_millis(10));
100 destination.get_or_init(|| {
101 let mut components = weakly_connected_components(graph.as_ref());
102 components.sort_unstable_by_key(|right| std::cmp::Reverse(right.len()));
103 components
104 });
105 });
106 }
107
108 pub(crate) fn resolve_node(&self, label: &str) -> std::result::Result<NodeIndex, String> {
109 if let Some(index) = self.graph.node_index(label) {
110 return Ok(index);
111 }
112 let matches = self
113 .graph
114 .nodes()
115 .iter()
116 .enumerate()
117 .filter(|(_, node)| node.label == label)
118 .collect::<Vec<_>>();
119 match matches.as_slice() {
120 [] => Err(format!("node not found: {label}")),
121 [(index, _)] => Ok(NodeIndex::new(
122 u32::try_from(*index).map_err(|_| "node index overflow")?,
123 )),
124 _ => Err(format!(
125 "ambiguous node label {label:?}; use one of: {}",
126 matches
127 .iter()
128 .take(8)
129 .map(|(_, node)| node.id.as_str())
130 .collect::<Vec<_>>()
131 .join(", ")
132 )),
133 }
134 }
135
136 pub(crate) fn node(&self, index: NodeIndex) -> std::result::Result<&Node, String> {
137 self.graph
138 .node_at(index)
139 .ok_or_else(|| format!("node index out of range: {}", index.index()))
140 }
141}
142
143pub struct Weavatrix {
144 analyzer: Analyzer,
145 state: RepositoryState,
146 known_states: BTreeMap<PathBuf, RepositoryState>,
147 tool_cache: BTreeMap<String, Value>,
148}
149
150impl Weavatrix {
151 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
157 let analyzer = Analyzer::default();
158 let state = RepositoryState::build(&analyzer, root)?;
159 let known_states = BTreeMap::from([(state.root.clone(), state.clone())]);
160 Ok(Self {
161 analyzer,
162 state,
163 known_states,
164 tool_cache: BTreeMap::new(),
165 })
166 }
167
168 pub(crate) fn from_state(state: RepositoryState) -> Self {
169 let known_states = BTreeMap::from([(state.root.clone(), state.clone())]);
170 Self {
171 analyzer: Analyzer::default(),
172 state,
173 known_states,
174 tool_cache: BTreeMap::new(),
175 }
176 }
177
178 #[must_use]
179 pub const fn state(&self) -> &RepositoryState {
180 &self.state
181 }
182
183 pub fn rebuild(&mut self) -> Result<()> {
189 self.state = RepositoryState::build(&self.analyzer, &self.state.root)?;
190 self.tool_cache.clear();
191 self.remember_active_state();
192 Ok(())
193 }
194
195 pub fn refresh_if_stale(&mut self) -> Result<bool> {
202 let scan = self
203 .analyzer
204 .scan(&self.state.root, Some(&self.state.scan))?;
205 if scan.revision == self.state.scan.revision {
206 self.state.scan = scan;
207 self.remember_active_state();
208 return Ok(false);
209 }
210 self.state = RepositoryState::from_scan(&self.analyzer, &self.state.root, scan)?;
211 self.tool_cache.clear();
212 self.remember_active_state();
213 Ok(true)
214 }
215
216 pub fn open_repository(&mut self, root: impl AsRef<Path>) -> Result<()> {
222 self.open_repository_with_build(root, true)?;
223 Ok(())
224 }
225
226 pub fn open_repository_with_build(
237 &mut self,
238 root: impl AsRef<Path>,
239 build: bool,
240 ) -> Result<bool> {
241 if build {
242 let state = RepositoryState::build(&self.analyzer, root)?;
243 self.known_states
244 .insert(self.state.root.clone(), self.state.clone());
245 self.state = state;
246 self.tool_cache.clear();
247 self.remember_active_state();
248 return Ok(true);
249 }
250
251 let requested = root
252 .as_ref()
253 .canonicalize()
254 .map_err(|source| crate::Error::io(root.as_ref(), source))?;
255 if requested == self.state.root {
256 return Ok(false);
257 }
258 let cached = self.known_states.get(&requested).cloned().ok_or_else(|| {
259 crate::Error::Analysis(format!(
260 "no in-process graph for {}; call open_repo with build:true first",
261 requested.display()
262 ))
263 })?;
264 self.known_states
265 .insert(self.state.root.clone(), self.state.clone());
266 self.state = cached;
267 self.tool_cache.clear();
268 Ok(false)
269 }
270
271 pub fn known_roots(&self) -> impl Iterator<Item = &Path> {
272 self.known_states.keys().map(PathBuf::as_path)
273 }
274
275 pub(crate) fn ensure_repository_state(&mut self, root: impl AsRef<Path>) -> Result<PathBuf> {
276 let requested = root
277 .as_ref()
278 .canonicalize()
279 .map_err(|source| crate::Error::io(root.as_ref(), source))?;
280 if requested == self.state.root || self.known_states.contains_key(&requested) {
281 return Ok(requested);
282 }
283 let state = RepositoryState::build(&self.analyzer, &requested)?;
284 self.known_states.insert(requested.clone(), state);
285 Ok(requested)
286 }
287
288 pub(crate) fn known_state(&self, root: &Path) -> Option<&RepositoryState> {
289 if root == self.state.root {
290 Some(&self.state)
291 } else {
292 self.known_states.get(root)
293 }
294 }
295
296 pub(crate) fn cached_tool_result(&self, key: &str) -> Option<Value> {
297 self.tool_cache.get(key).cloned()
298 }
299
300 pub(crate) fn remember_tool_result(&mut self, key: String, value: Value) {
301 const MAX_TOOL_CACHE_ENTRIES: usize = 32;
302 if self.tool_cache.len() >= MAX_TOOL_CACHE_ENTRIES {
303 self.tool_cache.clear();
304 }
305 self.tool_cache.insert(key, value);
306 }
307
308 fn remember_active_state(&mut self) {
309 self.known_states
310 .insert(self.state.root.clone(), self.state.clone());
311 }
312}