1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
6pub 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
15pub 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)]
25pub struct InvariantViolation {
27 pub message: String,
28}
29
30pub 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
64pub 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
103pub 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
116pub 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}