Skip to main content

scc_engine/
status.rs

1//! Engine status: the `scc status` value (no println).
2
3#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
4// trace:exempt reason=internal-detail
5pub 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
21// trace:exempt reason=internal-detail
22pub 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
61/// Scan explanation: which files the indexer would index and why
62/// (languages, ignore rules, budgets). The `scc scan` value — transports
63/// render it; the engine owns the derivation.
64// trace:v1 id=impl.scc-engine-status.scan work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
65pub 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    // Scale cap (mockingbird receipt: 100k skipped rows serialize to
81    // multi-MB JSON and dominate `scc scan` render). Counts in stats stay
82    // exact; only the per-file detail list is capped, with the cap
83    // disclosed in the payload so no one mistakes it for completeness.
84    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}