1pub mod archetype;
7pub mod boundaries;
8pub mod clustering;
9pub mod cochange;
10pub mod components;
11pub mod flowgraph;
12pub mod flows;
13pub mod impact;
14pub mod invariants;
15pub mod lifecycle;
16pub mod state;
17pub mod trust;
18pub mod workflow;
19
20pub use trust::{TrustedGraphView, TrustPolicy};
21
22impl RealityGraph {
24 pub fn empty() -> RealityGraph {
25 RealityGraph {
26 repo_id: String::new(),
27 entities: HashMap::new(),
28 out: HashMap::new(),
29 inn: HashMap::new(),
30 components: Vec::new(),
31 flows: Vec::new(),
32 invariants: Vec::new(),
33 }
34 }
35}
36
37use scc_core::{Entity, Flow, Invariant, Relationship};
38use scc_store::Store;
39use std::collections::HashMap;
40
41#[derive(Debug, thiserror::Error)]
42pub enum GraphError {
44 #[error("store: {0}")]
45 Store(#[from] scc_store::StoreError),
46 #[error("cochange: {0}")]
47 Cochange(String),
48 #[error("impact: {0}")]
49 Impact(String),
50}
51
52pub type Result<T> = std::result::Result<T, GraphError>;
53
54pub struct RealityGraph {
57 pub repo_id: String,
58 pub entities: HashMap<String, Entity>,
59 pub out: HashMap<String, Vec<Relationship>>,
61 pub inn: HashMap<String, Vec<Relationship>>,
63 pub components: Vec<Entity>,
64 pub flows: Vec<Flow>,
65 pub invariants: Vec<Invariant>,
66}
67
68impl RealityGraph {
69 pub fn load(store: &Store) -> Result<RealityGraph> {
70 let mut entities = HashMap::new();
71 for e in store.all_entities()? {
72 entities.insert(e.id.clone(), e);
73 }
74 let mut out: HashMap<String, Vec<Relationship>> = HashMap::new();
75 let mut inn: HashMap<String, Vec<Relationship>> = HashMap::new();
76 for r in store.all_relationships()? {
77 out.entry(r.subject.clone()).or_default().push(r.clone());
78 inn.entry(r.object.clone()).or_default().push(r);
79 }
80 Ok(RealityGraph {
81 repo_id: store.repo_id.clone(),
82 entities,
83 out,
84 inn,
85 components: store.components()?,
86 flows: store.flows()?,
87 invariants: store.invariants()?,
88 })
89 }
90
91 pub fn entity(&self, id: &str) -> Option<&Entity> {
92 self.entities.get(id)
93 }
94
95 pub fn out_edges(&self, id: &str) -> Vec<&Relationship> {
96 self.out.get(id).map(|v| v.iter().collect()).unwrap_or_default()
97 }
98
99 pub fn in_edges(&self, id: &str) -> Vec<&Relationship> {
100 self.inn.get(id).map(|v| v.iter().collect()).unwrap_or_default()
101 }
102
103 pub fn out_pred(&self, id: &str, predicate: &str) -> Vec<&Relationship> {
104 self.out
105 .get(id)
106 .map(|v| {
107 v.iter()
108 .filter(|r| r.predicate == predicate)
109 .collect()
110 })
111 .unwrap_or_default()
112 }
113
114 pub fn in_pred(&self, id: &str, predicate: &str) -> Vec<&Relationship> {
115 self.inn
116 .get(id)
117 .map(|v| {
118 v.iter()
119 .filter(|r| r.predicate == predicate)
120 .collect()
121 })
122 .unwrap_or_default()
123 }
124
125 pub fn entities_of_kind(&self, kind: &str) -> Vec<&Entity> {
127 let mut v: Vec<&Entity> = self
128 .entities
129 .values()
130 .filter(|e| e.kind == kind)
131 .collect();
132 v.sort_by(|a, b| a.name.cmp(&b.name));
133 v
134 }
135}
136
137pub fn symbol_component_map(graph: &RealityGraph) -> HashMap<String, String> {
140 let mut symbol_comp: HashMap<String, String> = HashMap::new();
141 for c in &graph.components {
142 for r in graph.out_pred(&c.id, scc_core::predicates::CONTAINS) {
143 for sr in graph.out_pred(&r.object, scc_core::predicates::CONTAINS) {
144 symbol_comp.insert(sr.object.clone(), c.id.clone());
145 }
146 }
147 }
148 symbol_comp
149}
150
151pub struct CompilationPipeline<'a> {
160 store: &'a Store,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum CompilationStage {
165 Components,
166 Flows,
167 Behavior,
168 Invariants,
169 Drift,
170 Boundaries,
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
174pub struct StageCounts {
175 pub components: usize,
176 pub flows: usize,
177 pub invariants: usize,
178 pub drift: usize,
179 pub boundaries: usize,
180}
181
182impl<'a> CompilationPipeline<'a> {
183 pub fn new(store: &'a Store) -> CompilationPipeline<'a> {
184 CompilationPipeline { store }
185 }
186
187 pub fn run(self) -> Result<RecompileReport> {
188 self.store
190 .bump_epoch(scc_store::ModelEpochKind::Derived)?;
191
192 let graph = RealityGraph::load(self.store)?;
199 let intent = self.store.intent_claims()?;
200 let pairs = cochange::cached_cochange_pairs(self.store).unwrap_or_default();
206 let comps = components::compile_components(&graph, self.store, &intent, &pairs)?;
207 self.store.replace_components(&comps)?;
208 cochange::enrich_components(self.store, &pairs).map_err(GraphError::Cochange)?;
209
210 let graph = RealityGraph::load(self.store)?;
212 let (seq_flows, data_flows, arch_flow) =
213 flows::compile_flows(&graph, self.store, &intent)?;
214 let mut all = seq_flows;
215 all.extend(data_flows);
216 if let Some(a) = arch_flow {
217 all.push(a);
218 }
219 self.store.replace_flows(&all)?;
220
221 let graph = RealityGraph::load(self.store)?;
226 let symbol_comp = symbol_component_map(&graph);
227 let graphs = flowgraph::compile_flow_graphs(&graph, self.store, &intent, &symbol_comp)?;
228 self.store.replace_flow_graphs(&graphs)?;
229 let graph = RealityGraph::load(self.store)?;
230 let mut lifecycles = lifecycle::compile_lifecycles(&graph, self.store)?;
231 let mut workflows = workflow::compile_workflows(&graph, self.store)?;
232 all.append(&mut lifecycles);
233 all.append(&mut workflows);
234 self.store.replace_flows(&all)?;
235
236 let graph = RealityGraph::load(self.store)?;
238 let invs = invariants::compile_invariants(&graph, &intent)?;
239 self.store.replace_invariants(&invs)?;
240
241 let graph = RealityGraph::load(self.store)?;
244 let stored_comps = self.store.components()?;
245 let findings = invariants::drift_findings(&graph, self.store, &intent, &stored_comps)?;
246 self.store.clear_drift_findings()?;
247 for (kind, severity, message) in &findings {
248 self.store
249 .add_drift_finding(kind, severity, message)?;
250 }
251
252 let graph = RealityGraph::load(self.store)?;
254 let crossings = boundaries::compile_boundaries(&graph, self.store)?;
255 for (rel, src) in crossings {
256 self.store.insert_relationship(&rel, &src)?;
257 }
258
259 self.store.sweep_orphan_evidence()?;
262
263 Ok(RecompileReport {
264 components: comps.len(),
265 flows: all.len(),
266 invariants: invs.len(),
267 drift: findings.len(),
268 boundaries: stored_comps.len(),
269 })
270 }
271}
272
273pub fn recompile(store: &Store) -> Result<RecompileReport> {
278 CompilationPipeline::new(store).run()
279}
280
281#[derive(Debug, Clone, Default)]
282pub struct RecompileReport {
283 pub components: usize,
284 pub flows: usize,
285 pub invariants: usize,
286 pub drift: usize,
289 pub boundaries: usize,
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn load_empty_store() {
299 let dir = tempfile::TempDir::new().unwrap();
300 let root = dir.path().join("repo");
301 std::fs::create_dir_all(&root).unwrap();
302 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
303 let g = RealityGraph::load(&store).unwrap();
304 assert!(g.entities.is_empty());
305 let rep = recompile(&store).unwrap();
306 assert_eq!(rep.components, 1, "empty repos still get a root component");
307 }
308
309 #[test]
310 fn pipeline_bumps_derived_epoch_and_reloads_between_stages() {
311 let dir = tempfile::TempDir::new().unwrap();
312 let root = dir.path().join("repo");
313 std::fs::create_dir_all(&root).unwrap();
314 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
315
316 let epoch_before = store.model_epoch().unwrap();
317 let rep = CompilationPipeline::new(&store).run().unwrap();
318 let epoch_after = store.model_epoch().unwrap();
319
320 assert_eq!(epoch_after.derived, epoch_before.derived + 1);
323 assert!(rep.components >= 1);
324 assert_eq!(rep.drift, 0, "no drift on an empty model");
325
326 let rep2 = CompilationPipeline::new(&store).run().unwrap();
329 assert_eq!(rep2.components, rep.components);
330 assert_eq!(rep2.flows, rep.flows);
331 assert_eq!(rep2.invariants, rep.invariants);
332 }
333
334 #[test]
335 fn drift_uses_newly_compiled_flows() {
336 let dir = tempfile::TempDir::new().unwrap();
340 let root = dir.path().join("repo");
341 std::fs::create_dir_all(&root).unwrap();
342 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
343
344 let rep = CompilationPipeline::new(&store).run().unwrap();
346 let base_flows = rep.flows;
347 assert!(base_flows >= 1);
348
349 let repo = store.repo_id.clone();
351 let route = scc_core::entity_id(&repo, "route", "get-/api/x");
352 store
353 .insert_entity(
354 scc_core::Entity::new(route.clone(), "route", "get-/api/x")
355 .attr("method", serde_json::json!("GET"))
356 .attr("path", serde_json::json!("/api/x"))
357 .attr("handler", serde_json::json!("handle_x")),
358 &["main.py".into()],
359 )
360 .unwrap();
361 let sym = scc_core::symbol_id(&repo, "main.py", "handle_x");
362 store
363 .insert_entity(&scc_core::Entity::new(sym.clone(), "symbol", "handle_x"), &["main.py".into()])
364 .unwrap();
365 store
366 .insert_relationship(
367 &scc_core::Relationship::new(
368 "rel:route",
369 sym.clone(),
370 scc_core::predicates::HANDLES,
371 route,
372 scc_core::Provenance::Extracted,
373 ),
374 "main.py",
375 )
376 .unwrap();
377
378 let rep2 = CompilationPipeline::new(&store).run().unwrap();
381 assert!(rep2.flows > base_flows, "flows: {} vs {base_flows}", rep2.flows);
382 let flows = store.flows().unwrap();
383 assert!(
384 flows.iter().any(|f| f.name.contains("get-/api/x")),
385 "compiled flow must be present: {flows:?}"
386 );
387 }
388
389 fn git_init(dir: &std::path::Path) {
391 for args in [
392 vec!["init", "-q"],
393 vec!["config", "user.email", "test@example.com"],
394 vec!["config", "user.name", "SCC Test"],
395 vec!["config", "commit.gpgsign", "false"],
399 ] {
400 let out = std::process::Command::new("git")
401 .args(&args)
402 .current_dir(dir)
403 .output()
404 .unwrap();
405 assert!(out.status.success(), "git {args:?} failed");
406 }
407 }
408
409 fn git_commit_all(dir: &std::path::Path, msg: &str) {
410 let out = std::process::Command::new("git")
411 .args(["add", "-A"])
412 .current_dir(dir)
413 .output()
414 .unwrap();
415 assert!(out.status.success());
416 let out = std::process::Command::new("git")
417 .args(["commit", "-q", "-m", msg])
418 .current_dir(dir)
419 .output()
420 .unwrap();
421 assert!(out.status.success(), "git commit failed");
422 }
423
424 fn git_write(dir: &std::path::Path, name: &str, content: &str) {
425 let p = dir.join(name);
426 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
427 std::fs::write(p, content).unwrap();
428 }
429
430 #[test]
431 fn pipeline_wires_cochange_into_clustering() {
432 let dir = tempfile::TempDir::new().unwrap();
437 let root = dir.path().join("repo");
438 std::fs::create_dir_all(&root).unwrap();
439 git_init(&root);
440 git_write(&root, "src/a.py", "a = 1\n");
441 git_write(&root, "src/b.py", "b = 2\n");
442 git_commit_all(&root, "c1");
443 git_write(&root, "src/a.py", "a = 2\n");
444 git_write(&root, "src/b.py", "b = 3\n");
445 git_commit_all(&root, "c2");
446
447 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
448 for f in ["src/a.py", "src/b.py"] {
449 let id = scc_core::entity_id(&store.repo_id, scc_core::kinds::FILE, f);
450 store
451 .insert_entity(
452 &scc_core::Entity::new(id, scc_core::kinds::FILE, f),
453 &[f.into()],
454 )
455 .unwrap();
456 }
457 store
458 .replace_intent_claims(&[(
459 "component".to_string(),
460 serde_json::json!({"name": "core", "paths": ["src"]}),
461 )])
462 .unwrap();
463
464 let rep = CompilationPipeline::new(&store).run().unwrap();
465 assert!(rep.components >= 2, "root + core: {}", rep.components);
466
467 let comps = store.components().unwrap();
468 let core = comps.iter().find(|c| c.name == "core").unwrap();
469 assert_eq!(
470 core.attributes["boundary_kind"],
471 serde_json::json!("declared")
472 );
473 assert_eq!(
475 core.attributes["clustering_score"],
476 serde_json::json!(2.0),
477 "{:?}",
478 core.attributes
479 );
480 let cc = core.attributes["cochange"].clone();
482 assert_eq!(cc["top"], 2);
483 assert!(
484 cc["pairs"][0]
485 .as_str()
486 .unwrap()
487 .starts_with("src/a.py <-> src/b.py"),
488 "{cc}"
489 );
490 let rep2 = CompilationPipeline::new(&store).run().unwrap();
492 assert_eq!(rep2.components, rep.components);
493 let comps2 = store.components().unwrap();
494 let core2 = comps2.iter().find(|c| c.name == "core").unwrap();
495 assert_eq!(
496 core.attributes["clustering_score"],
497 core2.attributes["clustering_score"]
498 );
499 }
500}