pub mod archetype;
pub mod boundaries;
pub mod clustering;
pub mod cochange;
pub mod components;
pub mod flowgraph;
pub mod flows;
pub mod impact;
pub mod invariants;
pub mod lifecycle;
pub mod state;
pub mod trust;
pub mod workflow;
pub use trust::{TrustedGraphView, TrustPolicy};
impl RealityGraph {
pub fn empty() -> RealityGraph {
RealityGraph {
repo_id: String::new(),
entities: HashMap::new(),
out: HashMap::new(),
inn: HashMap::new(),
components: Vec::new(),
flows: Vec::new(),
invariants: Vec::new(),
}
}
}
use scc_core::{Entity, Flow, Invariant, Relationship};
use scc_store::Store;
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum GraphError {
#[error("store: {0}")]
Store(#[from] scc_store::StoreError),
#[error("cochange: {0}")]
Cochange(String),
#[error("impact: {0}")]
Impact(String),
}
pub type Result<T> = std::result::Result<T, GraphError>;
pub struct RealityGraph {
pub repo_id: String,
pub entities: HashMap<String, Entity>,
pub out: HashMap<String, Vec<Relationship>>,
pub inn: HashMap<String, Vec<Relationship>>,
pub components: Vec<Entity>,
pub flows: Vec<Flow>,
pub invariants: Vec<Invariant>,
}
impl RealityGraph {
pub fn load(store: &Store) -> Result<RealityGraph> {
let mut entities = HashMap::new();
for e in store.all_entities()? {
entities.insert(e.id.clone(), e);
}
let mut out: HashMap<String, Vec<Relationship>> = HashMap::new();
let mut inn: HashMap<String, Vec<Relationship>> = HashMap::new();
for r in store.all_relationships()? {
out.entry(r.subject.clone()).or_default().push(r.clone());
inn.entry(r.object.clone()).or_default().push(r);
}
Ok(RealityGraph {
repo_id: store.repo_id.clone(),
entities,
out,
inn,
components: store.components()?,
flows: store.flows()?,
invariants: store.invariants()?,
})
}
pub fn entity(&self, id: &str) -> Option<&Entity> {
self.entities.get(id)
}
pub fn out_edges(&self, id: &str) -> Vec<&Relationship> {
self.out.get(id).map(|v| v.iter().collect()).unwrap_or_default()
}
pub fn in_edges(&self, id: &str) -> Vec<&Relationship> {
self.inn.get(id).map(|v| v.iter().collect()).unwrap_or_default()
}
pub fn out_pred(&self, id: &str, predicate: &str) -> Vec<&Relationship> {
self.out
.get(id)
.map(|v| {
v.iter()
.filter(|r| r.predicate == predicate)
.collect()
})
.unwrap_or_default()
}
pub fn in_pred(&self, id: &str, predicate: &str) -> Vec<&Relationship> {
self.inn
.get(id)
.map(|v| {
v.iter()
.filter(|r| r.predicate == predicate)
.collect()
})
.unwrap_or_default()
}
pub fn entities_of_kind(&self, kind: &str) -> Vec<&Entity> {
let mut v: Vec<&Entity> = self
.entities
.values()
.filter(|e| e.kind == kind)
.collect();
v.sort_by(|a, b| a.name.cmp(&b.name));
v
}
}
pub fn symbol_component_map(graph: &RealityGraph) -> HashMap<String, String> {
let mut symbol_comp: HashMap<String, String> = HashMap::new();
for c in &graph.components {
for r in graph.out_pred(&c.id, scc_core::predicates::CONTAINS) {
for sr in graph.out_pred(&r.object, scc_core::predicates::CONTAINS) {
symbol_comp.insert(sr.object.clone(), c.id.clone());
}
}
}
symbol_comp
}
pub struct CompilationPipeline<'a> {
store: &'a Store,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompilationStage {
Components,
Flows,
Behavior,
Invariants,
Drift,
Boundaries,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StageCounts {
pub components: usize,
pub flows: usize,
pub invariants: usize,
pub drift: usize,
pub boundaries: usize,
}
impl<'a> CompilationPipeline<'a> {
pub fn new(store: &'a Store) -> CompilationPipeline<'a> {
CompilationPipeline { store }
}
pub fn run(self) -> Result<RecompileReport> {
self.store
.bump_epoch(scc_store::ModelEpochKind::Derived)?;
let graph = RealityGraph::load(self.store)?;
let intent = self.store.intent_claims()?;
let pairs = cochange::cached_cochange_pairs(self.store).unwrap_or_default();
let comps = components::compile_components(&graph, self.store, &intent, &pairs)?;
self.store.replace_components(&comps)?;
cochange::enrich_components(self.store, &pairs).map_err(GraphError::Cochange)?;
let graph = RealityGraph::load(self.store)?;
let (seq_flows, data_flows, arch_flow) =
flows::compile_flows(&graph, self.store, &intent)?;
let mut all = seq_flows;
all.extend(data_flows);
if let Some(a) = arch_flow {
all.push(a);
}
self.store.replace_flows(&all)?;
let graph = RealityGraph::load(self.store)?;
let symbol_comp = symbol_component_map(&graph);
let graphs = flowgraph::compile_flow_graphs(&graph, self.store, &intent, &symbol_comp)?;
self.store.replace_flow_graphs(&graphs)?;
let graph = RealityGraph::load(self.store)?;
let mut lifecycles = lifecycle::compile_lifecycles(&graph, self.store)?;
let mut workflows = workflow::compile_workflows(&graph, self.store)?;
all.append(&mut lifecycles);
all.append(&mut workflows);
self.store.replace_flows(&all)?;
let graph = RealityGraph::load(self.store)?;
let invs = invariants::compile_invariants(&graph, &intent)?;
self.store.replace_invariants(&invs)?;
let graph = RealityGraph::load(self.store)?;
let stored_comps = self.store.components()?;
let findings = invariants::drift_findings(&graph, self.store, &intent, &stored_comps)?;
self.store.clear_drift_findings()?;
for (kind, severity, message) in &findings {
self.store
.add_drift_finding(kind, severity, message)?;
}
let graph = RealityGraph::load(self.store)?;
let crossings = boundaries::compile_boundaries(&graph, self.store)?;
for (rel, src) in crossings {
self.store.insert_relationship(&rel, &src)?;
}
self.store.sweep_orphan_evidence()?;
Ok(RecompileReport {
components: comps.len(),
flows: all.len(),
invariants: invs.len(),
drift: findings.len(),
boundaries: stored_comps.len(),
})
}
}
pub fn recompile(store: &Store) -> Result<RecompileReport> {
CompilationPipeline::new(store).run()
}
#[derive(Debug, Clone, Default)]
pub struct RecompileReport {
pub components: usize,
pub flows: usize,
pub invariants: usize,
pub drift: usize,
pub boundaries: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_empty_store() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let g = RealityGraph::load(&store).unwrap();
assert!(g.entities.is_empty());
let rep = recompile(&store).unwrap();
assert_eq!(rep.components, 1, "empty repos still get a root component");
}
#[test]
fn pipeline_bumps_derived_epoch_and_reloads_between_stages() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let epoch_before = store.model_epoch().unwrap();
let rep = CompilationPipeline::new(&store).run().unwrap();
let epoch_after = store.model_epoch().unwrap();
assert_eq!(epoch_after.derived, epoch_before.derived + 1);
assert!(rep.components >= 1);
assert_eq!(rep.drift, 0, "no drift on an empty model");
let rep2 = CompilationPipeline::new(&store).run().unwrap();
assert_eq!(rep2.components, rep.components);
assert_eq!(rep2.flows, rep.flows);
assert_eq!(rep2.invariants, rep.invariants);
}
#[test]
fn drift_uses_newly_compiled_flows() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let rep = CompilationPipeline::new(&store).run().unwrap();
let base_flows = rep.flows;
assert!(base_flows >= 1);
let repo = store.repo_id.clone();
let route = scc_core::entity_id(&repo, "route", "get-/api/x");
store
.insert_entity(
scc_core::Entity::new(route.clone(), "route", "get-/api/x")
.attr("method", serde_json::json!("GET"))
.attr("path", serde_json::json!("/api/x"))
.attr("handler", serde_json::json!("handle_x")),
&["main.py".into()],
)
.unwrap();
let sym = scc_core::symbol_id(&repo, "main.py", "handle_x");
store
.insert_entity(&scc_core::Entity::new(sym.clone(), "symbol", "handle_x"), &["main.py".into()])
.unwrap();
store
.insert_relationship(
&scc_core::Relationship::new(
"rel:route",
sym.clone(),
scc_core::predicates::HANDLES,
route,
scc_core::Provenance::Extracted,
),
"main.py",
)
.unwrap();
let rep2 = CompilationPipeline::new(&store).run().unwrap();
assert!(rep2.flows > base_flows, "flows: {} vs {base_flows}", rep2.flows);
let flows = store.flows().unwrap();
assert!(
flows.iter().any(|f| f.name.contains("get-/api/x")),
"compiled flow must be present: {flows:?}"
);
}
fn git_init(dir: &std::path::Path) {
for args in [
vec!["init", "-q"],
vec!["config", "user.email", "test@example.com"],
vec!["config", "user.name", "SCC Test"],
vec!["config", "commit.gpgsign", "false"],
] {
let out = std::process::Command::new("git")
.args(&args)
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "git {args:?} failed");
}
}
fn git_commit_all(dir: &std::path::Path, msg: &str) {
let out = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success());
let out = std::process::Command::new("git")
.args(["commit", "-q", "-m", msg])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "git commit failed");
}
fn git_write(dir: &std::path::Path, name: &str, content: &str) {
let p = dir.join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, content).unwrap();
}
#[test]
fn pipeline_wires_cochange_into_clustering() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
git_init(&root);
git_write(&root, "src/a.py", "a = 1\n");
git_write(&root, "src/b.py", "b = 2\n");
git_commit_all(&root, "c1");
git_write(&root, "src/a.py", "a = 2\n");
git_write(&root, "src/b.py", "b = 3\n");
git_commit_all(&root, "c2");
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
for f in ["src/a.py", "src/b.py"] {
let id = scc_core::entity_id(&store.repo_id, scc_core::kinds::FILE, f);
store
.insert_entity(
&scc_core::Entity::new(id, scc_core::kinds::FILE, f),
&[f.into()],
)
.unwrap();
}
store
.replace_intent_claims(&[(
"component".to_string(),
serde_json::json!({"name": "core", "paths": ["src"]}),
)])
.unwrap();
let rep = CompilationPipeline::new(&store).run().unwrap();
assert!(rep.components >= 2, "root + core: {}", rep.components);
let comps = store.components().unwrap();
let core = comps.iter().find(|c| c.name == "core").unwrap();
assert_eq!(
core.attributes["boundary_kind"],
serde_json::json!("declared")
);
assert_eq!(
core.attributes["clustering_score"],
serde_json::json!(2.0),
"{:?}",
core.attributes
);
let cc = core.attributes["cochange"].clone();
assert_eq!(cc["top"], 2);
assert!(
cc["pairs"][0]
.as_str()
.unwrap()
.starts_with("src/a.py <-> src/b.py"),
"{cc}"
);
let rep2 = CompilationPipeline::new(&store).run().unwrap();
assert_eq!(rep2.components, rep.components);
let comps2 = store.components().unwrap();
let core2 = comps2.iter().find(|c| c.name == "core").unwrap();
assert_eq!(
core.attributes["clustering_score"],
core2.attributes["clustering_score"]
);
}
}