1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
4pub struct Status {
6 pub repository: String,
7 pub repository_id: String,
8 pub remote: Option<String>,
9 pub revision: String,
10 pub branch: Option<String>,
11 pub indexed_at: Option<String>,
12 pub stats: std::collections::HashMap<String, u64>,
13 pub freshness: String,
14 pub stale_files: Vec<String>,
15 pub stale_count: usize,
16 pub analysis_quality: Option<String>,
17 pub scan_stats: Option<serde_json::Value>,
18 pub indexed: bool,
19}
20
21pub fn status(store: &scc_store::Store) -> crate::Result<Status> {
23 let repo = store.repository();
24 let stale = crate::workspace::stale_paths(store)?;
25 Ok(match store.snapshot_status()? {
26 Some((snap, _)) => Status {
27 repository: repo.name,
28 repository_id: repo.id,
29 remote: repo.url,
30 revision: snap.revision,
31 branch: snap.branch,
32 indexed_at: Some(snap.indexed_at),
33 stats: store.stats()?,
34 freshness: if stale.is_empty() { "CURRENT".into() } else { "STALE".into() },
35 stale_files: stale.iter().take(10).cloned().collect(),
36 stale_count: stale.len(),
37 analysis_quality: store.meta_get("analysis_quality")?,
38 scan_stats: store
39 .meta_get("scan_stats")?
40 .and_then(|raw| serde_json::from_str(&raw).ok()),
41 indexed: true,
42 },
43 None => Status {
44 repository: repo.name,
45 repository_id: repo.id,
46 remote: repo.url,
47 revision: "not-indexed".into(),
48 branch: None,
49 indexed_at: None,
50 stats: Default::default(),
51 freshness: "NOT-INDEXED".into(),
52 stale_files: vec![],
53 stale_count: 0,
54 analysis_quality: None,
55 scan_stats: None,
56 indexed: false,
57 },
58 })
59}
60
61pub fn scan(
66 root: &std::path::Path,
67 config: &scc_indexer::Config,
68 path: Option<&str>,
69) -> crate::Result<serde_json::Value> {
70 let exp = scc_indexer::scan::explain_scan(root, &config.index)
71 .map_err(|e| crate::EngineError::Other(e.to_string()))?;
72 let indexed: Vec<serde_json::Value> = exp
73 .indexed
74 .into_iter()
75 .filter(|f| path.filter(|p| !p.is_empty()).map(|p| f.path == p || f.path.starts_with(p)).unwrap_or(true))
76 .map(|f| {
77 serde_json::json!({"path": f.path, "language": f.language.as_str(), "kind": f.kind.as_str(), "bytes": f.size})
78 })
79 .collect();
80 const MAX_SKIPPED_DETAIL: usize = 5000;
85 let skipped_total = exp.skipped.len();
86 let skipped: Vec<serde_json::Value> = exp
87 .skipped
88 .into_iter()
89 .take(MAX_SKIPPED_DETAIL)
90 .map(|sk| serde_json::json!({"path": sk.path, "reason": sk.reason, "rule": sk.rule}))
91 .collect();
92 let skipped_capped = skipped_total.saturating_sub(skipped.len());
93 let st = &exp.stats;
94 Ok(serde_json::json!({"indexed": indexed, "skipped": skipped,
95 "skipped_detail_capped": skipped_capped,
96 "stats": {"discovered": st.discovered, "indexed": st.indexed, "ignored": st.ignored,
97 "unsupported": st.unsupported, "oversized": st.oversized, "unreadable": st.unreadable,
98 "symlink_escape": st.symlink_escape}}))
99}