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) {
126 const WANT: [&str; 2] = [".scc/*", "!.scc/intent.yaml"];
130 let gi = root.join(".gitignore");
131 let content = std::fs::read_to_string(&gi).unwrap_or_default();
132 let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
133 let mut changed = false;
134 lines.retain(|l| {
135 let bare = l.trim() == ".scc/" || l.trim() == ".scc";
136 if bare {
137 changed = true;
138 }
139 !bare
140 });
141 for line in WANT {
142 if !lines.iter().any(|l| l.trim() == line) {
143 lines.push(line.to_string());
144 changed = true;
145 }
146 }
147 if changed {
148 let mut out = lines.join("\n");
149 out.push('\n');
150 let _ = std::fs::write(&gi, out);
151 }
152}
153
154pub fn is_store_corruption(e: &CliError) -> bool {
160 match e {
161 CliError::Store(s) => Store::is_corruption(s),
162 CliError::Index(scc_indexer::IndexError::Store(s)) => Store::is_corruption(s),
163 CliError::Graph(scc_graph::GraphError::Store(s)) => Store::is_corruption(s),
164 _ => false,
165 }
166}
167
168pub fn resilient_index<T>(
172 root: &Path,
173 mut attempt: impl FnMut() -> Result<T>,
174) -> Result<T> {
175 match attempt() {
176 Ok(v) => Ok(v),
177 Err(e) if is_store_corruption(&e) => {
178 let q = Store::quarantine_db(&db_path(root))?;
179 report_quarantine(&Some(q));
180 attempt()
181 }
182 Err(e) => Err(e),
183 }
184}
185
186pub fn open_store_recovering(root: &Path) -> Result<(Store, Option<std::path::PathBuf>)> {
188 let dir = state_dir(root);
189 std::fs::create_dir_all(&dir)?;
190 Ok(Store::open_recovering(&db_path(root), root)?)
191}
192
193pub fn report_quarantine(quarantined: &Option<std::path::PathBuf>) {
195 if let Some(q) = quarantined {
196 eprintln!(
197 "warning: existing index was corrupt (malformed database); quarantined to {} and rebuilding from scratch.",
198 q.display()
199 );
200 }
201}
202
203pub fn recompile(store: &Store) -> Result<scc_graph::RecompileReport> {
205 Ok(scc_graph::recompile(store)?)
206}
207
208pub fn stale_paths(store: &Store) -> Result<Vec<String>> {
224 scc_engine::workspace::stale_paths(store).map_err(|e| CliError::Other(e.to_string()))
225}
226
227pub struct Compiler<'a> {
229 pub store: &'a Store,
230 pub graph: RealityGraph,
231 pub settings: scc_context::ContextSettings,
232 pub stale: Vec<String>,
233}
234
235pub fn compiler<'a>(
238 store: &'a Store,
239 config: &Config,
240 stale: Vec<String>,
241) -> Result<Compiler<'a>> {
242 let graph = RealityGraph::load(store)?;
243 let plugin_salt = scc_engine::workspace::cache_key_fragment(
246 &scc_engine::plugins::active(&store.root, config),
247 );
248 let settings = scc_context::ContextSettings {
249 startup_tokens: config.context.startup_tokens,
250 task_tokens: config.context.task_tokens,
251 atlas_tokens: config.context.atlas_tokens,
252 detail_tokens: config.context.detail_tokens,
253 include_low_confidence_inference: config.context.include_low_confidence_inference,
254 rank_salt: format!(
255 "{}:{}:{}:{}",
256 config.inference.enabled,
257 config.inference.embedding_model,
258 config.inference.rerank_model,
259 plugin_salt,
260 ),
261 pack_allocator: scc_context::PackAllocator::AdaptivePriority,
262 };
263 Ok(Compiler {
264 store,
265 graph,
266 settings,
267 stale,
268 })
269}
270
271impl Compiler<'_> {
273 pub fn ctx(&self) -> ContextCompiler<'_> {
276 ContextCompiler::new(
277 self.store,
278 &self.graph,
279 self.settings.clone(),
280 self.stale.clone(),
281 )
282 }
283}
284
285pub fn index_and_recompile(root: &Path, config: &Config) -> Result<scc_indexer::IndexReport> {
287 ensure_scc_ignored(root);
288 resilient_index(root, || {
289 let (store, quarantined) = open_store_recovering(root)?;
290 report_quarantine(&quarantined);
291 let indexer = scc_indexer::Indexer::new(store, config.clone());
292 let report = indexer.index()?;
293 let store = open_store(root)?;
294 if config.index.auto_resolve {
298 let _ = scc_indexer::resolver::resolve_repository(
299 &store,
300 root,
301 scc_indexer::resolver::MAX_CALL_SITES,
302 );
303 }
304 let unchanged = report.changed == 0 && report.removed == 0 && !config.index.auto_resolve;
313 let extractor_current = store
314 .revisions()
315 .map(|rs| {
316 rs.into_iter().last().map(|h| {
317 h.extractor_version
318 == format!(
319 "store:{};core:{}",
320 scc_store::SCHEMA_VERSION,
321 scc_core::SCHEMA_VERSION
322 )
323 })
324 .unwrap_or(false)
325 })
326 .unwrap_or(false);
327 if !(unchanged && extractor_current) {
328 recompile(&store)?;
329 }
330 let _ = store.record_current_revision_with_config(
339 &scc_indexer::semantic_config_hash(config),
340 )?;
341 Ok(report)
342 })
343}
344
345pub fn resolve_and_recompile(root: &Path) -> Result<scc_indexer::resolver::ResolveReport> {
349 let store = open_store(root)?;
350 let report = scc_indexer::resolver::resolve_repository(
351 &store,
352 root,
353 scc_indexer::resolver::MAX_CALL_SITES,
354 )
355 .map_err(CliError::Other)?;
356 recompile(&store)?;
357 Ok(report)
358}
359
360pub fn export_ir(store: &Store) -> Result<scc_core::SystemIr> {
366 let repository = store.repository();
367 let snapshot = store
368 .latest_snapshot()?
369 .unwrap_or(scc_core::Snapshot {
370 revision: "not-indexed".into(),
371 branch: None,
372 indexed_at: scc_core::now_rfc3339(),
373 });
374 let mut ir = scc_core::SystemIr::empty(repository, snapshot);
375 let mut seen = std::collections::HashSet::new();
378 for e in store.all_entities()? {
379 if seen.insert(e.id.clone()) {
380 ir.entities.push(e);
381 }
382 }
383 ir.relationships = store.all_relationships()?;
384 ir.flows = store.flows()?;
385 ir.invariants = store.invariants()?;
386 ir.evidence = store.all_evidence()?;
387 Ok(ir)
388}
389
390pub fn export_jsonl(ir: &scc_core::SystemIr) -> Result<Vec<String>> {
394 let mut out = Vec::new();
395 out.push(serde_json::to_string(&serde_json::json!({
396 "type": "repository", "repository": ir.repository
397 }))?);
398 out.push(serde_json::to_string(&serde_json::json!({
399 "type": "snapshot", "snapshot": ir.snapshot, "schema_version": ir.schema_version
400 }))?);
401 for e in &ir.entities {
402 out.push(serde_json::to_string(&serde_json::json!({"type": "entity", "entity": e}))?);
403 }
404 for r in &ir.relationships {
405 out.push(serde_json::to_string(&serde_json::json!({"type": "relationship", "relationship": r}))?);
406 }
407 for f in &ir.flows {
408 out.push(serde_json::to_string(&serde_json::json!({"type": "flow", "flow": f}))?);
409 }
410 for i in &ir.invariants {
411 out.push(serde_json::to_string(&serde_json::json!({"type": "invariant", "invariant": i}))?);
412 }
413 for e in &ir.evidence {
414 out.push(serde_json::to_string(&serde_json::json!({"type": "evidence", "evidence": e}))?);
415 }
416 Ok(out)
417}
418
419pub fn export_ccg(ir: &scc_core::SystemIr) -> Result<serde_json::Value> {
423 let l1: Vec<serde_json::Value> = ir
424 .entities
425 .iter()
426 .filter(|e| {
427 e.kind == kinds::COMPONENT
428 || e.kind == kinds::SERVICE
429 || e.kind == kinds::DATA_STORE
430 || e.kind == kinds::DEPLOYMENT_UNIT
431 || e.kind == kinds::EXTERNAL_API
432 })
433 .map(|e| {
434 serde_json::json!({
435 "id": e.id,
436 "name": e.name,
437 "kind": e.kind,
438 "attributes": e.attributes,
439 })
440 })
441 .collect();
442 let l2: Vec<serde_json::Value> = ir
443 .entities
444 .iter()
445 .filter(|e| e.kind == kinds::SYMBOL)
446 .map(|e| {
447 serde_json::json!({
448 "id": e.id,
449 "name": e.name,
450 "kind": e.attributes.get("kind").cloned().unwrap_or(serde_json::json!("symbol")),
451 "file": e.attributes.get("file").cloned().unwrap_or_default(),
452 })
453 })
454 .collect();
455 Ok(serde_json::json!({
456 "schema": "ccg",
457 "producer": "scc",
458 "repository": ir.repository,
459 "snapshot": ir.snapshot,
460 "layers": {
461 "L0": {
462 "manifest": {
463 "repository": ir.repository.name,
464 "revision": ir.snapshot.revision,
465 "entity_count": ir.entities.len(),
466 "relationship_count": ir.relationships.len(),
467 }
468 },
469 "L1": { "architecture": l1 },
470 "L2": { "symbols": l2 },
471 }
472 }))
473}
474
475pub fn flow_kind_str(k: &scc_core::FlowKind) -> &'static str {
477 match k {
478 scc_core::FlowKind::Architecture => "architecture",
479 scc_core::FlowKind::Workflow => "workflow",
480 scc_core::FlowKind::Sequence => "sequence",
481 scc_core::FlowKind::Dataflow => "dataflow",
482 scc_core::FlowKind::Lifecycle => "lifecycle",
483 }
484}
485
486pub use scc_core::kinds;
487
488pub fn relative_of(root: &Path, abs: &Path) -> Option<String> {
491 let root_c = root.canonicalize().ok()?;
492 let abs_c = abs.canonicalize().ok()?;
493 let rel = abs_c.strip_prefix(&root_c).ok()?;
494 if rel.as_os_str().is_empty() {
495 return None;
496 }
497 let s = rel.to_string_lossy().replace('\\', "/");
498 if s.starts_with(".scc/") {
499 return None;
500 }
501 Some(s)
502}