1pub mod agents_md;
4pub mod bench;
5pub mod benchagent;
6pub mod benchatlas;
7pub mod benchctx;
8pub mod benchloop;
9pub mod benchres;
10pub mod benchret;
11pub mod commands;
12pub mod compress;
13pub mod embed_cli;
14pub mod httpd;
15pub mod mcp;
16pub mod plugin;
17pub mod plugin_hermes;
18pub mod plugin_omp;
19pub mod viewer;
20pub mod resolve;
21
22use scc_context::ContextCompiler;
23use scc_graph::RealityGraph;
24use scc_indexer::Config;
25use scc_store::Store;
26use std::path::{Path, PathBuf};
27
28pub const SCC_DIR: &str = ".scc";
29pub const DB_FILE: &str = "scc.db";
30pub const CONFIG_FILE: &str = "config.yaml";
31pub const CHECKPOINT_FILE: &str = "checkpoint.json";
32
33#[derive(Debug, thiserror::Error)]
34pub enum CliError {
35 #[error("store: {0}")]
36 Store(#[from] scc_store::StoreError),
37 #[error("index: {0}")]
38 Index(#[from] scc_indexer::IndexError),
39 #[error("graph: {0}")]
40 Graph(#[from] scc_graph::GraphError),
41 #[error("io: {0}")]
42 Io(#[from] std::io::Error),
43 #[error("config: {0}")]
44 Config(#[from] scc_indexer::config::ConfigError),
45 #[error("json: {0}")]
46 Json(#[from] serde_json::Error),
47 #[error("{0}")]
48 Other(String),
49}
50
51pub type Result<T> = std::result::Result<T, CliError>;
52
53pub fn state_dir(root: &Path) -> PathBuf {
59 match std::env::var("SCC_STATE_DIR") {
60 Ok(dir) if !dir.is_empty() => PathBuf::from(dir),
61 _ => scc_dir(root),
62 }
63}
64
65pub fn scc_dir(root: &Path) -> PathBuf {
67 root.join(SCC_DIR)
68}
69
70pub fn db_path(root: &Path) -> PathBuf {
72 state_dir(root).join(DB_FILE)
73}
74
75pub fn config_path(root: &Path) -> PathBuf {
79 scc_dir(root).join(CONFIG_FILE)
80}
81
82pub fn checkpoint_path(root: &Path) -> PathBuf {
84 state_dir(root).join(CHECKPOINT_FILE)
85}
86
87pub fn find_root(start: &Path) -> PathBuf {
91 let mut dir = Some(start.to_path_buf());
92 while let Some(d) = dir {
93 if d.join(".git").exists() || d.join(SCC_DIR).exists() {
94 return d;
95 }
96 dir = d.parent().map(|p| p.to_path_buf());
97 }
98 start.to_path_buf()
99}
100
101pub fn load_config(root: &Path) -> Result<Config> {
103 let p = config_path(root);
104 if p.exists() {
105 Ok(Config::load(&p)?)
106 } else {
107 Ok(Config::default())
108 }
109}
110
111pub fn open_store(root: &Path) -> Result<Store> {
113 let dir = state_dir(root);
114 std::fs::create_dir_all(&dir)?;
115 Ok(Store::open(&db_path(root), root)?)
116}
117
118pub fn ensure_scc_ignored(root: &Path) {
124 scc_engine::workspace::ensure_scc_ignored(root)
125}
126
127pub fn is_store_corruption(e: &CliError) -> bool {
133 match e {
134 CliError::Store(s) => Store::is_corruption(s),
135 CliError::Index(scc_indexer::IndexError::Store(s)) => Store::is_corruption(s),
136 CliError::Graph(scc_graph::GraphError::Store(s)) => Store::is_corruption(s),
137 _ => false,
138 }
139}
140
141pub fn resilient_index<T>(
145 root: &Path,
146 mut attempt: impl FnMut() -> Result<T>,
147) -> Result<T> {
148 match attempt() {
149 Ok(v) => Ok(v),
150 Err(e) if is_store_corruption(&e) => {
151 let q = Store::quarantine_db(&db_path(root))?;
152 report_quarantine(&Some(q));
153 attempt()
154 }
155 Err(e) => Err(e),
156 }
157}
158
159pub fn open_store_recovering(root: &Path) -> Result<(Store, Option<std::path::PathBuf>)> {
161 let dir = state_dir(root);
162 std::fs::create_dir_all(&dir)?;
163 Ok(Store::open_recovering(&db_path(root), root)?)
164}
165
166pub fn report_quarantine(quarantined: &Option<std::path::PathBuf>) {
168 if let Some(q) = quarantined {
169 eprintln!(
170 "warning: existing index was corrupt (malformed database); quarantined to {} and rebuilding from scratch.",
171 q.display()
172 );
173 }
174}
175
176pub fn recompile(store: &Store) -> Result<scc_graph::RecompileReport> {
178 Ok(scc_graph::recompile(store)?)
179}
180
181pub fn stale_paths(store: &Store) -> Result<Vec<String>> {
197 scc_engine::workspace::stale_paths(store).map_err(|e| CliError::Other(e.to_string()))
198}
199
200pub struct Compiler<'a> {
202 pub store: &'a Store,
203 pub graph: RealityGraph,
204 pub settings: scc_context::ContextSettings,
205 pub stale: Vec<String>,
206}
207
208pub fn compiler<'a>(
211 store: &'a Store,
212 config: &Config,
213 stale: Vec<String>,
214) -> Result<Compiler<'a>> {
215 let graph = RealityGraph::load(store)?;
216 let plugin_salt = scc_engine::workspace::cache_key_fragment(
219 &scc_engine::plugins::active(&store.root, config),
220 );
221 let settings = scc_context::ContextSettings {
222 startup_tokens: config.context.startup_tokens,
223 task_tokens: config.context.task_tokens,
224 atlas_tokens: config.context.atlas_tokens,
225 detail_tokens: config.context.detail_tokens,
226 include_low_confidence_inference: config.context.include_low_confidence_inference,
227 rank_salt: format!(
228 "{}:{}:{}:{}",
229 config.inference.enabled,
230 config.inference.embedding_model,
231 config.inference.rerank_model,
232 plugin_salt,
233 ),
234 pack_allocator: scc_context::PackAllocator::AdaptivePriority,
235 };
236 Ok(Compiler {
237 store,
238 graph,
239 settings,
240 stale,
241 })
242}
243
244impl Compiler<'_> {
246 pub fn ctx(&self) -> ContextCompiler<'_> {
249 ContextCompiler::new(
250 self.store,
251 &self.graph,
252 self.settings.clone(),
253 self.stale.clone(),
254 )
255 }
256}
257
258pub fn index_and_recompile(root: &Path, config: &Config) -> Result<scc_indexer::IndexReport> {
260 ensure_scc_ignored(root);
261 resilient_index(root, || {
262 let (store, quarantined) = open_store_recovering(root)?;
263 report_quarantine(&quarantined);
264 let indexer = scc_indexer::Indexer::new(store, config.clone());
265 let report = indexer.index()?;
266 let store = open_store(root)?;
267 if config.index.auto_resolve {
271 let _ = scc_indexer::resolver::resolve_repository(
272 &store,
273 root,
274 scc_indexer::resolver::MAX_CALL_SITES,
275 );
276 }
277 let unchanged = report.changed == 0 && report.removed == 0 && !config.index.auto_resolve;
286 let extractor_current = store
287 .revisions()
288 .map(|rs| {
289 rs.into_iter().last().map(|h| {
290 h.extractor_version
291 == format!(
292 "store:{};core:{}",
293 scc_store::SCHEMA_VERSION,
294 scc_core::SCHEMA_VERSION
295 )
296 })
297 .unwrap_or(false)
298 })
299 .unwrap_or(false);
300 if !(unchanged && extractor_current) {
301 recompile(&store)?;
302 }
303 let _ = store.record_current_revision_with_config(
312 &scc_indexer::semantic_config_hash(config),
313 )?;
314 let _ = store.prune_revisions(config.history.max_revisions);
316 Ok(report)
317 })
318}
319
320pub fn resolve_and_recompile(root: &Path) -> Result<scc_indexer::resolver::ResolveReport> {
324 let store = open_store(root)?;
325 let report = scc_indexer::resolver::resolve_repository(
326 &store,
327 root,
328 scc_indexer::resolver::MAX_CALL_SITES,
329 )
330 .map_err(CliError::Other)?;
331 recompile(&store)?;
332 Ok(report)
333}
334
335pub fn export_ir(store: &Store) -> Result<scc_core::SystemIr> {
341 let repository = store.repository();
342 let snapshot = store
343 .latest_snapshot()?
344 .unwrap_or(scc_core::Snapshot {
345 revision: "not-indexed".into(),
346 branch: None,
347 indexed_at: scc_core::now_rfc3339(),
348 });
349 let mut ir = scc_core::SystemIr::empty(repository, snapshot);
350 let mut seen = std::collections::HashSet::new();
353 for e in store.all_entities()? {
354 if seen.insert(e.id.clone()) {
355 ir.entities.push(e);
356 }
357 }
358 ir.relationships = store.all_relationships()?;
359 ir.flows = store.flows()?;
360 ir.invariants = store.invariants()?;
361 ir.evidence = store.all_evidence()?;
362 Ok(ir)
363}
364
365pub fn export_jsonl(ir: &scc_core::SystemIr) -> Result<Vec<String>> {
369 let mut out = Vec::new();
370 out.push(serde_json::to_string(&serde_json::json!({
371 "type": "repository", "repository": ir.repository
372 }))?);
373 out.push(serde_json::to_string(&serde_json::json!({
374 "type": "snapshot", "snapshot": ir.snapshot, "schema_version": ir.schema_version
375 }))?);
376 for e in &ir.entities {
377 out.push(serde_json::to_string(&serde_json::json!({"type": "entity", "entity": e}))?);
378 }
379 for r in &ir.relationships {
380 out.push(serde_json::to_string(&serde_json::json!({"type": "relationship", "relationship": r}))?);
381 }
382 for f in &ir.flows {
383 out.push(serde_json::to_string(&serde_json::json!({"type": "flow", "flow": f}))?);
384 }
385 for i in &ir.invariants {
386 out.push(serde_json::to_string(&serde_json::json!({"type": "invariant", "invariant": i}))?);
387 }
388 for e in &ir.evidence {
389 out.push(serde_json::to_string(&serde_json::json!({"type": "evidence", "evidence": e}))?);
390 }
391 Ok(out)
392}
393
394pub fn export_ccg(ir: &scc_core::SystemIr) -> Result<serde_json::Value> {
398 let l1: Vec<serde_json::Value> = ir
399 .entities
400 .iter()
401 .filter(|e| {
402 e.kind == kinds::COMPONENT
403 || e.kind == kinds::SERVICE
404 || e.kind == kinds::DATA_STORE
405 || e.kind == kinds::DEPLOYMENT_UNIT
406 || e.kind == kinds::EXTERNAL_API
407 })
408 .map(|e| {
409 serde_json::json!({
410 "id": e.id,
411 "name": e.name,
412 "kind": e.kind,
413 "attributes": e.attributes,
414 })
415 })
416 .collect();
417 let l2: Vec<serde_json::Value> = ir
418 .entities
419 .iter()
420 .filter(|e| e.kind == kinds::SYMBOL)
421 .map(|e| {
422 serde_json::json!({
423 "id": e.id,
424 "name": e.name,
425 "kind": e.attributes.get("kind").cloned().unwrap_or(serde_json::json!("symbol")),
426 "file": e.attributes.get("file").cloned().unwrap_or_default(),
427 })
428 })
429 .collect();
430 Ok(serde_json::json!({
431 "schema": "ccg",
432 "producer": "scc",
433 "repository": ir.repository,
434 "snapshot": ir.snapshot,
435 "layers": {
436 "L0": {
437 "manifest": {
438 "repository": ir.repository.name,
439 "revision": ir.snapshot.revision,
440 "entity_count": ir.entities.len(),
441 "relationship_count": ir.relationships.len(),
442 }
443 },
444 "L1": { "architecture": l1 },
445 "L2": { "symbols": l2 },
446 }
447 }))
448}
449
450pub fn flow_kind_str(k: &scc_core::FlowKind) -> &'static str {
452 match k {
453 scc_core::FlowKind::Architecture => "architecture",
454 scc_core::FlowKind::Workflow => "workflow",
455 scc_core::FlowKind::Sequence => "sequence",
456 scc_core::FlowKind::Dataflow => "dataflow",
457 scc_core::FlowKind::Lifecycle => "lifecycle",
458 }
459}
460
461pub use scc_core::kinds;
462
463pub fn relative_of(root: &Path, abs: &Path) -> Option<String> {
466 let root_c = root.canonicalize().ok()?;
467 let abs_c = abs.canonicalize().ok()?;
468 let rel = abs_c.strip_prefix(&root_c).ok()?;
469 if rel.as_os_str().is_empty() {
470 return None;
471 }
472 let s = rel.to_string_lossy().replace('\\', "/");
473 if s.starts_with(".scc/") {
474 return None;
475 }
476 Some(s)
477}