Skip to main content

scc_engine/
misc.rs

1//! Engine misc operations: drift, invariants, cochange, history,
2//! snapshots, lessons, import, runtime status. Value-returning; the CLI
3//! renders. Moved verbatim from `scc-cli` commands.rs.
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
6// trace:exempt reason=internal-detail
7pub struct DriftFinding {
8    pub id: i64,
9    pub kind: String,
10    pub severity: String,
11    pub message: String,
12    pub created_at: String,
13}
14
15// trace:exempt reason=internal-detail
16pub fn drift(store: &scc_store::Store) -> crate::Result<Vec<DriftFinding>> {
17    Ok(store
18        .drift_findings(false)?
19        .into_iter()
20        .map(|(id, kind, severity, message, created_at)| DriftFinding { id, kind, severity, message, created_at })
21        .collect())
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25// trace:exempt reason=internal-detail
26pub struct InvariantViolation {
27    pub message: String,
28}
29
30// trace:exempt reason=internal-detail
31pub fn check_invariants(store: &scc_store::Store) -> crate::Result<Vec<InvariantViolation>> {
32    use scc_core::kinds;
33    let graph = scc_graph::RealityGraph::load(store)?;
34    let mut out = Vec::new();
35    for r in graph.all_rels() {
36        let known = |id: &str| {
37            graph.entities.contains_key(id)
38                || id.contains("/external_api/")
39                || id.contains("/component/")
40                || id.contains("/flow/")
41                || id.contains("/invariant/")
42        };
43        if !known(&r.subject) {
44            out.push(InvariantViolation { message: format!("dangling subject: {} — {}", r.subject, r.predicate) });
45        }
46        if !known(&r.object) {
47            out.push(InvariantViolation { message: format!("dangling object: {} — {}", r.predicate, r.object) });
48        }
49    }
50    for r in graph.all_rels() {
51        if r.provenance == scc_core::Provenance::Resolved && r.evidence.is_empty() {
52            out.push(InvariantViolation { message: format!("RESOLVED without evidence: {} — {}", r.subject, r.predicate) });
53        }
54    }
55    for inv in store.invariants()? {
56        if inv.severity == scc_core::Severity::Critical && inv.enforced_by.is_empty() {
57            out.push(InvariantViolation { message: format!("critical invariant unenforced: {}", inv.statement) });
58        }
59    }
60    let _ = kinds::DATA_STORE;
61    Ok(out)
62}
63
64// trace:exempt reason=internal-detail
65pub fn ci_check(
66    store: &scc_store::Store,
67    violations: &[InvariantViolation],
68    max_severity: &str,
69) -> crate::Result<(bool, Vec<String>)> {
70    let mut lines = Vec::new();
71    let mut ok = violations.is_empty();
72    let allowed = match max_severity {
73        "low" => 1u8,
74        "medium" => 2u8,
75        "high" => 3u8,
76        "critical" => 4u8,
77        _ => 2u8,
78    };
79    for (_, kind, sev, msg, _) in store.drift_findings(true)? {
80        let rank = match sev.as_str() {
81            "low" => 1u8,
82            "medium" => 2u8,
83            "high" => 3u8,
84            "critical" => 4u8,
85            _ => 2u8,
86        };
87        if rank > allowed {
88            lines.push(format!("[ci:fail] [{sev}] {kind}: {msg}"));
89            ok = false;
90        } else {
91            lines.push(format!("[ci:warn] [{sev}] {kind}: {msg}"));
92        }
93    }
94    for v in violations {
95        lines.push(v.message.clone());
96    }
97    if ok {
98        lines.push("ci check passed".into());
99    }
100    Ok((ok, lines))
101}
102
103// trace:exempt reason=internal-detail
104pub fn cochange(root: &std::path::Path, min_commits: u32) -> crate::Result<(Vec<scc_graph::cochange::CochangePair>, usize)> {
105    let pairs = scc_graph::cochange::cochange_pairs(root, min_commits)
106        .map_err(crate::EngineError::Other)?;
107    let mut enriched = 0usize;
108    if crate::workspace::db_path(root).exists() {
109        let store = crate::workspace::open_store(root)?;
110        enriched = scc_graph::cochange::enrich_components(&store, &pairs)
111            .map_err(crate::EngineError::Other)?;
112    }
113    Ok((pairs, enriched))
114}
115
116// trace:exempt reason=internal-detail
117pub fn snapshot_save(
118    root: &std::path::Path,
119    task: &str,
120    budget: Option<usize>,
121) -> crate::Result<scc_store::snapshot::ContextSnapshot> {
122    let req = scc_api::TaskContextRequest { goal: task.into(), files: vec![], symbols: vec![], budget, hook: false, record_visibility: true };
123    let store = crate::workspace::open_store(root)?;
124    let config = crate::workspace::load_config(root)?;
125    let stale = crate::workspace::stale_paths(&store)?;
126    let engine = crate::workspace::open_engine(&store, &config, stale)?;
127    let artifact = crate::task::build_task_context(&engine, &config, root, &req, None, None)?;
128    let head = store.revisions()?.into_iter().last().map(|r| r.rev).unwrap_or(0);
129    let epoch = store.model_epoch()?.composite(&head.to_string());
130    let mut ids = artifact.pack.entity_ids.clone();
131    ids.extend(artifact.delta_ids.iter().cloned());
132    ids.sort();
133    ids.dedup();
134    Ok(store.save_snapshot(scc_store::snapshot::SnapshotSave {
135        task,
136        epoch: &epoch,
137        revision: head,
138        artifact: &format!("{}{}", artifact.pack.content, artifact.delta),
139        entity_ids: &ids,
140        budget: artifact.token_count,
141        warnings: &artifact.pack.warnings,
142    })?)
143}