Skip to main content

scc_engine/
exports.rs

1//! Engine export operations: System IR, JSONL, CCG, capsule.
2//!
3//! Moved verbatim from `scc-cli` lib.rs / compress.rs. The CLI wrappers
4//! delegate; derivation is byte-identical.
5
6use scc_core::kinds;
7
8// trace:exempt reason=internal-detail
9pub fn system_ir(store: &scc_store::Store) -> crate::Result<scc_core::SystemIr> {
10    let repository = store.repository();
11    let snapshot = store
12        .latest_snapshot()?
13        .unwrap_or(scc_core::Snapshot {
14            revision: "not-indexed".into(),
15            branch: None,
16            indexed_at: scc_core::now_rfc3339(),
17        });
18    let mut ir = scc_core::SystemIr::empty(repository, snapshot);
19    let mut seen = std::collections::HashSet::new();
20    for e in store.all_entities()? {
21        if seen.insert(e.id.clone()) {
22            ir.entities.push(e);
23        }
24    }
25    ir.relationships = store.all_relationships()?;
26    ir.flows = store.flows()?;
27    ir.invariants = store.invariants()?;
28    ir.evidence = store.all_evidence()?;
29    // §106: preserve plugin facts under a versioned extensions section.
30    // A fact belongs to provider P when its kind/predicate is namespaced
31    // `plugin:P/...` or any evidence id starts with `plugin:P`.
32    let mut ext: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
33    let mut note = |provider: &str, id: &str| {
34        ext.entry(provider.to_string()).or_default().push(id.to_string());
35    };
36    for en in &ir.entities {
37        if let Some(rest) = en.kind.strip_prefix("plugin:") {
38            let p = rest.split('/').next().unwrap_or(rest);
39            note(p, &en.id);
40        }
41        for ev in &en.evidence {
42            if let Some(rest) = ev.strip_prefix("plugin:") {
43                let p = rest.split('/').next().unwrap_or(rest);
44                note(p, &en.id);
45            }
46        }
47    }
48    for r in &ir.relationships {
49        if let Some(rest) = r.predicate.strip_prefix("plugin:") {
50            let p = rest.split('/').next().unwrap_or(rest);
51            note(p, &r.id);
52        }
53        for ev in &r.evidence {
54            if let Some(rest) = ev.strip_prefix("plugin:") {
55                let p = rest.split('/').next().unwrap_or(rest);
56                note(p, &r.id);
57            }
58        }
59    }
60    for v in ext.values_mut() {
61        v.sort();
62        v.dedup();
63    }
64    ir.extensions = ext;
65    Ok(ir)
66}
67
68// trace:exempt reason=internal-detail
69pub fn jsonl(ir: &scc_core::SystemIr) -> crate::Result<Vec<String>> {
70    let mut out = Vec::new();
71    out.push(serde_json::to_string(&serde_json::json!({
72        "type": "repository", "repository": ir.repository
73    }))?);
74    out.push(serde_json::to_string(&serde_json::json!({
75        "type": "snapshot", "snapshot": ir.snapshot, "schema_version": ir.schema_version
76    }))?);
77    for e in &ir.entities {
78        out.push(serde_json::to_string(&serde_json::json!({"type": "entity", "entity": e}))?);
79    }
80    for r in &ir.relationships {
81        out.push(serde_json::to_string(&serde_json::json!({"type": "relationship", "relationship": r}))?);
82    }
83    for f in &ir.flows {
84        out.push(serde_json::to_string(&serde_json::json!({"type": "flow", "flow": f}))?);
85    }
86    for i in &ir.invariants {
87        out.push(serde_json::to_string(&serde_json::json!({"type": "invariant", "invariant": i}))?);
88    }
89    for e in &ir.evidence {
90        out.push(serde_json::to_string(&serde_json::json!({"type": "evidence", "evidence": e}))?);
91    }
92    Ok(out)
93}
94
95// trace:exempt reason=internal-detail
96pub fn ccg(ir: &scc_core::SystemIr) -> crate::Result<serde_json::Value> {
97    let l1: Vec<serde_json::Value> = ir
98        .entities
99        .iter()
100        .filter(|e| {
101            e.kind == kinds::COMPONENT
102                || e.kind == kinds::SERVICE
103                || e.kind == kinds::DATA_STORE
104                || e.kind == kinds::DEPLOYMENT_UNIT
105                || e.kind == kinds::EXTERNAL_API
106        })
107        .map(|e| {
108            serde_json::json!({
109                "id": e.id,
110                "name": e.name,
111                "kind": e.kind,
112                "attributes": e.attributes,
113            })
114        })
115        .collect();
116    let l2: Vec<serde_json::Value> = ir
117        .entities
118        .iter()
119        .filter(|e| e.kind == kinds::SYMBOL)
120        .map(|e| {
121            serde_json::json!({
122                "id": e.id,
123                "name": e.name,
124                "kind": e.attributes.get("kind").cloned().unwrap_or(serde_json::json!("symbol")),
125                "file": e.attributes.get("file").cloned().unwrap_or_default(),
126            })
127        })
128        .collect();
129    Ok(serde_json::json!({
130        "schema": "ccg",
131        "producer": "scc",
132        "repository": ir.repository,
133        "snapshot": ir.snapshot,
134        "layers": {
135            "L0": {
136                "manifest": {
137                    "repository": ir.repository.name,
138                    "revision": ir.snapshot.revision,
139                    "entity_count": ir.entities.len(),
140                    "relationship_count": ir.relationships.len(),
141                }
142            },
143            "L1": { "architecture": l1 },
144            "L2": { "symbols": l2 },
145        }
146    }))
147}
148
149// trace:exempt reason=internal-detail
150pub fn capsule(root: &std::path::Path) -> crate::Result<String> {
151    let store = crate::workspace::open_store(root)?;
152    let config = crate::workspace::load_config(root)?;
153    let stale = crate::workspace::stale_paths(&store)?;
154    let engine = crate::workspace::open_engine(&store, &config, stale)?;
155    let overview = engine.context().overview()?;
156    let revision = overview.repository_revision.clone();
157    let repo = store.repository();
158    let paths: Vec<String> = store
159        .all_files()
160        .unwrap_or_default()
161        .into_iter()
162        .map(|(p, _, _, _, _)| p)
163        .collect();
164    let skeleton = scc_context::skeleton::build_skeleton(
165        &paths,
166        scc_context::skeleton::skeleton_budget(config.context.startup_tokens),
167    );
168    Ok(format!(
169        "<!-- SCC-CAPSULE v1 repo={} revision={} generated={} -->\n# SYSTEM CAPSULE\n\n{}\n## REPOSITORY SKELETON\n\n{}\n\n(Physical layout above; run `scc context startup` for the fused live architecture.)\n",
170        repo.id,
171        revision,
172        scc_core::now_rfc3339(),
173        overview.content,
174        skeleton.text
175    ))
176}
177
178/// Complete live model (§35): repository, snapshot, epoch, files, symbols,
179/// entities, relationships, evidence, components, flows, flow graphs,
180/// invariants, and stats in one structured envelope.
181///
182/// This is the programmatic `give me everything SCC knows`; the export
183/// formats stay the interoperable representations. Sections reuse the same
184/// store getters as `system_ir` — one derivation, one envelope.
185// trace:v1 id=impl.scc-engine-exports.model-get work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
186pub fn model_get(store: &scc_store::Store) -> crate::Result<serde_json::Value> {
187    let ir = system_ir(store)?;
188    let (revision, indexed_at, branch) = match store.snapshot_status()? {
189        Some((snap, _)) => (snap.revision, Some(snap.indexed_at), snap.branch),
190        None => ("not-indexed".to_string(), None, None),
191    };
192    Ok(serde_json::json!({
193        "repository": store.repository(),
194        "revision": revision,
195        "branch": branch,
196        "indexed_at": indexed_at,
197        "epoch": store.model_epoch()?,
198        "stats": store.stats()?,
199        "files": store.all_files()?,
200        "entities": ir.entities,
201        "relationships": ir.relationships,
202        "evidence": ir.evidence,
203        "components": store.components()?,
204        "flows": store.flows()?,
205        "flow_graphs": store.flow_graphs()?,
206        "invariants": ir.invariants,
207    }))
208}
209
210/// Architecture diagram (spec catalog `export.diagram`): the deterministic
211/// L1 model plus Mermaid/SVG text. Pure Store derivation — every transport
212/// renders the same bytes from the same code.
213// trace:v1 id=impl.scc-engine-exports.diagram work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
214pub fn diagram(store: &scc_store::Store, format: &str) -> crate::Result<serde_json::Value> {
215    let model = crate::diagram::build_diagram_model(store)?;
216    let text = match format {
217        "mermaid" => crate::diagram::render_mermaid(&model),
218        "svg" => crate::diagram::render_svg(&model),
219        other => {
220            return Err(crate::EngineError::Other(format!(
221                "unknown diagram format '{other}' (use mermaid|svg)"
222            )))
223        }
224    };
225    Ok(serde_json::json!({
226        "format": format,
227        "text": text,
228        "nodes": model.nodes.len(),
229        "edges": model.edges.len(),
230        "flows": model.flows.len(),
231    }))
232}