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    Ok(ir)
30}
31
32// trace:exempt reason=internal-detail
33pub fn jsonl(ir: &scc_core::SystemIr) -> crate::Result<Vec<String>> {
34    let mut out = Vec::new();
35    out.push(serde_json::to_string(&serde_json::json!({
36        "type": "repository", "repository": ir.repository
37    }))?);
38    out.push(serde_json::to_string(&serde_json::json!({
39        "type": "snapshot", "snapshot": ir.snapshot, "schema_version": ir.schema_version
40    }))?);
41    for e in &ir.entities {
42        out.push(serde_json::to_string(&serde_json::json!({"type": "entity", "entity": e}))?);
43    }
44    for r in &ir.relationships {
45        out.push(serde_json::to_string(&serde_json::json!({"type": "relationship", "relationship": r}))?);
46    }
47    for f in &ir.flows {
48        out.push(serde_json::to_string(&serde_json::json!({"type": "flow", "flow": f}))?);
49    }
50    for i in &ir.invariants {
51        out.push(serde_json::to_string(&serde_json::json!({"type": "invariant", "invariant": i}))?);
52    }
53    for e in &ir.evidence {
54        out.push(serde_json::to_string(&serde_json::json!({"type": "evidence", "evidence": e}))?);
55    }
56    Ok(out)
57}
58
59// trace:exempt reason=internal-detail
60pub fn ccg(ir: &scc_core::SystemIr) -> crate::Result<serde_json::Value> {
61    let l1: Vec<serde_json::Value> = ir
62        .entities
63        .iter()
64        .filter(|e| {
65            e.kind == kinds::COMPONENT
66                || e.kind == kinds::SERVICE
67                || e.kind == kinds::DATA_STORE
68                || e.kind == kinds::DEPLOYMENT_UNIT
69                || e.kind == kinds::EXTERNAL_API
70        })
71        .map(|e| {
72            serde_json::json!({
73                "id": e.id,
74                "name": e.name,
75                "kind": e.kind,
76                "attributes": e.attributes,
77            })
78        })
79        .collect();
80    let l2: Vec<serde_json::Value> = ir
81        .entities
82        .iter()
83        .filter(|e| e.kind == kinds::SYMBOL)
84        .map(|e| {
85            serde_json::json!({
86                "id": e.id,
87                "name": e.name,
88                "kind": e.attributes.get("kind").cloned().unwrap_or(serde_json::json!("symbol")),
89                "file": e.attributes.get("file").cloned().unwrap_or_default(),
90            })
91        })
92        .collect();
93    Ok(serde_json::json!({
94        "schema": "ccg",
95        "producer": "scc",
96        "repository": ir.repository,
97        "snapshot": ir.snapshot,
98        "layers": {
99            "L0": {
100                "manifest": {
101                    "repository": ir.repository.name,
102                    "revision": ir.snapshot.revision,
103                    "entity_count": ir.entities.len(),
104                    "relationship_count": ir.relationships.len(),
105                }
106            },
107            "L1": { "architecture": l1 },
108            "L2": { "symbols": l2 },
109        }
110    }))
111}
112
113// trace:exempt reason=internal-detail
114pub fn capsule(root: &std::path::Path) -> crate::Result<String> {
115    let store = crate::workspace::open_store(root)?;
116    let config = crate::workspace::load_config(root)?;
117    let stale = crate::workspace::stale_paths(&store)?;
118    let engine = crate::workspace::open_engine(&store, &config, stale)?;
119    let overview = engine.context().overview()?;
120    let revision = overview.repository_revision.clone();
121    let repo = store.repository();
122    let paths: Vec<String> = store
123        .all_files()
124        .unwrap_or_default()
125        .into_iter()
126        .map(|(p, _, _, _, _)| p)
127        .collect();
128    let skeleton = scc_context::skeleton::build_skeleton(
129        &paths,
130        scc_context::skeleton::skeleton_budget(config.context.startup_tokens),
131    );
132    Ok(format!(
133        "<!-- 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",
134        repo.id,
135        revision,
136        scc_core::now_rfc3339(),
137        overview.content,
138        skeleton.text
139    ))
140}
141
142/// Complete live model (§35): repository, snapshot, epoch, files, symbols,
143/// entities, relationships, evidence, components, flows, flow graphs,
144/// invariants, and stats in one structured envelope.
145///
146/// This is the programmatic `give me everything SCC knows`; the export
147/// formats stay the interoperable representations. Sections reuse the same
148/// store getters as `system_ir` — one derivation, one envelope.
149// trace:v1 id=impl.scc-engine-exports.model-get work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
150pub fn model_get(store: &scc_store::Store) -> crate::Result<serde_json::Value> {
151    let ir = system_ir(store)?;
152    let (revision, indexed_at, branch) = match store.snapshot_status()? {
153        Some((snap, _)) => (snap.revision, Some(snap.indexed_at), snap.branch),
154        None => ("not-indexed".to_string(), None, None),
155    };
156    Ok(serde_json::json!({
157        "repository": store.repository(),
158        "revision": revision,
159        "branch": branch,
160        "indexed_at": indexed_at,
161        "epoch": store.model_epoch()?,
162        "stats": store.stats()?,
163        "files": store.all_files()?,
164        "entities": ir.entities,
165        "relationships": ir.relationships,
166        "evidence": ir.evidence,
167        "components": store.components()?,
168        "flows": store.flows()?,
169        "flow_graphs": store.flow_graphs()?,
170        "invariants": ir.invariants,
171    }))
172}
173
174/// Architecture diagram (spec catalog `export.diagram`): the deterministic
175/// L1 model plus Mermaid/SVG text. Pure Store derivation — every transport
176/// renders the same bytes from the same code.
177// trace:v1 id=impl.scc-engine-exports.diagram work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
178pub fn diagram(store: &scc_store::Store, format: &str) -> crate::Result<serde_json::Value> {
179    let model = crate::diagram::build_diagram_model(store)?;
180    let text = match format {
181        "mermaid" => crate::diagram::render_mermaid(&model),
182        "svg" => crate::diagram::render_svg(&model),
183        other => {
184            return Err(crate::EngineError::Other(format!(
185                "unknown diagram format '{other}' (use mermaid|svg)"
186            )))
187        }
188    };
189    Ok(serde_json::json!({
190        "format": format,
191        "text": text,
192        "nodes": model.nodes.len(),
193        "edges": model.edges.len(),
194        "flows": model.flows.len(),
195    }))
196}